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..28b17f4
--- /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://f.prxu.org/72/subscribelinks.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 bd05e09..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,6 +93,8 @@ const feeds: FeedUpdate[] = [
phase7.podcastChat,
phase7.podcastPublisher,
+ phase8.podcastFollow,
+
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..0e017d3
--- /dev/null
+++ b/src/parser/phase/phase-8.ts
@@ -0,0 +1,22 @@
+import { firstIfArray, getAttribute, getKnownAttribute } from "../shared";
+import type { XmlNode } from "../types";
+
+export type Phase8Follow = {
+ url: string;
+};
+
+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 = {