fix tvguide.com

This commit is contained in:
whitesnakeftw committed 2025-07-30 03:23:06 +02:00
1 parent b1592c7f7b
commit f3124d42fc
4 files changed
+8281 -94

No files matched your search

File diff suppressed because it is too large. Load diff
+78
View File
@@ -0,0 +1,78 @@
{
"data": {
"item": {
"id": 9000058285,
"name": "Secrets of the Zoo: North Carolina",
"isSportsEvent": false,
"tvRating": "TV-14",
"categoryId": 5,
"subCategoryId": 0,
"episodeNumber": 1,
"mcoId": 1000990897,
"title": "Secrets of the Zoo: North Carolina",
"type": "show",
"slug": "secrets-of-the-zoo-north-carolina",
"typeId": 1,
"images": [
{
"id": "2-e5b17322185dd95f066a2de47efd8a12",
"provider": "2",
"imageType": {
"typeId": 1,
"typeName": "showcard",
"providerTypeName": "showcard"
},
"bucketType": "catalog",
"bucketPath": "/provider/2/13/2-e5b17322185dd95f066a2de47efd8a12.jpg",
"filename": "",
"width": 3840,
"height": 2160
},
{
"id": "2-59b2fd193bc035d4f2d4d3f2a68629d3",
"provider": "2",
"imageType": {
"typeId": 2,
"typeName": "poster art",
"providerTypeName": "poster art"
},
"bucketType": "catalog",
"bucketPath": "/provider/2/2/2-59b2fd193bc035d4f2d4d3f2a68629d3.jpg",
"filename": "",
"width": 2000,
"height": 3000
}
],
"genres": [
{
"id": 14,
"name": "Reality",
"genres": [
"Reality-TV"
]
}
],
"metacriticSummary": null,
"video": null,
"parentId": 1000990897,
"description": "Chimps living at the North Carolina Zoo, a zoo located in the center of North Carolina that serves as the world's largest natural habitat zoo, as well as one of two state-supported zoos, are cared for",
"rating": null,
"episodeTitle": "Chimp Off the Old Block",
"releaseYear": 2020,
"seoUrl": null,
"episodeAirDate": "/Date(1604102400000)/",
"seasonNumber": 1,
"duration": null
}
},
"links": {
"self": {
"href": "https://backend.tvguide.com/tvschedules/tvguide/programdetails/9000058285/web"
}
},
"meta": {
"componentName": null,
"componentDisplayName": null,
"componentType": null
}
}
+53 -36
View File
@@ -2,22 +2,28 @@ const axios = require('axios')
const dayjs = require('dayjs') const dayjs = require('dayjs')
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 doFetch = require('@ntlab/sfetch')
const debug = require('debug')('site:tvguide.com') const debug = require('debug')('site:tvguide.com')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
doFetch.setDebugger(debug).setCheckResult(false)
const providerId = '9100001138' const providerId = '9100001138'
const maxDuration = 240 const maxDuration = 240
const segments = 1440 / maxDuration const segments = 1440 / maxDuration
const headers = {
'referer': 'https://www.tvguide.com/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
}
module.exports = { module.exports = {
site: 'tvguide.com', site: 'tvguide.com',
days: 2, days: 2,
request: { request: {
headers: function () {
return headers
},
responseType: 'application/json',
decompress: true,
cache: { cache: {
ttl: 24 * 60 * 60 * 1000 // 1 day ttl: 24 * 60 * 60 * 1000 // 1 day
} }
@@ -40,7 +46,7 @@ module.exports = {
`https://backend.tvguide.com/tvschedules/tvguide/${providerId}/web?${params.join('&')}` : `https://backend.tvguide.com/tvschedules/tvguide/${providerId}/web?${params.join('&')}` :
`https://backend.tvguide.com/tvschedules/tvguide/serviceprovider/${providerId}/sources/web?${params.join('&')}` `https://backend.tvguide.com/tvschedules/tvguide/serviceprovider/${providerId}/sources/web?${params.join('&')}`
}, },
async parser({ content, date, channel }) { async parser({ content, date, channel, fetchSegments = true }) {
const programs = [] const programs = []
const f = data => { const f = data => {
const result = [] const result = []
@@ -60,55 +66,66 @@ module.exports = {
return result return result
} }
const queues = f(content) const queues = f(content)
if (queues.length) { if (queues.length && fetchSegments) {
const parts = [] for (let segment = 2; segment <= segments; segment++) {
for (let i = 2; i <= segments; i++) { const segmentUrl = await module.exports.url({ date, segment })
parts.push(await module.exports.url({ date, segment: i })) debug(`fetch segment ${segment}: ${segmentUrl}`)
try {
const res = await axios.get(segmentUrl, { headers })
queues.push(...f(res.data))
} catch (err) {
debug(`Failed to fetch segment ${segment}: ${err.message}`)
}
} }
await doFetch(parts, (url, res) => { }
queues.push(...f(res)) for (const queue of queues) {
}) try {
await doFetch(queues, (queue, res) => { const res = await axios.get(queue.url, { headers })
const item = res?.data?.item ? res.data.item : queue.i const item = res.data?.data?.item || queue.i
programs.push({ programs.push({
title: item.title ? item.title : queue.i.title, title: item.title || queue.i.title,
sub_title: item.episodeNumber ? item.episodeTitle : null, sub_title: item.episodeNumber ? item.episodeTitle : null,
description: item.description, description: item.description,
season: item.seasonNumber, season: item.seasonNumber,
episode: item.episodeNumber, episode: item.episodeNumber,
rating: item.rating ? { system: 'MPA', value: item.rating } : null, rating: item.rating ? { system: 'MPA', value: item.rating } : null,
categories: Array.isArray(item.genres) ? item.genres.map(g => g.name) : null, categories: Array.isArray(item.genres) ? item.genres.map(g => g.name) : null,
start: dayjs.unix(item.startTime ? item.startTime : queue.i.startTime), start: dayjs.unix(item.startTime || queue.i.startTime),
stop: dayjs.unix(item.endTime ? item.endTime : queue.i.endTime) stop: dayjs.unix(item.endTime || queue.i.endTime),
}) })
}) } catch (err) {
debug(`Failed to fetch program details ${queue.url}: ${err.message}`)
}
} }
return programs return programs
}, },
async channels() { async channels() {
const channels = [] const channels = []
const data = await axios try {
.get(await this.url({})) const data = await axios
.then(r => r.data) .get(await this.url({}), { headers })
.catch(console.error) .then(r => r.data)
data.data.items.forEach(item => {
data.data.items.forEach(item => { channels.push({
channels.push({ lang: 'en',
lang: 'en', site_id: item.sourceId,
site_id: item.sourceId, name: item.fullName.replace(/Channel|Schedule/g, '').trim()
name: item.fullName.replace(/Channel|Schedule/g, '').trim() })
}) })
}) } catch (err) {
console.error('Failed to fetch channels:', err.message)
}
return channels return channels
}, },
async fetchApiKey() { async fetchApiKey() {
const data = await axios try {
.get('https://www.tvguide.com/listings/') const data = await axios
.then(r => r.data) .get('https://www.tvguide.com/listings/')
.catch(console.error) .then(r => r.data)
return data ? data.match(/apiKey=([a-zA-Z0-9]+)&/)[1] : null
return data ? data.match(/apiKey=([a-zA-Z0-9]+)&/)[1] : null } catch (err) {
console.error('Failed to fetch API key:', err.message)
return null
}
} }
} }
+41 -58
View File
@@ -5,83 +5,66 @@ const axios = require('axios')
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('2025-01-12', 'YYYY-MM-DD').startOf('d') const date = dayjs.utc('2025-07-29', 'YYYY-MM-DD').startOf('d')
const channel = { const channel = {
site_id: '9200018514', site_id: '9200004683',
xmltv_id: 'CBSEast.us' xmltv_id: 'NatGeoWild.us'
} }
axios.get.mockImplementation(url => {
const result = {}
const urls = {
'https://www.tvguide.com/listings/':
'content.html',
'https://backend.tvguide.com/tvschedules/tvguide/9100001138/web?start=1736640000&duration=240&apiKey=DI9elXhZ3bU6ujsA2gXEKOANyncXGUGc':
'content1.json',
'https://backend.tvguide.com/tvschedules/tvguide/9100001138/web?start=1736654400&duration=240&apiKey=DI9elXhZ3bU6ujsA2gXEKOANyncXGUGc':
'content2.json',
'https://backend.tvguide.com/tvschedules/tvguide/programdetails/9000351140/web':
'program1.json',
'https://backend.tvguide.com/tvschedules/tvguide/programdetails/9000000408/web':
'program2.json',
}
if (urls[url] !== undefined) {
result.data = fs.readFileSync(path.join(__dirname, '__data__', urls[url])).toString()
if (!urls[url].startsWith('content1') && !urls[url].endsWith('.html')) {
result.data = JSON.parse(result.data)
}
}
return Promise.resolve(result)
})
it('can generate valid url', async () => { it('can generate valid url', async () => {
expect(await url({ date })).toBe( axios.get.mockImplementation(url => {
'https://backend.tvguide.com/tvschedules/tvguide/9100001138/web?start=1736640000&duration=240&apiKey=DI9elXhZ3bU6ujsA2gXEKOANyncXGUGc' if (url === 'https://www.tvguide.com/listings/') {
return Promise.resolve({
data: 'html_apiKey=DI9elXhZ3bU6ujsA2gXEKOANyncXGUGc&...'
})
}
throw new Error(`Unexpected URL: ${url}`)
})
const result = await url({ date })
expect(result).toBe(
'https://backend.tvguide.com/tvschedules/tvguide/9100001138/web?start=1753747200&duration=240&apiKey=DI9elXhZ3bU6ujsA2gXEKOANyncXGUGc'
) )
}) })
it('can parse response', async () => { it('can parse response', async () => {
const content = fs.readFileSync(path.join(__dirname, '__data__', 'content1.json')).toString() const content = JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/content.json'), 'utf-8'))
const results = (await parser({ content, channel, date })).map(p => {
axios.get.mockImplementation(url => {
if (
url ===
'https://backend.tvguide.com/tvschedules/tvguide/programdetails/9000058285/web'
) {
return Promise.resolve({
data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program.json')))
})
} else {
return Promise.resolve({ data: '' })
}
})
let results = await parser({ content, date, channel, fetchSegments: false })
results = results.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(5)
expect(results[0]).toMatchObject({ expect(results[0]).toMatchObject({
start: '2025-01-12T01:00:00.000Z', start: '2025-07-29T00:00:00.000Z',
stop: '2025-01-12T02:00:00.000Z', stop: '2025-07-29T01:00:00.000Z',
title: 'FBI: International', title: 'Secrets of the Zoo: North Carolina',
sub_title: 'Gift', sub_title: 'Chimp Off the Old Block',
description: description:
'The owner of a prominent cyber security company is murdered in Copenhagen just before a massive data leak surfaces online, leading the NSA to ask the team for assistance in catching the killer and leaker before more data is revealed.', 'Chimps living at the North Carolina Zoo, a zoo located in the center of North Carolina that serves as the world\'s largest natural habitat zoo, as well as one of two state-supported zoos, are cared for',
categories: ['Action & Adventure', 'Suspense', 'Drama'], categories: ['Reality'],
season: 3, season: 1,
episode: 12, episode: 1,
rating: {
system: 'MPA',
value: 'L'
}
})
expect(results[4]).toMatchObject({
start: '2025-01-12T06:00:00.000Z',
stop: '2025-01-12T08:00:00.000Z',
title: 'Local Programs',
description:
'Local programming information.',
categories: [],
rating: {
system: 'MPA',
value: 'L'
}
}) })
}) })
@@ -89,7 +72,7 @@ it('can handle empty guide', async () => {
const results = await parser({ const results = await parser({
date, date,
channel, channel,
content: fs.readFileSync(path.join(__dirname, '__data__', 'no-content.json')).toString() content: fs.readFileSync(path.resolve(__dirname, '__data__/no-content.json'))
}) })
expect(results).toMatchObject([]) expect(results).toMatchObject([])
}) })