Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions src/parser/phase/__test__/phase-8.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/* 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:image", () => {
const supportedName = "image";

it("skips missing tag", () => {
const result = helpers.parseValidFeed(feed);

expect(result).not.toHaveProperty("podcastImage");
expect(result.items[0]).not.toHaveProperty("podcastImage");
expect(helpers.getPhaseSupport(result, phase)).not.toContain(supportedName);
});

it("ignores missing href", () => {
const xml = helpers.spliceFeed(
feed,
`<podcast:image type="image/jpeg" width="1400" height="1400"/>`
);
const result = helpers.parseValidFeed(xml);

expect(result).not.toHaveProperty("podcastImage");
expect(helpers.getPhaseSupport(result, phase)).not.toContain(supportedName);
});

it("extracts multiple channel images", () => {
const xml = helpers.spliceFeed(
feed,
`<podcast:image href="https://example.com/square.jpg" alt="Square art" aspect-ratio="1/1" type="image/jpeg" width="1400" height="1400" purpose="artwork"/>
<podcast:image href="https://example.com/banner.jpg" aspect-ratio="16/9" width="1920" purpose="artwork social"/>`
);
const result = helpers.parseValidFeed(xml);

expect(result).toHaveProperty("podcastImage");
expect(result.podcastImage).toHaveLength(2);
expect(result.podcastImage?.[0]).toEqual({
href: "https://example.com/square.jpg",
alt: "Square art",
aspectRatio: "1/1",
type: "image/jpeg",
width: 1400,
height: 1400,
purpose: "artwork",
});
expect(result.podcastImage?.[1]).toEqual({
href: "https://example.com/banner.jpg",
aspectRatio: "16/9",
width: 1920,
purpose: "artwork social",
});
expect(helpers.getPhaseSupport(result, phase)).toContain(supportedName);
});

it("extracts item images", () => {
const xml = helpers.spliceFirstItem(
feed,
`<podcast:image href="https://example.com/episode.mp4" type="video/mp4" aspect-ratio="9/16" width="1200" purpose="canvas"/>`
);
const result = helpers.parseValidFeed(xml);

expect(result.items[0]).toHaveProperty("podcastImage");
expect(result.items[0].podcastImage).toHaveLength(1);
expect(result.items[0].podcastImage?.[0]).toEqual({
href: "https://example.com/episode.mp4",
type: "video/mp4",
aspectRatio: "9/16",
width: 1200,
purpose: "canvas",
});
expect(result.items[1]).not.toHaveProperty("podcastImage");
expect(helpers.getPhaseSupport(result, phase)).toContain(supportedName);
});

it("filters item images without href while keeping valid images", () => {
const xml = helpers.spliceFirstItem(
feed,
`<podcast:image type="image/jpeg" width="1400" height="1400"/>
<podcast:image href="https://example.com/item-valid.jpg" type="image/jpeg" width="1400" height="1400"/>`
);
const result = helpers.parseValidFeed(xml);

expect(result.items[0]).toHaveProperty("podcastImage");
expect(result.items[0].podcastImage).toEqual([
{
href: "https://example.com/item-valid.jpg",
type: "image/jpeg",
width: 1400,
height: 1400,
},
]);
expect(helpers.getPhaseSupport(result, phase)).toContain(supportedName);
});

it("extracts liveItem images", () => {
const xml = helpers.spliceFeed(
feed,
`<podcast:liveItem status="live" start="2021-09-26T07:30:00.000-0600" end="2021-09-26T08:30:00.000-0600">
<title>Live Episode</title>
<guid>live-episode</guid>
<enclosure url="https://example.com/live.mp3" length="1234" type="audio/mpeg"/>
<podcast:image href="https://example.com/live.jpg" alt="Live art" aspect-ratio="16/9"/>
</podcast:liveItem>`
);
const result = helpers.parseValidFeed(xml);

expect(result.podcastLiveItems).toHaveLength(1);
expect(result.podcastLiveItems?.[0]).toHaveProperty("podcastImage");
expect(result.podcastLiveItems?.[0].podcastImage).toEqual([
{
href: "https://example.com/live.jpg",
alt: "Live art",
aspectRatio: "16/9",
},
]);
});
});
});
5 changes: 5 additions & 0 deletions src/parser/phase/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,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";

Expand Down Expand Up @@ -99,6 +100,8 @@ const feeds: FeedUpdate[] = [
phase7.podcastChat,
phase7.podcastPublisher,

phase8.podcastImage,

pending.metaBoost,
pending.id,
pending.social,
Expand Down Expand Up @@ -128,6 +131,8 @@ const items: ItemUpdate[] = [

phase7.podcastChat,

phase8.podcastImage,

pending.podcastRecommendations,
pending.podcastGateway,
];
Expand Down
1 change: 1 addition & 0 deletions src/parser/phase/phase-4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ export type Phase4PodcastLiveItemItem = Pick<Episode, "title" | "guid" | "enclos
| "podcastPeople"
| "alternativeEnclosures"
| "podcastImages"
| "podcastImage"
| "values"
>
> & {
Expand Down
49 changes: 49 additions & 0 deletions src/parser/phase/phase-8.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {
ensureArray,
extractOptionalIntegerAttribute,
extractOptionalStringAttribute,
getAttribute,
getKnownAttribute,
} from "../shared";
import type { XmlNode } from "../types";

import { addSubTag } from "./helpers";

export type Phase8PodcastImage = {
href: string;
alt?: string;
aspectRatio?: string;
width?: number;
height?: number;
type?: string;
purpose?: string;
};

function parseImage(node: XmlNode): Phase8PodcastImage {
return {
href: getKnownAttribute(node, "href"),
...extractOptionalStringAttribute(node, "alt"),
...extractOptionalStringAttribute(node, "aspect-ratio", "aspectRatio"),
...extractOptionalIntegerAttribute(node, "width"),
...extractOptionalIntegerAttribute(node, "height"),
...extractOptionalStringAttribute(node, "type"),
...extractOptionalStringAttribute(node, "purpose"),
};
}

export const podcastImage = {
phase: 8,
name: "image",
tag: "podcast:image",
nodeTransform: (node: XmlNode | XmlNode[]): XmlNode[] =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
ensureArray(node).filter((n) => getAttribute(n, "href")),
supportCheck: (node: XmlNode[]): boolean => node.length > 0,
fn(node: XmlNode[]): { podcastImage: Phase8PodcastImage[] } {
return {
podcastImage: node.map(parseImage),
};
},
};

addSubTag("liveItem", podcastImage);
5 changes: 5 additions & 0 deletions src/parser/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { Phase8PodcastImage } from "./phase/phase-8";
import {
PhasePendingPodcastId,
PhasePendingSocial,
Expand Down Expand Up @@ -203,6 +204,8 @@ export interface FeedObject extends BasicFeed {
/** PENDING AND LIKELY TO CHANGE This tag tells the an application what the content contained within the feed IS, as opposed to what the content is ABOUT in the case of a category. */
medium?: Phase4Medium;
podcastImages?: Phase4PodcastImage[];
/** Phase 8: media assets for channel, item, or liveItem presentation. */
podcastImage?: Phase8PodcastImage[];
podcastRecommendations?: PhasePendingPodcastRecommendation[];
// #endregion
}
Expand Down Expand Up @@ -273,6 +276,8 @@ export interface Episode {

// #region Pending Phase
podcastImages?: Phase4PodcastImage[];
/** Phase 8: media assets for channel, item, or liveItem presentation. */
podcastImage?: Phase8PodcastImage[];
podcastRecommendations?: PhasePendingPodcastRecommendation[];
podcastGateway?: PhasePendingGateway;
// #endregion
Expand Down