diff --git a/sites/france.tv/france.tv.config.js b/sites/france.tv/france.tv.config.js index 88577263..aa13b733 100644 --- a/sites/france.tv/france.tv.config.js +++ b/sites/france.tv/france.tv.config.js @@ -1,135 +1,135 @@ -const dayjs = require('dayjs') -const axios = require('axios') -const utc = require('dayjs/plugin/utc') -const timezone = require('dayjs/plugin/timezone') -const customParseFormat = require('dayjs/plugin/customParseFormat') - -dayjs.extend(utc) -dayjs.extend(timezone) -dayjs.extend(customParseFormat) -dayjs.tz.setDefault('Europe/Paris') - -// 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. -module.exports = { - site: 'france.tv', - days: 2, - url: function ({ channel, date }) { - return `https://www.france.tv/api/epg/videos/?date=${date.format('YYYY-MM-DD')}&channel=${channel.site_id}` - }, - parser: async function ({ channel, content, date }) { - const programs = [] - let items = [] - - 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}` - - try { - const responseDayBefore = await axios.get(linkDayBefore) - const programmingDayBefore = responseDayBefore.data || [] - - // 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. - if (Array.isArray(programmingDayBefore)) { - programmingDayBefore.forEach(item => { - const time = item?.content?.broadcastBeginDate - if (!time) return - const hour = parseInt(time.split('h')[0]) - - if (hour < 6) { - items.push(item) - } - }) - } - } catch { - // Day before data unavailable, continue with current day only - } - - // 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. - try { - const currentDayItems = JSON.parse(content) || [] - if (Array.isArray(currentDayItems)) { - currentDayItems.forEach(item => { - const time = item?.content?.broadcastBeginDate - if (!time) return - const hour = parseInt(time.split('h')[0]) - - if (hour >= 6) { - items.push(item) - } - }) - } - } catch { - return programs - } - - items.forEach(item => { - const { start, stop } = parseDuration(date, item) - if (!start.isValid() || !stop.isValid()) return - // Can contain Season and Episode in title, but not always. If title is missing, skip the program - if (!item?.content?.title) return - - let title = item.content.title - let season = null - let episode = null - - const seMatch = title.match(/\s*-?\s*S(\d+)\s+E(\d+)\s*-?\s*/) - if (seMatch) { - season = parseInt(seMatch[1]) - episode = parseInt(seMatch[2]) - title = title.replace(seMatch[0], ' ').replace(/^\s+/, '').replace(/\s+$/, '').trim() - } - - const fullTitle = (item.content.titleLeading ? item.content.titleLeading + (title ? ' - ' : '') : '') + title - - programs.push({ - title: fullTitle, - description: item.content.description, - image: getImageUrl(item), - icon: getImageUrl(item), - start, - stop, - season: season, - episode: episode, - rating: item.content.csa - }) - }) - - return programs - } -} - -function parseDuration(date, item) { - const current_date = date.format('YYYY-MM-DD') - const time = item.content?.broadcastBeginDate - 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) } - - const timeParts = time.split('h') - - let durationInSeconds = 0 - if (duration) { - const durationParts = duration.split(' ') - for (let i = 0; i < durationParts.length; i++) { - const part = durationParts[i] - if (part === 'h' && i > 0) { - durationInSeconds += parseInt(durationParts[i - 1]) * 3600 - } else if (part === 'min' && i > 0) { - durationInSeconds += parseInt(durationParts[i - 1]) * 60 - } else if (part === 's' && i > 0) { - durationInSeconds += parseInt(durationParts[i - 1]) - } - } - } - - const start = dayjs.utc(`${current_date} ${timeParts[0]}:${timeParts[1]}`, 'YYYY-MM-DD HH:mm') - const stop = start.add(durationInSeconds, 'second') - return { start, stop } -} - -function getImageUrl(item) { - const url = item.content?.thumbnail?.x1 - return url -} +const dayjs = require('dayjs') +const axios = require('axios') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') +const customParseFormat = require('dayjs/plugin/customParseFormat') + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(customParseFormat) +dayjs.tz.setDefault('Europe/Paris') + +// 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. +module.exports = { + site: 'france.tv', + days: 2, + url: function ({ channel, date }) { + return `https://www.france.tv/api/epg/videos/?date=${date.format('YYYY-MM-DD')}&channel=${channel.site_id}` + }, + parser: async function ({ channel, content, date }) { + const programs = [] + let items = [] + + 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}` + + try { + const responseDayBefore = await axios.get(linkDayBefore) + const programmingDayBefore = responseDayBefore.data || [] + + // 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. + if (Array.isArray(programmingDayBefore)) { + programmingDayBefore.forEach(item => { + const time = item?.content?.broadcastBeginDate + if (!time) return + const hour = parseInt(time.split('h')[0]) + + if (hour < 6) { + items.push(item) + } + }) + } + } catch { + // Day before data unavailable, continue with current day only + } + + // 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. + try { + const currentDayItems = JSON.parse(content) || [] + if (Array.isArray(currentDayItems)) { + currentDayItems.forEach(item => { + const time = item?.content?.broadcastBeginDate + if (!time) return + const hour = parseInt(time.split('h')[0]) + + if (hour >= 6) { + items.push(item) + } + }) + } + } catch { + return programs + } + + items.forEach(item => { + const { start, stop } = parseDuration(date, item) + if (!start.isValid() || !stop.isValid()) return + // Can contain Season and Episode in title, but not always. If title is missing, skip the program + if (!item?.content?.title) return + + let title = item.content.title + let season = null + let episode = null + + const seMatch = title.match(/\s*-?\s*S(\d+)\s+E(\d+)\s*-?\s*/) + if (seMatch) { + season = parseInt(seMatch[1]) + episode = parseInt(seMatch[2]) + title = title.replace(seMatch[0], ' ').replace(/^\s+/, '').replace(/\s+$/, '').trim() + } + + const fullTitle = (item.content.titleLeading ? item.content.titleLeading + (title ? ' - ' : '') : '') + title + + programs.push({ + title: fullTitle, + description: item.content.description, + image: getImageUrl(item), + icon: getImageUrl(item), + start, + stop, + season: season, + episode: episode, + rating: item.content.csa + }) + }) + + return programs + } +} + +function parseDuration(date, item) { + const current_date = date.format('YYYY-MM-DD') + const time = item.content?.broadcastBeginDate + 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) } + + const timeParts = time.split('h') + + let durationInSeconds = 0 + if (duration) { + const durationParts = duration.split(' ') + for (let i = 0; i < durationParts.length; i++) { + const part = durationParts[i] + if (part === 'h' && i > 0) { + durationInSeconds += parseInt(durationParts[i - 1]) * 3600 + } else if (part === 'min' && i > 0) { + durationInSeconds += parseInt(durationParts[i - 1]) * 60 + } else if (part === 's' && i > 0) { + durationInSeconds += parseInt(durationParts[i - 1]) + } + } + } + + const start = dayjs.utc(`${current_date} ${timeParts[0]}:${timeParts[1]}`, 'YYYY-MM-DD HH:mm') + const stop = start.add(durationInSeconds, 'second') + return { start, stop } +} + +function getImageUrl(item) { + const url = item.content?.thumbnail?.x1 + return url +} diff --git a/sites/france.tv/france.tv.test.js b/sites/france.tv/france.tv.test.js index b7be8c07..8edb7946 100644 --- a/sites/france.tv/france.tv.test.js +++ b/sites/france.tv/france.tv.test.js @@ -1,54 +1,54 @@ -const { parser, url } = require('./france.tv.config.js') -const axios = require('axios') -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) - -jest.mock('axios') - -const date = dayjs.utc('2026-02-19', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: 'france-2', - xmltv_id: 'France2.fr@HD' -} - -it('can generate valid url', () => { - expect(url({ channel, date })).toBe('https://www.france.tv/api/epg/videos/?date=2026-02-19&channel=france-2') -}) - -it('can parse response', async () => { - axios.get.mockResolvedValue({ data: [] }) - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const results = (await parser({ content, date, channel })).map(p => { - p.start = p.start.toJSON() - p.stop = p.stop.toJSON() - return p - }) - - expect(results.length).toBe(18) - expect(results[0]).toMatchObject({ - 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.", - 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', - stop: '2026-02-19T06:30:00.000Z' - }) - expect(results[17]).toMatchObject({ - 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...", - 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', - stop: '2026-02-20T00:00:00.000Z' - }) -}) - -it('can handle empty guide', async () => { - axios.get.mockResolvedValue({ data: [] }) - const results = await parser({ content: [], date, channel }) - - expect(results).toMatchObject([]) -}) +const { parser, url } = require('./france.tv.config.js') +const axios = require('axios') +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) + +jest.mock('axios') + +const date = dayjs.utc('2026-02-19', 'YYYY-MM-DD').startOf('d') +const channel = { + site_id: 'france-2', + xmltv_id: 'France2.fr@HD' +} + +it('can generate valid url', () => { + expect(url({ channel, date })).toBe('https://www.france.tv/api/epg/videos/?date=2026-02-19&channel=france-2') +}) + +it('can parse response', async () => { + axios.get.mockResolvedValue({ data: [] }) + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const results = (await parser({ content, date, channel })).map(p => { + p.start = p.start.toJSON() + p.stop = p.stop.toJSON() + return p + }) + + expect(results.length).toBe(18) + expect(results[0]).toMatchObject({ + 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.", + 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', + stop: '2026-02-19T06:30:00.000Z' + }) + expect(results[17]).toMatchObject({ + 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...", + 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', + stop: '2026-02-20T00:00:00.000Z' + }) +}) + +it('can handle empty guide', async () => { + axios.get.mockResolvedValue({ data: [] }) + const results = await parser({ content: [], date, channel }) + + expect(results).toMatchObject([]) +}) diff --git a/sites/syn.is/syn.is.config.js b/sites/syn.is/syn.is.config.js index 7a575b49..c3e84236 100644 --- a/sites/syn.is/syn.is.config.js +++ b/sites/syn.is/syn.is.config.js @@ -1,67 +1,67 @@ -const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -const axios = require('axios') - -dayjs.extend(utc) - -module.exports = { - site: 'syn.is', - days: 7, - request: { - cache: { - ttl: 60 * 60 * 1000 // 1 hour - } - }, - url({ channel, date }) { - return `https://www.syn.is/api/epg/${channel.site_id}/${date.format('YYYY-MM-DD')}` - }, - parser: function ({ content }) { - let data - try { - data = JSON.parse(content) - } catch (error) { - console.error('Error parsing JSON:', error) - return [] - } - - const programs = [] - - if (data && Array.isArray(data)) { - data.forEach(item => { - if (!item) return - const start = dayjs.utc(item.upphaf) - const stop = start.add(item.slott, 'm') - - programs.push({ - title: item.isltitill, - sub_title: item.undirtitill, - description: item.lysing, - actors: item.adalhlutverk, - directors: item.leikstjori, - start, - stop - }) - }) - } - - return programs - }, - async channels() { - try { - const response = await axios.get('https://www.syn.is/api/epg/') - if (!response.data || !Array.isArray(response.data)) { - console.error('Error: No channels data found') - return [] - } - return response.data.map(item => { - return { - lang: 'is', - site_id: item - } - }) - } catch (error) { - console.error('Error fetching channels:', error) - return [] - } - } -} +const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const axios = require('axios') + +dayjs.extend(utc) + +module.exports = { + site: 'syn.is', + days: 7, + request: { + cache: { + ttl: 60 * 60 * 1000 // 1 hour + } + }, + url({ channel, date }) { + return `https://www.syn.is/api/epg/${channel.site_id}/${date.format('YYYY-MM-DD')}` + }, + parser: function ({ content }) { + let data + try { + data = JSON.parse(content) + } catch (error) { + console.error('Error parsing JSON:', error) + return [] + } + + const programs = [] + + if (data && Array.isArray(data)) { + data.forEach(item => { + if (!item) return + const start = dayjs.utc(item.upphaf) + const stop = start.add(item.slott, 'm') + + programs.push({ + title: item.isltitill, + sub_title: item.undirtitill, + description: item.lysing, + actors: item.adalhlutverk, + directors: item.leikstjori, + start, + stop + }) + }) + } + + return programs + }, + async channels() { + try { + const response = await axios.get('https://www.syn.is/api/epg/') + if (!response.data || !Array.isArray(response.data)) { + console.error('Error: No channels data found') + return [] + } + return response.data.map(item => { + return { + lang: 'is', + site_id: item + } + }) + } catch (error) { + console.error('Error fetching channels:', error) + return [] + } + } +} diff --git a/sites/syn.is/syn.is.test.js b/sites/syn.is/syn.is.test.js index 068d58aa..b16bb5eb 100644 --- a/sites/syn.is/syn.is.test.js +++ b/sites/syn.is/syn.is.test.js @@ -1,46 +1,46 @@ -const { parser, url } = require('./stod2.is.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') - -dayjs.extend(utc) -dayjs.extend(customParseFormat) -dayjs.extend(timezone) - -const date = dayjs.utc('2025-01-03', 'YYYY-MM-DD').startOf('day') -const channel = { site_id: 'stod2', xmltv_id: 'Stod2.is' } - -it('can generate valid url', () => { - const generatedUrl = url({ date, channel }) - expect(generatedUrl).toBe('https://www.syn.is/api/epg/stod2/2025-01-03') -}) - -it('can parse response', () => { - const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const result = parser({ content }).map(p => { - p.start = p.start.toISOString() - p.stop = p.stop.toISOString() - return p - }) - - expect(result).toMatchObject([ - { - title: 'Heimsókn', - sub_title: 'Telma Borgþórsdóttir', - 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.', - actors: '', - directors: '', - start: '2025-01-03T08:00:00.000Z', - stop: '2025-01-03T08:15:00.000Z' - } - ]) -}) - -it('can handle empty guide', () => { - const result = parser({ content: '[]' }) - expect(result).toMatchObject([]) -}) +const { parser, url } = require('./stod2.is.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') + +dayjs.extend(utc) +dayjs.extend(customParseFormat) +dayjs.extend(timezone) + +const date = dayjs.utc('2025-01-03', 'YYYY-MM-DD').startOf('day') +const channel = { site_id: 'stod2', xmltv_id: 'Stod2.is' } + +it('can generate valid url', () => { + const generatedUrl = url({ date, channel }) + expect(generatedUrl).toBe('https://www.syn.is/api/epg/stod2/2025-01-03') +}) + +it('can parse response', () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) + const result = parser({ content }).map(p => { + p.start = p.start.toISOString() + p.stop = p.stop.toISOString() + return p + }) + + expect(result).toMatchObject([ + { + title: 'Heimsókn', + sub_title: 'Telma Borgþórsdóttir', + 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.', + actors: '', + directors: '', + start: '2025-01-03T08:00:00.000Z', + stop: '2025-01-03T08:15:00.000Z' + } + ]) +}) + +it('can handle empty guide', () => { + const result = parser({ content: '[]' }) + expect(result).toMatchObject([]) +})