From 8d7502c437b095a018f7ba58b5172fcaca19a8da Mon Sep 17 00:00:00 2001 From: luuvasara Date: Fri, 10 Jul 2026 22:37:04 +0300 Subject: [PATCH] Fix dna.fi guide: full-day results, hard errors on bad responses The grabber requested each day as a fixed UTC+2 window. The API's broadcast days start at 04:00 Europe/Helsinki, so during daylight saving time (UTC+3) the misaligned interval was answered with a "303 See Other" redirect to a widened two-day window. On top of that the API paginates 20 items per response by default, so each day was truncated to the first ~20 programmes. - Request the exact broadcast-day window in Europe/Helsinki time - Add limit=1000 so a full day fits in one response - Drop the local-timezone day filter: the requested interval already bounds the results, and the filter discarded late-night programmes depending on the runner's timezone - Throw on malformed or unrecognized responses instead of returning an empty list, so failures are reported as errors rather than a silently empty (but "successful") guide --- sites/dna.fi/dna.fi.config.js | 54 +++++++++++++++++++++-------------- sites/dna.fi/dna.fi.test.js | 16 ++++++++++- 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/sites/dna.fi/dna.fi.config.js b/sites/dna.fi/dna.fi.config.js index 228254a21..a9687907e 100644 --- a/sites/dna.fi/dna.fi.config.js +++ b/sites/dna.fi/dna.fi.config.js @@ -1,20 +1,35 @@ const axios = require('axios') const dayjs = require('dayjs') +const utc = require('dayjs/plugin/utc') +const timezone = require('dayjs/plugin/timezone') + +dayjs.extend(utc) +dayjs.extend(timezone) + +// The API groups programmes into broadcast days that start at 04:00 +// Europe/Helsinki. Requesting exactly that window (instead of a fixed +// UTC+2 offset) keeps the interval aligned year-round; a misaligned +// interval gets answered with a "303 See Other" to a widened window. +// Without an explicit `limit` the API paginates 20 items per response, +// so only the first ~20 programmes of each day would be returned. +const dayStart = date => dayjs.tz(date.format('YYYY-MM-DD'), 'Europe/Helsinki').add(4, 'h') module.exports = { site: 'dna.fi', days: 2, url({ date, channel }) { - const beginTimestamp = date.add(2, 'h').valueOf() - const endTimestamp = date.add(1, 'd').add(2, 'h').subtract(1, 's').valueOf() + const beginTimestamp = dayStart(date).valueOf() + const endTimestamp = dayStart(date.add(1, 'd')).subtract(1, 's').valueOf() - return `https://mts-pro-envoy-vip.dna.fi/hbx/api/pub/xrtv/g/media?q=channel:${channel.site_id}&q=profile:pr&q=start-interval:${beginTimestamp}/${endTimestamp}` + return `https://mts-pro-envoy-vip.dna.fi/hbx/api/pub/xrtv/g/media?q=channel:${channel.site_id}&q=profile:pr&q=start-interval:${beginTimestamp}/${endTimestamp}&limit=1000` }, - parser({ content, date }) { + parser({ content }) { let programs = [] - let items = parseItems(content, date) + let items = parseItems(content) items.forEach(item => { const data = item?._embedded?.['xrtv:meta']?.data + if (!data?.start) return + programs.push({ title: data?.title, subtitle: data?.episode_title, @@ -27,8 +42,8 @@ module.exports = { images: parseImages(item), directors: parseCast(data, 'director'), actors: parseCast(data, 'actors'), - start: dayjs(data?.start), - stop: dayjs(data?.end) + start: dayjs(data.start), + stop: dayjs(data.end) }) }) @@ -80,20 +95,17 @@ function parseImages(item) { return Array.isArray(images) ? images.map(image => image.src) : [] } -function parseItems(content, date) { - try { - const data = JSON.parse(content) - let items = data?._embedded?.['xrtv:media-item'] - items = Array.isArray(items) ? items : [] - items = items.filter(item => { - const start = item?._embedded?.['xrtv:meta']?.data?.start - if (!start) return false +function parseItems(content) { + if (!content) return [] - return date.isSame(dayjs(start), 'day') - }) - - return items - } catch { - return [] + // Let malformed content (e.g. an HTML error page) throw, so the grab + // is reported as an error instead of silently producing no programmes. + const data = JSON.parse(content) + if (!data?.page) { + throw new Error('Unexpected response structure') } + + const items = data?._embedded?.['xrtv:media-item'] + + return Array.isArray(items) ? items : [] } diff --git a/sites/dna.fi/dna.fi.test.js b/sites/dna.fi/dna.fi.test.js index 9373e5941..45a137190 100644 --- a/sites/dna.fi/dna.fi.test.js +++ b/sites/dna.fi/dna.fi.test.js @@ -15,7 +15,14 @@ const channel = { it('can generate valid url', async () => { expect(url({ date, channel })).toBe( - 'https://mts-pro-envoy-vip.dna.fi/hbx/api/pub/xrtv/g/media?q=channel:ch-216356&q=profile:pr&q=start-interval:1736906400000/1736992799000' + 'https://mts-pro-envoy-vip.dna.fi/hbx/api/pub/xrtv/g/media?q=channel:ch-216356&q=profile:pr&q=start-interval:1736906400000/1736992799000&limit=1000' + ) +}) + +it('can generate valid url during daylight saving time', async () => { + const dstDate = dayjs.utc('2025-07-15', 'YYYY-MM-DD').startOf('d') + expect(url({ date: dstDate, channel })).toBe( + 'https://mts-pro-envoy-vip.dna.fi/hbx/api/pub/xrtv/g/media?q=channel:ch-216356&q=profile:pr&q=start-interval:1752541200000/1752627599000&limit=1000' ) }) @@ -136,3 +143,10 @@ it('can handle empty guide', () => { expect(results).toMatchObject([]) }) + +it('throws on unexpected response', () => { + expect(() => parser({ date, content: 'Error' })).toThrow() + expect(() => parser({ date, content: '{"message":"Internal Server Error"}' })).toThrow( + 'Unexpected response structure' + ) +})