From 9672efd80dd58786d53152ac218f22d3dd616dd3 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Wed, 14 Oct 2026 15:07:46 +0800 Subject: [PATCH] fix(json): serialize podcast durations as numeric seconds Affected files: - lib/views/json.test.ts - lib/views/json.ts --- lib/views/json.test.ts | 27 +++++++++++++++++++++++++++ lib/views/json.ts | 22 ++++++++++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/lib/views/json.test.ts b/lib/views/json.test.ts index 28fa1302bd..690d7ca69b 100644 --- a/lib/views/json.test.ts +++ b/lib/views/json.test.ts @@ -3,6 +3,33 @@ import { describe, expect, it } from 'vitest'; import jsonView from '@/views/json'; describe('JSON view', () => { + it.each([ + [65, 65], + ['65', 65], + ['0:01:05', 65], + ['01:02:03', 3723], + ['90:00', 5400], + ['0:00:01.5', 1.5], + [0, 0], + ['invalid', undefined], + ['0:99:00', undefined], + [-1, undefined], + [Infinity, undefined], + ])('serializes podcast duration %s as numeric seconds', (duration, seconds) => { + const feed = JSON.parse( + jsonView({ + title: 'Podcast', + item: [{ title: 'Episode', enclosure_url: 'https://example.com/audio.mp3', enclosure_type: 'audio/mpeg', itunes_duration: duration }], + }) + ); + const attachment = feed.items[0].attachments[0]; + if (seconds === undefined) { + expect(attachment).not.toHaveProperty('duration_in_seconds'); + } else { + expect(attachment.duration_in_seconds).toBe(seconds); + } + }); + it('renders summary, authors, tags, attachments, and extras', () => { const jsonOutput = jsonView({ title: 'JSON Feed', diff --git a/lib/views/json.ts b/lib/views/json.ts index 091ab79e6b..4c0895b0e2 100644 --- a/lib/views/json.ts +++ b/lib/views/json.ts @@ -1,4 +1,22 @@ -import type { Data } from '@/types'; +import type { Data, DataItem } from '@/types'; + +const toDurationInSeconds = (duration: DataItem['itunes_duration']) => { + if (typeof duration === 'number') { + return Number.isFinite(duration) && duration >= 0 ? duration : undefined; + } + if (typeof duration !== 'string') { + return; + } + if (/^\d+(?:\.\d+)?$/.test(duration)) { + const seconds = Number(duration); + return Number.isFinite(seconds) ? seconds : undefined; + } + if (!/^\d+:(?:[0-5]?\d:)?[0-5]?\d(?:\.\d+)?$/.test(duration)) { + return; + } + const seconds = duration.split(':').reduce((total, part) => total * 60 + Number(part), 0); + return Number.isFinite(seconds) ? seconds : undefined; +}; /** * This function should be used by RSSHub middleware only. @@ -39,7 +57,7 @@ const json = (data: Data) => { mime_type: item.enclosure_type, title: item.enclosure_title, size_in_bytes: item.enclosure_length, - duration_in_seconds: item.itunes_duration, + duration_in_seconds: toDurationInSeconds(item.itunes_duration), }, ] : undefined),