From dcad343674f9a78fe43ac0344e1df06fe4a35a58 Mon Sep 17 00:00:00 2001 From: Nathan Gathright Date: Sat, 6 Sep 2025 13:48:29 -0500 Subject: [PATCH 1/4] feat: add Phase 8 structure foundation - Create phase-8.ts module with placeholder structure - Add Phase 8 section to phase index with placeholder comment - Prepare infrastructure for future Phase 8 tag implementations - No specific tags implemented yet - ready for development --- src/parser/phase/index.ts | 3 +++ src/parser/phase/phase-8.ts | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 src/parser/phase/phase-8.ts diff --git a/src/parser/phase/index.ts b/src/parser/phase/index.ts index bd05e09..aca2975 100644 --- a/src/parser/phase/index.ts +++ b/src/parser/phase/index.ts @@ -92,6 +92,9 @@ const feeds: FeedUpdate[] = [ phase7.podcastChat, phase7.podcastPublisher, + // Phase 8 - Currently no tags implemented + // phase8.* tags will be added here as they are implemented + pending.id, pending.social, pending.podcastRecommendations, diff --git a/src/parser/phase/phase-8.ts b/src/parser/phase/phase-8.ts new file mode 100644 index 0000000..d5c90ea --- /dev/null +++ b/src/parser/phase/phase-8.ts @@ -0,0 +1,23 @@ +// Phase 8 - Podcast Namespace +// This phase will contain podcast namespace tags that are confirmed for Phase 8 +// Currently no tags are implemented in this phase + +// Placeholder for future Phase 8 implementations +// Example structure for future tags: +// export const exampleTag = { +// phase: 8, +// name: "example", +// tag: "podcast:example", +// nodeTransform: firstIfArray, +// supportCheck: (node: XmlNode): boolean => Boolean(getAttribute(node, "requiredAttr")), +// fn(node: XmlNode): { exampleTag: ExampleType } { +// return { +// exampleTag: { +// // extracted properties +// }, +// }; +// }, +// }; + +// Export empty object to make this a valid module +export {}; From e532955d8016164647f65b3d1715d41c56d6ff77 Mon Sep 17 00:00:00 2001 From: Nathan Gathright Date: Sat, 6 Sep 2025 14:07:34 -0500 Subject: [PATCH 2/4] feat: implement podcast:follow tag in Phase 8 - Add Phase8Follow type definition with required url field - Implement podcastFollow parser with proper validation - Add podcastFollow field to FeedObject interface in Phase 8 section - Register podcastFollow in phase processing pipeline - Add comprehensive test coverage (7 test cases) - Handle edge cases: missing url, empty url, whitespace-only url - Support multiple follow tags (takes first one) - All tests pass (311 total tests) - Clean build successful --- src/parser/phase/__test__/phase-8.test.ts | 95 +++++++++++++++++++++++ src/parser/phase/index.ts | 4 +- src/parser/phase/phase-8.ts | 41 +++++----- src/parser/types.ts | 5 ++ 4 files changed, 122 insertions(+), 23 deletions(-) create mode 100644 src/parser/phase/__test__/phase-8.test.ts diff --git a/src/parser/phase/__test__/phase-8.test.ts b/src/parser/phase/__test__/phase-8.test.ts new file mode 100644 index 0000000..6e3420b --- /dev/null +++ b/src/parser/phase/__test__/phase-8.test.ts @@ -0,0 +1,95 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import * as helpers from "../../__test__/helpers"; + +const phase = 8; + +describe("phase 8", () => { + let feed; + beforeAll(async () => { + feed = await helpers.loadSimple(); + }); + + describe("podcast:follow", () => { + const supportedName = "follow"; + + it("correctly identifies a basic feed", () => { + const result = helpers.parseValidFeed(feed); + + expect(result).not.toHaveProperty("podcastFollow"); + expect(helpers.getPhaseSupport(result, phase)).not.toContain(supportedName); + }); + + it("ignores missing url", () => { + const xml = helpers.spliceFeed( + feed, + // missing url, not valid + `` + ); + const result = helpers.parseValidFeed(xml); + + expect(result).not.toHaveProperty("podcastFollow"); + expect(helpers.getPhaseSupport(result, phase)).not.toContain(supportedName); + }); + + it("extracts a single follow url", () => { + const xml = helpers.spliceFeed( + feed, + `` + ); + const result = helpers.parseValidFeed(xml); + + expect(result).toHaveProperty("podcastFollow"); + expect(result.podcastFollow).toHaveProperty("url", "https://examplehost.com/feed/12345678/followlinks.json"); + expect(helpers.getPhaseSupport(result, phase)).toContain(supportedName); + }); + + it("extracts follow url with different domain", () => { + const xml = helpers.spliceFeed( + feed, + `` + ); + const result = helpers.parseValidFeed(xml); + + expect(result).toHaveProperty("podcastFollow"); + expect(result.podcastFollow).toHaveProperty("url", "https://podnews.net/followlinks.json"); + expect(helpers.getPhaseSupport(result, phase)).toContain(supportedName); + }); + + it("handles multiple follow tags by taking the first one", () => { + const xml = helpers.spliceFeed( + feed, + ` + ` + ); + const result = helpers.parseValidFeed(xml); + + expect(result).toHaveProperty("podcastFollow"); + expect(result.podcastFollow).toHaveProperty("url", "https://examplehost.com/feed/12345678/followlinks.json"); + expect(helpers.getPhaseSupport(result, phase)).toContain(supportedName); + }); + + it("handles empty url attribute", () => { + const xml = helpers.spliceFeed( + feed, + `` + ); + const result = helpers.parseValidFeed(xml); + + expect(result).not.toHaveProperty("podcastFollow"); + expect(helpers.getPhaseSupport(result, phase)).not.toContain(supportedName); + }); + + it("handles whitespace-only url attribute", () => { + const xml = helpers.spliceFeed( + feed, + `` + ); + const result = helpers.parseValidFeed(xml); + + expect(result).not.toHaveProperty("podcastFollow"); + expect(helpers.getPhaseSupport(result, phase)).not.toContain(supportedName); + }); + }); +}); diff --git a/src/parser/phase/index.ts b/src/parser/phase/index.ts index aca2975..6b9cce7 100644 --- a/src/parser/phase/index.ts +++ b/src/parser/phase/index.ts @@ -15,6 +15,7 @@ import * as phase4 from "./phase-4"; import * as phase5 from "./phase-5"; import * as phase6 from "./phase-6"; import * as phase7 from "./phase-7"; +import * as phase8 from "./phase-8"; import * as pending from "./phase-pending"; import { XmlNodeSource } from "./types"; @@ -92,8 +93,7 @@ const feeds: FeedUpdate[] = [ phase7.podcastChat, phase7.podcastPublisher, - // Phase 8 - Currently no tags implemented - // phase8.* tags will be added here as they are implemented + phase8.podcastFollow, pending.id, pending.social, diff --git a/src/parser/phase/phase-8.ts b/src/parser/phase/phase-8.ts index d5c90ea..0e017d3 100644 --- a/src/parser/phase/phase-8.ts +++ b/src/parser/phase/phase-8.ts @@ -1,23 +1,22 @@ -// Phase 8 - Podcast Namespace -// This phase will contain podcast namespace tags that are confirmed for Phase 8 -// Currently no tags are implemented in this phase +import { firstIfArray, getAttribute, getKnownAttribute } from "../shared"; +import type { XmlNode } from "../types"; -// Placeholder for future Phase 8 implementations -// Example structure for future tags: -// export const exampleTag = { -// phase: 8, -// name: "example", -// tag: "podcast:example", -// nodeTransform: firstIfArray, -// supportCheck: (node: XmlNode): boolean => Boolean(getAttribute(node, "requiredAttr")), -// fn(node: XmlNode): { exampleTag: ExampleType } { -// return { -// exampleTag: { -// // extracted properties -// }, -// }; -// }, -// }; +export type Phase8Follow = { + url: string; +}; -// Export empty object to make this a valid module -export {}; +export const podcastFollow = { + phase: 8, + name: "follow", + tag: "podcast:follow", + nodeTransform: firstIfArray, + supportCheck: (node: XmlNode): boolean => + Boolean(getAttribute(node, "url")), + fn(node: XmlNode): { podcastFollow: Phase8Follow } { + return { + podcastFollow: { + url: getKnownAttribute(node, "url"), + }, + }; + }, +}; diff --git a/src/parser/types.ts b/src/parser/types.ts index c5119f6..38e6bee 100644 --- a/src/parser/types.ts +++ b/src/parser/types.ts @@ -22,6 +22,7 @@ import type { import type { Phase5Blocked, Phase5BlockedPlatforms, Phase5SocialInteract } from "./phase/phase-5"; import type { Phase6RemoteItem, Phase6TxtEntry } from "./phase/phase-6"; import type { Phase7Chat, Phase7Publisher } from "./phase/phase-7"; +import type { Phase8Follow } from "./phase/phase-8"; import { PhasePendingPodcastId, PhasePendingSocial, @@ -199,6 +200,10 @@ export interface FeedObject extends BasicFeed { podcastImages?: Phase4PodcastImage[]; podcastRecommendations?: PhasePendingPodcastRecommendation[]; // #endregion + // #region Phase 8 + /** URL pointing to a JSON file containing follow links for the podcast */ + podcastFollow?: Phase8Follow; + // #endregion } export type Enclosure = { From 85cd4c263436fd69844e278bdd3b4b9b505cb9af Mon Sep 17 00:00:00 2001 From: Nathan Gathright Date: Sat, 6 Sep 2025 15:15:15 -0500 Subject: [PATCH 3/4] test: update phase-8 test to use Radiotopia domain instead of Podnews - Replace podnews.net URL with radiotopia.fm in test - Aligns with the Radiotopia feed reference - All tests still pass (7/7) --- src/parser/phase/__test__/phase-8.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parser/phase/__test__/phase-8.test.ts b/src/parser/phase/__test__/phase-8.test.ts index 6e3420b..1401170 100644 --- a/src/parser/phase/__test__/phase-8.test.ts +++ b/src/parser/phase/__test__/phase-8.test.ts @@ -48,12 +48,12 @@ describe("phase 8", () => { it("extracts follow url with different domain", () => { const xml = helpers.spliceFeed( feed, - `` + `` ); const result = helpers.parseValidFeed(xml); expect(result).toHaveProperty("podcastFollow"); - expect(result.podcastFollow).toHaveProperty("url", "https://podnews.net/followlinks.json"); + expect(result.podcastFollow).toHaveProperty("url", "https://radiotopia.fm/followlinks.json"); expect(helpers.getPhaseSupport(result, phase)).toContain(supportedName); }); From 055871537c54ea6424ba3e4a7c772a1152fe4d06 Mon Sep 17 00:00:00 2001 From: Nathan Gathright Date: Sat, 6 Sep 2025 16:35:39 -0500 Subject: [PATCH 4/4] Update phase-8.test.ts --- src/parser/phase/__test__/phase-8.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parser/phase/__test__/phase-8.test.ts b/src/parser/phase/__test__/phase-8.test.ts index 1401170..28b17f4 100644 --- a/src/parser/phase/__test__/phase-8.test.ts +++ b/src/parser/phase/__test__/phase-8.test.ts @@ -48,12 +48,12 @@ describe("phase 8", () => { it("extracts follow url with different domain", () => { const xml = helpers.spliceFeed( feed, - `` + `` ); const result = helpers.parseValidFeed(xml); expect(result).toHaveProperty("podcastFollow"); - expect(result.podcastFollow).toHaveProperty("url", "https://radiotopia.fm/followlinks.json"); + expect(result.podcastFollow).toHaveProperty("url", "https://f.prxu.org/72/subscribelinks.json"); expect(helpers.getPhaseSupport(result, phase)).toContain(supportedName); });