Update orangetv.orange.es guide.

Test:

```sh
npm test --- orangetv.orange.es

> test
> run-script-os orangetv.orange.es

> test:win32
> SET "TZ=Pacific/Nauru" && npx jest --runInBand orangetv.orange.es

 PASS  sites/orangetv.orange.es/orangetv.orange.es.test.js
  √ can generate valid url (6 ms)
  √ can parse response (6 ms)
  √ can handle empty guide (1 ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        0.701 s, estimated 1 s
Ran all test suites matching /orangetv.orange.es/i.
```

Grab:

```sh
pm run grab --- --site=orangetv.orange.es

> grab
> npx tsx scripts/commands/epg/grab.ts --site=orangetv.orange.es

starting...
config:
  output: guide.xml
  maxConnections: 1
  gzip: false
  site: orangetv.orange.es
loading channels...
  found 2 channel(s)
run #1:
  [1/2] orangetv.orange.es (es) - La1.es - Jan 12, 2025 (18 programs)
  [2/2] orangetv.orange.es (es) - La2.es - Jan 12, 2025 (39 programs)
  saving to "guide.xml"...
  done in 00h 00m 18s
```

Signed-off-by: Toha <tohenk@yahoo.com>
This commit is contained in:
Toha 2025-01-12 14:14:28 +07:00
parent 3c19dd1725
commit 3dbece7ff0
No known key found for this signature in database
GPG key ID: 2D7AA6389D44DCAB
7 changed files with 103 additions and 77 deletions

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
[{"responseElementType": "ProgramList", "channelExternalId": "1010", "programs": [{"startDate": 1736636100000, "endDate": 1736642400000, "attachments": [{"name": "COVER", "value": "/epg/COVER/COVER_2247567.jpg"}], "seriesSeason": "", "episodeId": "", "referenceProgramId": "780906206", "prLevel": 7, "id": 15071377885, "description": "Cassie tenía un brillante futuro por delante. Sin embargo, un incidente provocó que no pudiese cumplir sus sueños. Con el paso del tiempo, tendrá la oportunidad de enmendar los errores del pasado.", "seriesName": "", "genres": [{"externalId": "Cine", "name": "Cine", "id": 128839623}, {"externalId": "Drama", "name": "Drama", "id": 862336010}, {"externalId": "Suspense", "name": "Suspense", "id": 862337524}], "name": "Una joven prometedora", "flags": 14}, {"startDate": 1736642400000, "endDate": 1736648100000, "attachments": [{"name": "COVER", "value": "/epg/COVER/COVER_3522912.jpg"}], "seriesSeason": "", "episodeId": "", "referenceProgramId": "780916568", "prLevel": 1, "id": 15073959700, "description": "César deambula por las calles de Montmarte en París, donde interpreta su música. Un día, se encuentra con Salomé, a quien dejó sin decir palabra un tiempo atrás, y descubre que es padre de una niña.", "seriesName": "", "genres": [{"externalId": "Cine", "name": "Cine", "id": 128839623}, {"externalId": "Comedia", "name": "Comedia", "id": 862331198}], "name": "Una comedia romántica", "flags": 14}, {"startDate": 1736648100000, "endDate": 1736658000000, "attachments": [{"name": "COVER", "value": "/epg/COVER/COVER_3108557.jpg"}], "seriesSeason": "", "episodeId": "", "referenceProgramId": "780871990", "prLevel": 1, "id": 15058341721, "description": "Noticias de los servicios informativos del Canal 24 Horas", "seriesName": "", "genres": [{"externalId": "Programa", "name": "Programa", "id": 9169074970}, {"externalId": "Informativo", "name": "Informativo", "id": 862353792}], "name": "Noticias 24H", "flags": 14}, {"startDate": 1736658000000, "endDate": 1736674500000, "attachments": [{"name": "COVER", "value": "/epg/COVER/COVER_3108557.jpg"}], "seriesSeason": "", "episodeId": "", "referenceProgramId": "780871999", "prLevel": 1, "id": 15058342585, "description": "Noticias de los servicios informativos del Canal 24 Horas", "seriesName": "", "genres": [{"externalId": "Programa", "name": "Programa", "id": 9169074970}, {"externalId": "Informativo", "name": "Informativo", "id": 862353792}], "name": "Noticias 24H", "flags": 14}]}]

View file

@ -0,0 +1 @@
[{"responseElementType": "ProgramList", "channelExternalId": "1010", "programs": [{"startDate": 1736658000000, "endDate": 1736674500000, "attachments": [{"name": "COVER", "value": "/epg/COVER/COVER_3108557.jpg"}], "seriesSeason": "", "episodeId": "", "referenceProgramId": "780871999", "prLevel": 1, "id": 15058342585, "description": "Noticias de los servicios informativos del Canal 24 Horas", "seriesName": "", "genres": [{"externalId": "Programa", "name": "Programa", "id": 9169074970}, {"externalId": "Informativo", "name": "Informativo", "id": 862353792}], "name": "Noticias 24H", "flags": 14}, {"startDate": 1736674500000, "endDate": 1736676600000, "attachments": [{"name": "COVER", "value": "/epg/COVER/COVER_469238.jpg"}], "seriesSeason": "8", "episodeId": "12", "referenceProgramId": "780872016", "prLevel": 1, "id": 15058342081, "description": "Magacín de salud semanal con la periodista Miriam Moreno con vocación de servicio público. En cada edición ofrece a los espectadores los mejores consejos y cambios en la rutina que pueden servir para llevar una vida más saludable.", "seriesName": "Saber vivir", "genres": [{"externalId": "Programa", "name": "Programa", "id": 9169074970}, {"externalId": "Salud", "name": "Salud", "id": 862341634}], "name": "Saber vivir - T8, E12: Saber vivir", "flags": 15}, {"startDate": 1736676600000, "endDate": 1736679900000, "attachments": [{"name": "COVER", "value": "/epg/COVER/COVER_3261229.jpg"}], "seriesSeason": "17", "episodeId": "8", "referenceProgramId": "780916571", "prLevel": 4, "id": 15073959340, "description": "El programa viaja hasta Bulgaria para conocer su capital Sofía, una de las ciudades más antiguas de Europa y también de las más desconocidas. El espacio de viajes de La 1 también llegará hasta la ciudad medieval de Veliko Tarnovo y Varna.", "seriesName": "Españoles en el mundo", "genres": [{"externalId": "Documental", "name": "Documental", "id": 30089199}, {"externalId": "Viajes y aventuras", "name": "Viajes y aventuras", "id": 9198977905}], "name": "Españoles en el mundo - T17, E08: Sofia, Bulgaria", "flags": 15}, {"startDate": 1736679900000, "endDate": 1736683200000, "attachments": [{"name": "COVER", "value": "/epg/COVER/COVER_3261229.jpg"}], "seriesSeason": "16", "episodeId": "9", "referenceProgramId": "780872010", "prLevel": 4, "id": 15058342225, "description": "El programa cruza el charco para trasladarse a Washington D.C., donde de la mano de seis personas españolas, se descubren lugares como la Casa Blanca, el Capitolio, el Museo de Historia Natural o el Memorial de los Veteranos de Vietnam.", "seriesName": "Españoles en el mundo", "genres": [{"externalId": "Documental", "name": "Documental", "id": 30089199}, {"externalId": "Viajes y aventuras", "name": "Viajes y aventuras", "id": 9198977905}], "name": "Españoles en el mundo - T16, E09: Washington, Distrito Columbia", "flags": 15}, {"startDate": 1736683200000, "endDate": 1736686500000, "attachments": [{"name": "COVER", "value": "/epg/COVER/COVER_3256949.jpg"}], "seriesSeason": "12", "episodeId": "4", "referenceProgramId": "780872014", "prLevel": 4, "id": 15058342441, "description": "Cruzamos el Estrecho de Gibraltar para conocer la huella española en los dominios del antiguo Protectorado en Marruecos.", "seriesName": "Españoles en el mundo", "genres": [{"externalId": "Documental", "name": "Documental", "id": 30089199}, {"externalId": "Viajes y aventuras", "name": "Viajes y aventuras", "id": 9198977905}], "name": "Españoles en el mundo - T12, E04: Tánger, Tetuán y Chauen", "flags": 15}, {"startDate": 1736686500000, "endDate": 1736690400000, "attachments": [{"name": "COVER", "value": "/epg/COVER/COVER_415745.jpg"}], "seriesSeason": "2", "episodeId": "4", "referenceProgramId": "780872015", "prLevel": 1, "id": 15058341793, "description": "Anne Igartiburu y Jordi González hacen un repaso a la actualidad de la crónica social, con los nombres propios más importantes de la semana y estando en directo en todos los acontecimientos y eventos que se produzcan.", "seriesName": "D Corazón", "genres": [{"externalId": "Programa", "name": "Programa", "id": 9169074970}, {"externalId": "Prensa rosa", "name": "Prensa rosa", "id": 11611838587}], "name": "D Corazón - T2, E04: D Corazón", "flags": 15}, {"startDate": 1736690400000, "endDate": 1736694000000, "attachments": [{"name": "COVER", "value": "/epg/COVER/COVER_663318.jpg"}], "seriesSeason": "", "episodeId": "", "referenceProgramId": "780872012", "prLevel": 1, "id": 15058342729, "description": "Informativo de Televisión Española de los fines de semana. Lara Siscar e Igor Gómez se encargan de presentar las últimas noticias de ámbito nacional e internacional surgidas durante el día", "seriesName": "", "genres": [{"externalId": "Programa", "name": "Programa", "id": 9169074970}, {"externalId": "Informativo", "name": "Informativo", "id": 862353792}], "name": "Telediario 1 Fin de Semana", "flags": 14}]}]

File diff suppressed because one or more lines are too long

View file

@ -1,9 +1,12 @@
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const axios = require('axios')
const doFetch = require('@ntlab/sfetch')
const debug = require('debug')('site:orangetv.orange.es')
dayjs.extend(utc)
doFetch.setDebugger(debug)
const API_PROGRAM_ENDPOINT = 'https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO'
const API_CHANNEL_ENDPOINT =
'https://pc.orangetv.orange.es/pc/api/rtv/v1/GetChannelList?bouquet_id=1&model_external_id=PC&filter_unsupported_channels=false&client=json'
@ -14,49 +17,39 @@ module.exports = {
days: 2,
request: {
cache: {
ttl: 60 * 60 * 1000 // 1 hour
ttl: 24 * 60 * 60 * 1000 // 1 day
}
},
url({ date }) {
return `${API_PROGRAM_ENDPOINT}/${date.format('YYYYMMDD')}_8h_1.json`
url({ date, segment = 1 }) {
return `${API_PROGRAM_ENDPOINT}/${date.format('YYYYMMDD')}_8h_${segment}.json`
},
async parser({ content, channel, date }) {
let programs = []
let items = parseItems(content, channel)
if (!items.length) return programs
const promises = [
axios.get(`${API_PROGRAM_ENDPOINT}/${date.format('YYYYMMDD')}_8h_1.json`),
axios.get(`${API_PROGRAM_ENDPOINT}/${date.format('YYYYMMDD')}_8h_2.json`),
axios.get(`${API_PROGRAM_ENDPOINT}/${date.format('YYYYMMDD')}_8h_3.json`)
]
await Promise.allSettled(promises)
.then(results => {
results.forEach(r => {
if (r.status === 'fulfilled') {
const parsed = parseItems(r.value.data, channel)
items = items
.filter((item, index) => items.findIndex(oi => oi.id === item.id) === index)
.concat(parsed)
const programs = []
const items = parseItems(content, channel)
if (items.length) {
const queues = [
module.exports.url({ date, segment: 2 }),
module.exports.url({ date, segment: 3 })
]
await doFetch(queues, (url, res) => {
items.push(...parseItems(res, channel))
})
programs.push(
...items.map(item => {
return {
title: item.name,
sub_title: item.seriesName,
description: item.description,
category: parseGenres(item),
season: item.seriesSeason ? parseInt(item.seriesSeason) : null,
episode: item.episodeId ? parseInt(item.episodeId) : null,
icon: parseIcon(item),
start: dayjs.utc(item.startDate),
stop: dayjs.utc(item.endDate)
}
})
})
.catch(console.error)
items.forEach(item => {
programs.push({
title: item.name,
description: item.description,
category: parseGenres(item),
season: item.seriesSeason || null,
episode: item.episodeId || null,
icon: parseIcon(item),
start: dayjs.utc(item.startDate) || null,
stop: dayjs.utc(item.endDate) || null
})
})
)
}
return programs
},
@ -65,7 +58,8 @@ module.exports = {
const data = await axios
.get(API_CHANNEL_ENDPOINT)
.then(r => r.data)
.catch(console.log)
.catch(console.error)
return data.response.map(item => {
return {
lang: 'es',
@ -77,15 +71,12 @@ module.exports = {
}
function parseIcon(item) {
if (item.attachments.length > 0) {
const cover = item.attachments.find(i => i.name === 'COVER' || i.name === 'cover')
if (item.attachments.length) {
const cover = item.attachments.find(i => i.name.match(/cover/i))
if (cover) {
return `${API_IMAGE_ENDPOINT}${cover.value}`
}
}
return ''
}
function parseGenres(item) {
@ -93,16 +84,16 @@ function parseGenres(item) {
}
function parseItems(content, channel) {
const result = []
const json =
typeof content === 'string' ? JSON.parse(content) : Array.isArray(content) ? content : []
if (!Array.isArray(json)) {
return []
if (Array.isArray(json)) {
json
.filter(i => i.channelExternalId === channel.site_id)
.forEach(i => {
result.push(...i.programs)
})
}
const channelData = json.find(i => i.channelExternalId == channel.site_id)
if (!channelData) return []
return channelData.programs
return result
}

View file

@ -1,51 +1,82 @@
const { parser, url } = require('./orangetv.orange.es.config.js')
const path = require('path')
const fs = require('fs')
const axios = require('axios')
const dayjs = require('dayjs')
const utc = require('dayjs/plugin/utc')
const customParseFormat = require('dayjs/plugin/customParseFormat')
dayjs.extend(customParseFormat)
dayjs.extend(utc)
const path = require('path')
const fs = require('fs')
const date = dayjs.utc('2024-12-01', 'YYYY-MM-DD').startOf('d')
jest.mock('axios')
const date = dayjs.utc('2025-01-12', 'YYYY-MM-DD').startOf('d')
const channel = {
site_id: '1010',
xmltv_id: 'La1.es'
}
axios.get.mockImplementation(url => {
const result = {}
const urls = {
'https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO/20250112_8h_1.json':
'data1.json',
'https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO/20250112_8h_2.json':
'data2.json',
'https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO/20250112_8h_3.json':
'data3.json',
}
if (urls[url] !== undefined) {
result.data = fs.readFileSync(path.join(__dirname, '__data__', urls[url])).toString()
if (!urls[url].startsWith('data1')) {
result.data = JSON.parse(result.data)
}
}
return Promise.resolve(result)
})
it('can generate valid url', () => {
expect(url({ date })).toBe(
`https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO/${date.format(
'YYYYMMDD'
)}_8h_1.json`
'https://epg.orangetv.orange.es/epg/Smartphone_Android/1_PRO/20250112_8h_1.json'
)
})
it('can parse response', async () => {
const content = fs.readFileSync(path.resolve(__dirname, '__data__/data.json')).toString()
let results = await parser({ content, channel, date })
results = results.map(p => {
const content = fs.readFileSync(path.join(__dirname, '__data__', 'data1.json')).toString()
const results = (await parser({ content, channel, date })).map(p => {
p.start = p.start.toJSON()
p.stop = p.stop.toJSON()
return p
})
expect(results.length).toBe(4)
var sampleResult = results[0]
expect(sampleResult).toMatchObject({
start: '2024-11-30T22:36:51.000Z',
stop: '2024-11-30T23:57:25.000Z',
category: ['Cine', 'Romance', 'Comedia', 'Comedia Romántica'],
expect(results.length).toBe(18)
expect(results[0]).toMatchObject({
start: '2025-01-11T22:55:00.000Z',
stop: '2025-01-12T00:40:00.000Z',
title: 'Una joven prometedora',
description:
'Charlie trabaja como director en una escuela de primaria y goza de una placentera existencia junto a sus amigos. A pesar de ello, no es feliz porque cada vez que se enamora pierde la cordura.',
title: 'Loco de amor'
'Cassie tenía un brillante futuro por delante. Sin embargo, un incidente provocó que no pudiese cumplir sus sueños. Con el paso del tiempo, tendrá la oportunidad de enmendar los errores del pasado.',
category: ['Cine', 'Drama', 'Suspense'],
icon: 'https://pc.orangetv.orange.es/pc/api/rtv/v1/images/epg/COVER/COVER_2247567.jpg'
})
expect(results[17]).toMatchObject({
start: '2025-01-12T21:05:00.000Z',
stop: '2025-01-12T23:05:00.000Z',
title: 'Bake Off: Famosos al horno - T2, E01: Bake Off: Famosos al horno',
sub_title: 'Bake Off: Famosos al horno',
description:
'Nervios y emoción en el debut de los 14 pasteleros de la nueva temporada de Bake off Famosos al horno. En el primer programa hornearán unas galletas dedicadas a sus mascotas y una tradicional tarta de queso.',
category: ['Programa', 'Reality'],
icon: 'https://pc.orangetv.orange.es/pc/api/rtv/v1/images/epg/COVER/COVER_3520028.jpg',
season: 2,
episode: 1
})
})
it('can handle empty guide', () => {
const result = parser({
it('can handle empty guide', async () => {
const result = await parser({
date,
channel,
content: '{}'

View file

@ -5,18 +5,20 @@ https://orangetv.orange.es/epg
### Download the guide
```sh
npm run grab -- --site=orangetv.orange.es
npm run grab --- --site=orangetv.orange.es
```
### Update channel list
```sh
npm run channels:parse -- --config=./sites/orangetv.orange.es/orangetv.orange.es.config.js --output=./sites/orangetv.orange.es/orangetv.orange.es.channels.xml
npm run channels:parse --- --config=./sites/orangetv.orange.es/orangetv.orange.es.config.js --output=./sites/orangetv.orange.es/orangetv.orange.es.channels.xml
```
### Test
npm test -- orangetv.orange.es
```sh
npm test --- orangetv.orange.es
```
### Todo