CRLF ?????

This commit is contained in:
Ismaël Moret
2026-02-20 12:03:14 +00:00
parent ad1e37988d
commit 703cf94d19
4 changed files with 302 additions and 302 deletions

View File

@@ -1,135 +1,135 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const axios = require('axios') const axios = require('axios')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const customParseFormat = require('dayjs/plugin/customParseFormat') const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.tz.setDefault('Europe/Paris') dayjs.tz.setDefault('Europe/Paris')
// Because France is excellent at pointing hours, their programs ALL start at 5/6 am, // Because France is excellent at pointing hours, their programs ALL start at 5/6 am,
// so we need to keep track of the earlier day's program to get the midnight programming. How... odd. // so we need to keep track of the earlier day's program to get the midnight programming. How... odd.
module.exports = { module.exports = {
site: 'france.tv', site: 'france.tv',
days: 2, days: 2,
url: function ({ channel, date }) { url: function ({ channel, date }) {
return `https://www.france.tv/api/epg/videos/?date=${date.format('YYYY-MM-DD')}&channel=${channel.site_id}` return `https://www.france.tv/api/epg/videos/?date=${date.format('YYYY-MM-DD')}&channel=${channel.site_id}`
}, },
parser: async function ({ channel, content, date }) { parser: async function ({ channel, content, date }) {
const programs = [] const programs = []
let items = [] let items = []
const dayBefore = date.subtract(1, 'd').format('YYYY-MM-DD') const dayBefore = date.subtract(1, 'd').format('YYYY-MM-DD')
const linkDayBefore = `https://www.france.tv/api/epg/videos/?date=${dayBefore}&channel=${channel.site_id}` const linkDayBefore = `https://www.france.tv/api/epg/videos/?date=${dayBefore}&channel=${channel.site_id}`
try { try {
const responseDayBefore = await axios.get(linkDayBefore) const responseDayBefore = await axios.get(linkDayBefore)
const programmingDayBefore = responseDayBefore.data || [] const programmingDayBefore = responseDayBefore.data || []
// The broadcast day starts at ~6 AM. Programs with hour < 6 in the day-before API // The broadcast day starts at ~6 AM. Programs with hour < 6 in the day-before API
// are actually early morning programs (00:00-05:59) of our target date. // are actually early morning programs (00:00-05:59) of our target date.
if (Array.isArray(programmingDayBefore)) { if (Array.isArray(programmingDayBefore)) {
programmingDayBefore.forEach(item => { programmingDayBefore.forEach(item => {
const time = item?.content?.broadcastBeginDate const time = item?.content?.broadcastBeginDate
if (!time) return if (!time) return
const hour = parseInt(time.split('h')[0]) const hour = parseInt(time.split('h')[0])
if (hour < 6) { if (hour < 6) {
items.push(item) items.push(item)
} }
}) })
} }
} catch { } catch {
// Day before data unavailable, continue with current day only // Day before data unavailable, continue with current day only
} }
// From the current day's API, only include programs starting from 6h onwards. // From the current day's API, only include programs starting from 6h onwards.
// Programs with hour < 6 belong to the next calendar day's schedule. // Programs with hour < 6 belong to the next calendar day's schedule.
try { try {
const currentDayItems = JSON.parse(content) || [] const currentDayItems = JSON.parse(content) || []
if (Array.isArray(currentDayItems)) { if (Array.isArray(currentDayItems)) {
currentDayItems.forEach(item => { currentDayItems.forEach(item => {
const time = item?.content?.broadcastBeginDate const time = item?.content?.broadcastBeginDate
if (!time) return if (!time) return
const hour = parseInt(time.split('h')[0]) const hour = parseInt(time.split('h')[0])
if (hour >= 6) { if (hour >= 6) {
items.push(item) items.push(item)
} }
}) })
} }
} catch { } catch {
return programs return programs
} }
items.forEach(item => { items.forEach(item => {
const { start, stop } = parseDuration(date, item) const { start, stop } = parseDuration(date, item)
if (!start.isValid() || !stop.isValid()) return if (!start.isValid() || !stop.isValid()) return
// Can contain Season and Episode in title, but not always. If title is missing, skip the program // Can contain Season and Episode in title, but not always. If title is missing, skip the program
if (!item?.content?.title) return if (!item?.content?.title) return
let title = item.content.title let title = item.content.title
let season = null let season = null
let episode = null let episode = null
const seMatch = title.match(/\s*-?\s*S(\d+)\s+E(\d+)\s*-?\s*/) const seMatch = title.match(/\s*-?\s*S(\d+)\s+E(\d+)\s*-?\s*/)
if (seMatch) { if (seMatch) {
season = parseInt(seMatch[1]) season = parseInt(seMatch[1])
episode = parseInt(seMatch[2]) episode = parseInt(seMatch[2])
title = title.replace(seMatch[0], ' ').replace(/^\s+/, '').replace(/\s+$/, '').trim() title = title.replace(seMatch[0], ' ').replace(/^\s+/, '').replace(/\s+$/, '').trim()
} }
const fullTitle = (item.content.titleLeading ? item.content.titleLeading + (title ? ' - ' : '') : '') + title const fullTitle = (item.content.titleLeading ? item.content.titleLeading + (title ? ' - ' : '') : '') + title
programs.push({ programs.push({
title: fullTitle, title: fullTitle,
description: item.content.description, description: item.content.description,
image: getImageUrl(item), image: getImageUrl(item),
icon: getImageUrl(item), icon: getImageUrl(item),
start, start,
stop, stop,
season: season, season: season,
episode: episode, episode: episode,
rating: item.content.csa rating: item.content.csa
}) })
}) })
return programs return programs
} }
} }
function parseDuration(date, item) { function parseDuration(date, item) {
const current_date = date.format('YYYY-MM-DD') const current_date = date.format('YYYY-MM-DD')
const time = item.content?.broadcastBeginDate const time = item.content?.broadcastBeginDate
const duration = item.content?.duration // e.g. "11 min 45 s", "1 h 30 min", "30 min" const duration = item.content?.duration // e.g. "11 min 45 s", "1 h 30 min", "30 min"
if (!time) return { start: dayjs(null), stop: dayjs(null) } if (!time) return { start: dayjs(null), stop: dayjs(null) }
const timeParts = time.split('h') const timeParts = time.split('h')
let durationInSeconds = 0 let durationInSeconds = 0
if (duration) { if (duration) {
const durationParts = duration.split(' ') const durationParts = duration.split(' ')
for (let i = 0; i < durationParts.length; i++) { for (let i = 0; i < durationParts.length; i++) {
const part = durationParts[i] const part = durationParts[i]
if (part === 'h' && i > 0) { if (part === 'h' && i > 0) {
durationInSeconds += parseInt(durationParts[i - 1]) * 3600 durationInSeconds += parseInt(durationParts[i - 1]) * 3600
} else if (part === 'min' && i > 0) { } else if (part === 'min' && i > 0) {
durationInSeconds += parseInt(durationParts[i - 1]) * 60 durationInSeconds += parseInt(durationParts[i - 1]) * 60
} else if (part === 's' && i > 0) { } else if (part === 's' && i > 0) {
durationInSeconds += parseInt(durationParts[i - 1]) durationInSeconds += parseInt(durationParts[i - 1])
} }
} }
} }
const start = dayjs.utc(`${current_date} ${timeParts[0]}:${timeParts[1]}`, 'YYYY-MM-DD HH:mm') const start = dayjs.utc(`${current_date} ${timeParts[0]}:${timeParts[1]}`, 'YYYY-MM-DD HH:mm')
const stop = start.add(durationInSeconds, 'second') const stop = start.add(durationInSeconds, 'second')
return { start, stop } return { start, stop }
} }
function getImageUrl(item) { function getImageUrl(item) {
const url = item.content?.thumbnail?.x1 const url = item.content?.thumbnail?.x1
return url return url
} }

View File

@@ -1,54 +1,54 @@
const { parser, url } = require('./france.tv.config.js') const { parser, url } = require('./france.tv.config.js')
const axios = require('axios') const axios = require('axios')
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)
jest.mock('axios') jest.mock('axios')
const date = dayjs.utc('2026-02-19', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2026-02-19', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: 'france-2', site_id: 'france-2',
xmltv_id: 'France2.fr@HD' xmltv_id: 'France2.fr@HD'
} }
it('can generate valid url', () => { it('can generate valid url', () => {
expect(url({ channel, date })).toBe('https://www.france.tv/api/epg/videos/?date=2026-02-19&channel=france-2') expect(url({ channel, date })).toBe('https://www.france.tv/api/epg/videos/?date=2026-02-19&channel=france-2')
}) })
it('can parse response', async () => { it('can parse response', async () => {
axios.get.mockResolvedValue({ data: [] }) axios.get.mockResolvedValue({ data: [] })
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const results = (await parser({ content, date, channel })).map(p => { const results = (await parser({ content, date, channel })).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(18) expect(results.length).toBe(18)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
title: 'Le 6h info - Émission du jeudi 19 février 2026', title: 'Le 6h info - Émission du jeudi 19 février 2026',
description: "Un rendez-vous réveil-matin, avec un point sur l'actualité assorti de différentes rubriques qui permettent d'en explorer certains aspects plus en profondeur.", description: "Un rendez-vous réveil-matin, avec un point sur l'actualité assorti de différentes rubriques qui permettent d'en explorer certains aspects plus en profondeur.",
image: 'https://medias.france.tv/S9p5NdAs4OR2UbyC1NIQWsYV-K4/240x0/filters:quality(85):format(webp)/b/f/3/e85c2e8fed4a4955965dfff63c3843fb.jpg', image: 'https://medias.france.tv/S9p5NdAs4OR2UbyC1NIQWsYV-K4/240x0/filters:quality(85):format(webp)/b/f/3/e85c2e8fed4a4955965dfff63c3843fb.jpg',
start: '2026-02-19T06:00:00.000Z', start: '2026-02-19T06:00:00.000Z',
stop: '2026-02-19T06:30:00.000Z' stop: '2026-02-19T06:30:00.000Z'
}) })
expect(results[17]).toMatchObject({ expect(results[17]).toMatchObject({
title: 'JO Club - Émission du jeudi 19 février 2026', title: 'JO Club - Émission du jeudi 19 février 2026',
description: "Tous les soirs, tout au long de ces Jeux olympiques d'hiver de Milan-Cortina, Laurent Luyat revient, avec les journalistes et consultants de France Télévisions, sur les épreuves de la journée. Il accueille les athlètes et les médaillés du jour. La journée a été marquée par du combiné nordique, avec l'épreuve par équipes messieurs, les demi-final...", description: "Tous les soirs, tout au long de ces Jeux olympiques d'hiver de Milan-Cortina, Laurent Luyat revient, avec les journalistes et consultants de France Télévisions, sur les épreuves de la journée. Il accueille les athlètes et les médaillés du jour. La journée a été marquée par du combiné nordique, avec l'épreuve par équipes messieurs, les demi-final...",
image: 'https://medias.france.tv/xuxaBPNFyhMiVB5eeYrZV_1nPj4/240x0/filters:quality(85):format(webp)/v/p/h/phpmhbhpv.jpg', image: 'https://medias.france.tv/xuxaBPNFyhMiVB5eeYrZV_1nPj4/240x0/filters:quality(85):format(webp)/v/p/h/phpmhbhpv.jpg',
start: '2026-02-19T23:00:00.000Z', start: '2026-02-19T23:00:00.000Z',
stop: '2026-02-20T00:00:00.000Z' stop: '2026-02-20T00:00:00.000Z'
}) })
}) })
it('can handle empty guide', async () => { it('can handle empty guide', async () => {
axios.get.mockResolvedValue({ data: [] }) axios.get.mockResolvedValue({ data: [] })
const results = await parser({ content: [], date, channel }) const results = await parser({ content: [], date, channel })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })

View File

@@ -1,67 +1,67 @@
const dayjs = require('dayjs') const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
const axios = require('axios') const axios = require('axios')
dayjs.extend(utc) dayjs.extend(utc)
module.exports = { module.exports = {
site: 'syn.is', site: 'syn.is',
days: 7, days: 7,
request: { request: {
cache: { cache: {
ttl: 60 * 60 * 1000 // 1 hour ttl: 60 * 60 * 1000 // 1 hour
} }
}, },
url({ channel, date }) { url({ channel, date }) {
return `https://www.syn.is/api/epg/${channel.site_id}/${date.format('YYYY-MM-DD')}` return `https://www.syn.is/api/epg/${channel.site_id}/${date.format('YYYY-MM-DD')}`
}, },
parser: function ({ content }) { parser: function ({ content }) {
let data let data
try { try {
data = JSON.parse(content) data = JSON.parse(content)
} catch (error) { } catch (error) {
console.error('Error parsing JSON:', error) console.error('Error parsing JSON:', error)
return [] return []
} }
const programs = [] const programs = []
if (data && Array.isArray(data)) { if (data && Array.isArray(data)) {
data.forEach(item => { data.forEach(item => {
if (!item) return if (!item) return
const start = dayjs.utc(item.upphaf) const start = dayjs.utc(item.upphaf)
const stop = start.add(item.slott, 'm') const stop = start.add(item.slott, 'm')
programs.push({ programs.push({
title: item.isltitill, title: item.isltitill,
sub_title: item.undirtitill, sub_title: item.undirtitill,
description: item.lysing, description: item.lysing,
actors: item.adalhlutverk, actors: item.adalhlutverk,
directors: item.leikstjori, directors: item.leikstjori,
start, start,
stop stop
}) })
}) })
} }
return programs return programs
}, },
async channels() { async channels() {
try { try {
const response = await axios.get('https://www.syn.is/api/epg/') const response = await axios.get('https://www.syn.is/api/epg/')
if (!response.data || !Array.isArray(response.data)) { if (!response.data || !Array.isArray(response.data)) {
console.error('Error: No channels data found') console.error('Error: No channels data found')
return [] return []
} }
return response.data.map(item => { return response.data.map(item => {
return { return {
lang: 'is', lang: 'is',
site_id: item site_id: item
} }
}) })
} catch (error) { } catch (error) {
console.error('Error fetching channels:', error) console.error('Error fetching channels:', error)
return [] return []
} }
} }
} }

View File

@@ -1,46 +1,46 @@
const { parser, url } = require('./stod2.is.config.js') const { parser, url } = require('./stod2.is.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')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(customParseFormat) dayjs.extend(customParseFormat)
dayjs.extend(timezone) dayjs.extend(timezone)
const date = dayjs.utc('2025-01-03', 'YYYY-MM-DD').startOf('day') const date = dayjs.utc('2025-01-03', 'YYYY-MM-DD').startOf('day')
const channel = { site_id: 'stod2', xmltv_id: 'Stod2.is' } const channel = { site_id: 'stod2', xmltv_id: 'Stod2.is' }
it('can generate valid url', () => { it('can generate valid url', () => {
const generatedUrl = url({ date, channel }) const generatedUrl = url({ date, channel })
expect(generatedUrl).toBe('https://www.syn.is/api/epg/stod2/2025-01-03') expect(generatedUrl).toBe('https://www.syn.is/api/epg/stod2/2025-01-03')
}) })
it('can parse response', () => { it('can parse response', () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json'))
const result = parser({ content }).map(p => { const result = parser({ content }).map(p => {
p.start = p.start.toISOString() p.start = p.start.toISOString()
p.stop = p.stop.toISOString() p.stop = p.stop.toISOString()
return p return p
}) })
expect(result).toMatchObject([ expect(result).toMatchObject([
{ {
title: 'Heimsókn', title: 'Heimsókn',
sub_title: 'Telma Borgþórsdóttir', sub_title: 'Telma Borgþórsdóttir',
description: description:
'Frábærir þættir með Sindra Sindrasyni sem lítur inn hjá íslenskum fagurkerum. Heimilin eru jafn ólík og þau eru mörg en eiga það þó eitt sameiginlegt að vera sett saman af alúð og smekklegheitum. Sindri hefur líka einstakt lag á að ná fram því besta í viðmælendum sínum.', 'Frábærir þættir með Sindra Sindrasyni sem lítur inn hjá íslenskum fagurkerum. Heimilin eru jafn ólík og þau eru mörg en eiga það þó eitt sameiginlegt að vera sett saman af alúð og smekklegheitum. Sindri hefur líka einstakt lag á að ná fram því besta í viðmælendum sínum.',
actors: '', actors: '',
directors: '', directors: '',
start: '2025-01-03T08:00:00.000Z', start: '2025-01-03T08:00:00.000Z',
stop: '2025-01-03T08:15:00.000Z' stop: '2025-01-03T08:15:00.000Z'
} }
]) ])
}) })
it('can handle empty guide', () => { it('can handle empty guide', () => {
const result = parser({ content: '[]' }) const result = parser({ content: '[]' })
expect(result).toMatchObject([]) expect(result).toMatchObject([])
}) })