diff --git a/sites/tvtv.us/__data__/content.json b/sites/tvtv.us/__data__/content.json new file mode 100644 index 00000000..e8e95c07 --- /dev/null +++ b/sites/tvtv.us/__data__/content.json @@ -0,0 +1 @@ +[[{"type":"O","startTime":"2025-01-30T00:00Z","start":0,"duration":30,"runTime":30,"flags":["New","HD 1080p"],"programId":"SH047338460000","title":"NY Sports Nation Nightly"},{"subtitle":"The Bow Tie Asymmetry","type":"O","startTime":"2025-01-30T00:30Z","start":120,"duration":30,"runTime":30,"flags":["CC","HD 1080p","HDTV","Stereo"],"programId":"EP009311820269","title":"The Big Bang Theory"},{"subtitle":"Reborn","type":"O","startTime":"2025-01-30T01:00Z","start":240,"duration":60,"runTime":60,"flags":["New","CC","DD 5.1","HD 1080p","HDTV","Stereo"],"programId":"EP029730910107","title":"All American"},{"subtitle":"Week 21","type":"O","startTime":"2025-01-30T02:00Z","start":480,"duration":60,"runTime":60,"flags":["New","CC","DD 5.1","HD 1080p","HDTV","Stereo"],"programId":"EP000388400649","title":"Inside the NFL"},{"type":"N","startTime":"2025-01-30T03:00Z","start":720,"duration":60,"runTime":60,"flags":["New","CC","HD 1080p","HDTV","Stereo"],"programId":"SH019353980000","title":"PIX11 News at Ten"},{"subtitle":"The Label Maker","type":"O","startTime":"2025-01-30T04:00Z","start":960,"duration":30,"runTime":30,"flags":["CC","HD 1080p","HDTV","Stereo"],"programId":"EP000169160107","title":"Seinfeld"},{"subtitle":"The Sniffing Accountant","type":"O","startTime":"2025-01-30T04:30Z","start":1080,"duration":30,"runTime":30,"flags":["CC","HD 1080p","HDTV","Stereo"],"programId":"EP000169160080","title":"Seinfeld"},{"subtitle":"The One That Could Have Been","type":"O","startTime":"2025-01-30T05:00Z","start":1200,"duration":30,"runTime":30,"flags":["CC","HD 1080p","Stereo"],"programId":"EP001151270156","title":"Friends"},{"subtitle":"It's Always Nazi Week","type":"O","startTime":"2025-01-30T05:30Z","start":1320,"duration":30,"runTime":30,"flags":["CC","HD 1080p","HDTV","Stereo"],"programId":"EP005927330125","title":"Two and a Half Men"},{"subtitle":"The Wildebeest Implementation","type":"O","startTime":"2025-01-30T06:00Z","start":1440,"duration":30,"runTime":30,"flags":["CC","HD 1080p","HDTV","Stereo"],"programId":"EP009311820089","title":"The Big Bang Theory"},{"subtitle":"A Box of Treasure and the Meemaw of Science","type":"O","startTime":"2025-01-30T06:30Z","start":1560,"duration":30,"runTime":30,"flags":["CC","DVS","HD 1080p","HDTV","Stereo"],"programId":"EP026422390078","title":"Young Sheldon"},{"type":"O","startTime":"2025-01-30T07:00Z","start":1680,"duration":30,"runTime":30,"flags":["CC","HD 1080p"],"programId":"SH000000010000","title":"Paid Programming"},{"subtitle":"Twanging Your Magic Clanger","type":"O","startTime":"2025-01-30T07:30Z","start":1800,"duration":30,"runTime":30,"flags":["CC","HD 1080p","HDTV","Stereo"],"programId":"EP005927330177","title":"Two and a Half Men"},{"subtitle":"Useless Potheads","type":"O","startTime":"2025-01-30T08:00Z","start":1920,"duration":30,"runTime":30,"flags":["CC","HD 1080p","HDTV","Stereo"],"programId":"EP032302210008","title":"Bob Hearts Abishola"},{"subtitle":"Oates & Oates","type":"O","startTime":"2025-01-30T08:30Z","start":2040,"duration":30,"runTime":30,"flags":["CC","DVS","HD 1080p","HDTV","Stereo"],"programId":"EP017396170163","title":"The Goldbergs"},{"type":"N","startTime":"2025-01-30T09:00Z","start":2160,"duration":30,"runTime":30,"flags":["New","CC","HD 1080p","HDTV","Stereo"],"programId":"SH040688380000","title":"PIX11 Morning News at 4am"},{"type":"N","startTime":"2025-01-30T09:30Z","start":2280,"duration":30,"runTime":30,"flags":["New","CC","HD 1080p","HDTV","Stereo"],"programId":"SH030981260000","title":"PIX11 Morning News at 4:30am"},{"type":"N","startTime":"2025-01-30T10:00Z","start":2400,"duration":60,"runTime":60,"flags":["New","CC","HD 1080p","HDTV","Stereo"],"programId":"SH024105120000","title":"PIX11 Morning News at 5am"},{"type":"N","startTime":"2025-01-30T11:00Z","start":2640,"duration":60,"runTime":60,"flags":["New","CC","HD 1080p","HDTV","Stereo"],"programId":"SH030981290000","title":"PIX11 Morning News at 6am"},{"type":"N","startTime":"2025-01-30T12:00Z","start":2880,"duration":120,"runTime":120,"flags":["New","CC","HD 1080p","HDTV","Stereo"],"programId":"SH030981310000","title":"PIX11 Morning News at 7am"},{"type":"N","startTime":"2025-01-30T14:00Z","start":3360,"duration":60,"runTime":60,"flags":["New","CC","HD 1080p","HDTV","Stereo"],"programId":"SH033926520000","title":"PIX11 Morning News at 9am"},{"type":"O","startTime":"2025-01-30T15:00Z","start":3600,"duration":60,"runTime":60,"flags":["New","CC","HD 1080p","HDTV","Stereo"],"programId":"SH041806690000","title":"New York Living"},{"type":"R","startTime":"2025-01-30T16:00Z","start":3840,"duration":30,"runTime":30,"flags":["New","CC","HD 1080p","HDTV","Stereo"],"programId":"EP048030230139","title":"Mathis Court With Judge Mathis"},{"subtitle":"Drop Dead Dropout & I Fooled You","type":"R","startTime":"2025-01-30T16:30Z","start":3960,"duration":30,"runTime":30,"flags":["CC","HD 1080p","HDTV","Stereo"],"programId":"EP048030230103","title":"Mathis Court With Judge Mathis"},{"type":"O","startTime":"2025-01-30T17:00Z","start":4080,"duration":60,"runTime":60,"flags":["CC","HD 1080p","HDTV","Stereo"],"programId":"SH009511390000","title":"The Steve Wilkos Show"},{"subtitle":"Unlock the Truth: Did My Bestie Drug Me?; DNA: Adopted and Seeking the Truth","type":"O","startTime":"2025-01-30T18:00Z","start":4320,"duration":60,"runTime":60,"flags":["New","CC","HD 1080p","HDTV","Stereo"],"programId":"EP043399210359","title":"Karamo"},{"subtitle":"Buyer Beware","type":"R","startTime":"2025-01-30T19:00Z","start":4560,"duration":30,"runTime":30,"flags":["CC","HD 1080p","HDTV","Stereo"],"programId":"EP040033230087","title":"Judy Justice"},{"subtitle":"Bitter Break-Up; Dissatisfied Tenants","type":"R","startTime":"2025-01-30T19:30Z","start":4680,"duration":30,"runTime":30,"flags":["CC","HD 1080p","HDTV","Stereo"],"programId":"EP040033230091","title":"Judy Justice"},{"subtitle":"Dr. Phil Interrogates: Did He Lie to the Nation?","type":"O","startTime":"2025-01-30T20:00Z","start":4800,"duration":60,"runTime":60,"flags":["CC","HD 1080p","HDTV","Stereo"],"programId":"EP005178511881","title":"Dr. Phil"},{"type":"N","startTime":"2025-01-30T21:00Z","start":5040,"duration":60,"runTime":60,"flags":["New","CC","HD 1080p","HDTV","Stereo"],"programId":"SH039765200000","title":"PIX11 News at 4"},{"type":"N","startTime":"2025-01-30T22:00Z","start":5280,"duration":60,"runTime":60,"flags":["New","CC","HD 1080p","HDTV","Stereo"],"programId":"SH019353950000","title":"PIX11 News at 5"},{"type":"N","startTime":"2025-01-30T23:00Z","start":5520,"duration":30,"runTime":30,"flags":["New","CC","HD 1080p","HDTV","Stereo"],"programId":"SH021560980000","title":"PIX11 News at 6"},{"type":"N","startTime":"2025-01-30T23:30Z","start":5644,"duration":30,"runTime":30,"flags":["New","CC","HD 1080p","HDTV","Stereo"],"programId":"SH042401180000","title":"PIX11 Evening News"}]] \ No newline at end of file diff --git a/sites/tvtv.us/__data__/program_1.json b/sites/tvtv.us/__data__/program_1.json new file mode 100644 index 00000000..323a5b43 --- /dev/null +++ b/sites/tvtv.us/__data__/program_1.json @@ -0,0 +1 @@ +{"type":"E","title":"The Big Bang Theory","image":"/gn/pi/assets/p185554_b_v11_az.jpg?w=240&h=360","description":"When Amy's parents and Sheldon's family arrive, everybody is focused on making sure the wedding arrangements go according to plan -- everyone except the bride and groom.","releaseYear":2018,"mainCast":["Johnny Galecki (Leonard Hofstadter)","Jim Parsons (Sheldon Cooper)","Kaley Cuoco (Penny)"],"directors":["Mark Cendrowski"],"genres":["Sitcom"],"ratings":[{"code":"TVPG","body":"USA Parental Rating"}],"seriesEpisode":{"seriesId":"185554","episodeTitle":"The Bow Tie Asymmetry","image":"/gn/pi/assets/p15015849_e_v8_aa.jpg?w=240&h=360","seasonEpisode":"Season 11; Episode 24"},"cast":[{"personId":"33631","name":"Johnny Galecki","role":"Leonard Hofstadter"},{"personId":"314170","name":"Jim Parsons","role":"Sheldon Cooper"},{"personId":"169721","name":"Kaley Cuoco","role":"Penny"},{"personId":"220961","name":"Simon Helberg","role":"Howard Wolowitz"},{"personId":"508075","name":"Kunal Nayyar","role":"Raj Koothrappali"},{"personId":"155","name":"Mayim Bialik","role":"Amy Farrah Fowler"},{"personId":"530748","name":"Melissa Rauch","role":"Bernadette Rostenkowski"},{"personId":"308458","name":"Kevin Sussman","role":"Stuart - Guest Star"},{"personId":"38285","name":"Laurie Metcalf","role":"Mary - Guest Star"},{"personId":"260209","name":"John Ross Bowie","role":"Kripke - Guest Star"},{"personId":"65798","name":"Wil Wheaton","role":"Himself - Guest Star"},{"personId":"181887","name":"Brian Posehn","role":"Bert - Guest Star"},{"personId":"31226","name":"Jerry O'Connell","role":"George - Guest Star"},{"personId":"271536","name":"Courtney Henggeler","role":"Missy - Guest Star"},{"personId":"620417","name":"Lauren Lapkus","role":"Denise - Guest Star"},{"personId":"232486","name":"Teller","role":"Mr. Fowler - Guest Star"},{"personId":"106","name":"Kathy Bates","role":"Mrs. Fowler - Guest Star"},{"personId":"73414","name":"Mark Hamill","role":"Himself - Guest Star"}],"crew":[{"personId":"75481","name":"Chuck Lorre","role":"Executive Producer"},{"personId":"232097","name":"Bill Prady","role":"Executive Producer"},{"personId":"262338","name":"Steven Molaro","role":"Executive Producer"},{"personId":"75481","name":"Chuck Lorre","role":"Writer"},{"personId":"262338","name":"Steven Molaro","role":"Writer"},{"personId":"490163","name":"Maria Ferrari","role":"Writer"},{"personId":"278500","name":"Steve Holland","role":"Writer"},{"personId":"383184","name":"Eric Kaplan","role":"Writer"},{"personId":"643632","name":"Tara Hernandez","role":"Writer"},{"personId":"188536","name":"Mark Cendrowski","role":"Director"}]} \ No newline at end of file diff --git a/sites/tvtv.us/tvtv.us.config.js b/sites/tvtv.us/tvtv.us.config.js index 098be11f..eef953fc 100644 --- a/sites/tvtv.us/tvtv.us.config.js +++ b/sites/tvtv.us/tvtv.us.config.js @@ -1,22 +1,16 @@ const dayjs = require('dayjs') -const utc = require('dayjs/plugin/utc') -dayjs.extend(utc) +let cachedPrograms = {} module.exports = { site: 'tvtv.us', days: 2, - url: function ({ date, channel }) { - if (!dayjs.isDayjs(date)) { - throw new Error('Invalid date object passed to url function') - } - + url({ date, channel }) { return `https://www.tvtv.us/api/v1/lineup/USA-NY71652-X/grid/${date.toJSON()}/${date .add(1, 'day') .toJSON()}/${channel.site_id}` }, request: { - method: 'GET', headers: { Accept: '*/*', Connection: 'keep-alive', @@ -27,27 +21,131 @@ module.exports = { 'sec-ch-ua-platform': '"Windows"' } }, - parser: function ({ content }) { + async parser(ctx) { let programs = [] + let queue = [] + + const items = parseItems(ctx.content) + for (const item of items) { + const start = dayjs(item.startTime) + const stop = start.add(item.duration, 'minute') - const items = parseItems(content) - items.forEach(item => { - const start = dayjs.utc(item.startTime) - const stop = start.add(item.runTime, 'minute') programs.push({ + id: item.programId, title: item.title, - description: item.subtitle, + subtitle: item.subtitle || null, start, stop }) + + // NOTE: This part of the code is commented out because loading additional data leads either to error 429 Too Many Requests or to even greater delays between requests. + // if (item.programId && !cachedPrograms[item.programId]) { + // queue.push({ + // programId: item.programId, + // url: `https://tvtv.us/api/v1/programs/${item.programId}`, + // httpAgent: ctx.request.agent, + // httpsAgent: ctx.request.agent, + // headers: module.exports.request.headers + // }) + // } + } + + const axios = require('axios') + for (const req of queue) { + await wait(5000) + + const data = await axios(req) + .then(r => r.data) + .catch(console.error) + + if (!data || !data.title) continue + + cachedPrograms[req.programId] = data + } + + programs.forEach(program => { + const data = cachedPrograms[program.id] + + if (!data) return + + program.description = data.description || null + program.image = data.image ? `https://tvtv.us${data.image}` : null + program.date = data.releaseYear ? data.releaseYear.toString() : null + program.directors = data.directors + program.categories = data.genres + program.actors = parseActors(data) + program.writers = parseWriters(data) + program.producers = parseProducers(data) + program.ratings = parseRatings(data) + program.season = parseSeason(data) + program.episode = parseEpisode(data) }) return programs } } -function parseItems(content) { - const json = JSON.parse(content) - if (!json.length) return [] - return json[0] +function parseEpisode(data) { + if (!data?.seriesEpisode?.seasonEpisode) return null + + const [, episode] = data.seriesEpisode.seasonEpisode.match(/Episode (\d+)/) || [null, null] + + return episode ? parseInt(episode) : null +} + +function parseSeason(data) { + if (!data?.seriesEpisode?.seasonEpisode) return null + + const [, season] = data.seriesEpisode.seasonEpisode.match(/Season (\d+);/) || [null, null] + + return season ? parseInt(season) : null +} + +function parseRatings(data) { + return Array.isArray(data.ratings) + ? data.ratings.map(rating => ({ + value: rating.code, + system: rating.body + })) + : [] +} + +function parseWriters(data) { + return data.crew.filter(member => member.role.includes('Writer')).map(member => member.name) +} + +function parseProducers(data) { + return data.crew.filter(member => member.role.includes('Producer')).map(member => member.name) +} + +function parseActors(data) { + return data.cast.map(actor => { + const guest = actor.role.includes('Guest Star') ? 'yes' : undefined + const role = actor.role.replace(' - Guest Star', '') + + return { + value: actor.name, + role, + guest + } + }) +} + +function parseItems(content) { + try { + const json = JSON.parse(content) + if (!json.length) return [] + + return json[0] + } catch { + return [] + } +} + +function wait(ms) { + if (process.env.NODE_ENV === 'test') return + + return new Promise(resolve => { + setTimeout(resolve, ms) + }) } diff --git a/sites/tvtv.us/tvtv.us.test.js b/sites/tvtv.us/tvtv.us.test.js index 66687893..2a5e6c9a 100644 --- a/sites/tvtv.us/tvtv.us.test.js +++ b/sites/tvtv.us/tvtv.us.test.js @@ -1,52 +1,172 @@ const { parser, url } = require('./tvtv.us.config.js') +const fs = require('fs') +const path = require('path') +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 date = dayjs.utc('2022-09-20', 'YYYY-MM-DD').startOf('d') -const channel = { - site_id: '62670', - xmltv_id: 'AMITV.ca', - logo: 'https://tvtv.us/gn/i/assets/s62670_ll_h15_ab.png?w=360&h=270' -} +jest.mock('axios') + +axios.mockImplementation(req => { + if (req.url === 'https://tvtv.us/api/v1/programs/EP009311820269') { + return Promise.resolve({ + data: JSON.parse(fs.readFileSync(path.resolve(__dirname, '__data__/program_1.json'))) + }) + } else { + return Promise.resolve({ data: '' }) + } +}) + +const date = dayjs.utc('2025-01-30', 'YYYY-MM-DD').startOf('d') +const channel = { site_id: '20373' } it('can generate valid url', () => { expect(url({ channel, date })).toBe( - 'https://www.tvtv.us/api/v1/lineup/USA-NY71652-X/grid/2022-09-20T00:00:00.000Z/2022-09-21T00:00:00.000Z/62670' + 'https://www.tvtv.us/api/v1/lineup/USA-NY71652-X/grid/2025-01-30T00:00:00.000Z/2025-01-31T00:00:00.000Z/20373' ) }) -it('can parse response', () => { - const content = - '[[{"programId":"EP039131940001","title":"Beyond the Field","subtitle":"Diversity in Sport","flags":["CC","DVS"],"type":"O","startTime":"2022-09-20T00:00Z","start":0,"duration":30,"runTime":30},{"programId":"EP032368970002","title":"IGotThis","subtitle":"Listen to Dis","flags":["CC","DVS"],"type":"O","startTime":"2022-09-20T00:30Z","start":120,"duration":30,"runTime":30}]]' +it('can parse response', async () => { + const content = fs.readFileSync(path.resolve(__dirname, '__data__/content.json')) - const result = parser({ content }).map(p => { + let results = await parser({ content, request: { agent: null } }) + results = results.map(p => { p.start = p.start.toJSON() p.stop = p.stop.toJSON() return p }) - expect(result).toMatchObject([ - { - start: '2022-09-20T00:00:00.000Z', - stop: '2022-09-20T00:30:00.000Z', - title: 'Beyond the Field', - description: 'Diversity in Sport' - }, - { - start: '2022-09-20T00:30:00.000Z', - stop: '2022-09-20T01:00:00.000Z', - title: 'IGotThis', - description: 'Listen to Dis' - } - ]) + expect(results.length).toBe(33) + expect(results[0]).toMatchObject({ + start: '2025-01-30T00:00:00.000Z', + stop: '2025-01-30T00:30:00.000Z', + title: 'NY Sports Nation Nightly', + subtitle: null + }) + expect(results[1]).toMatchObject({ + start: '2025-01-30T00:30:00.000Z', + stop: '2025-01-30T01:00:00.000Z', + title: 'The Big Bang Theory', + subtitle: 'The Bow Tie Asymmetry' + // description: + // "When Amy's parents and Sheldon's family arrive, everybody is focused on making sure the wedding arrangements go according to plan -- everyone except the bride and groom.", + // image: 'https://tvtv.us/gn/pi/assets/p185554_b_v11_az.jpg?w=240&h=360', + // date: '2018', + // season: 11, + // episode: 24, + // actors: [ + // { + // value: 'Johnny Galecki', + // role: 'Leonard Hofstadter' + // }, + // { + // value: 'Jim Parsons', + // role: 'Sheldon Cooper' + // }, + // { + // value: 'Kaley Cuoco', + // role: 'Penny' + // }, + // { + // value: 'Simon Helberg', + // role: 'Howard Wolowitz' + // }, + // { + // value: 'Kunal Nayyar', + // role: 'Raj Koothrappali' + // }, + // { + // value: 'Mayim Bialik', + // role: 'Amy Farrah Fowler' + // }, + // { + // value: 'Melissa Rauch', + // role: 'Bernadette Rostenkowski' + // }, + // { + // value: 'Kevin Sussman', + // role: 'Stuart', + // guest: 'yes' + // }, + // { + // value: 'Laurie Metcalf', + // role: 'Mary', + // guest: 'yes' + // }, + // { + // value: 'John Ross Bowie', + // role: 'Kripke', + // guest: 'yes' + // }, + // { + // value: 'Wil Wheaton', + // role: 'Himself', + // guest: 'yes' + // }, + // { + // value: 'Brian Posehn', + // role: 'Bert', + // guest: 'yes' + // }, + // { + // value: "Jerry O'Connell", + // role: 'George', + // guest: 'yes' + // }, + // { + // value: 'Courtney Henggeler', + // role: 'Missy', + // guest: 'yes' + // }, + // { + // value: 'Lauren Lapkus', + // role: 'Denise', + // guest: 'yes' + // }, + // { + // value: 'Teller', + // role: 'Mr. Fowler', + // guest: 'yes' + // }, + // { + // value: 'Kathy Bates', + // role: 'Mrs. Fowler', + // guest: 'yes' + // }, + // { + // value: 'Mark Hamill', + // role: 'Himself', + // guest: 'yes' + // } + // ], + // directors: ['Mark Cendrowski'], + // producers: ['Chuck Lorre', 'Bill Prady', 'Steven Molaro'], + // writers: [ + // 'Chuck Lorre', + // 'Steven Molaro', + // 'Maria Ferrari', + // 'Steve Holland', + // 'Eric Kaplan', + // 'Tara Hernandez' + // ], + // categories: ['Sitcom'], + // ratings: [ + // { + // value: 'TVPG', + // system: 'USA Parental Rating' + // } + // ] + }) }) -it('can handle empty guide', () => { - const result = parser({ - content: '[]' +it('can handle empty guide', async () => { + const results = await parser({ + content: '[]', + request: { agent: null } }) - expect(result).toMatchObject([]) + + expect(results).toMatchObject([]) })