Support for seasons (#48)

Closes https://github.com/olaven/nrss/issues/14.
This commit is contained in:
Olav Sundfør 2025-10-06 14:11:32 +02:00 committed by GitHub
parent b2c8662861
commit a29575e54e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 143 additions and 29 deletions

View File

@ -2,12 +2,15 @@ import { assertEquals, assertExists, assertGreaterOrEqual } from "$std/assert/mo
import { nrkRadio } from "./nrk.ts";
import { forTestingOnly } from "./nrk.ts";
Deno.test("Verify search query `trygd` returns one result: 'Trygdekontoret'", async () => {
const result = await nrkRadio.search("trygd");
assertExists(result);
assertEquals(result.length, 1);
assertEquals(result[0].seriesId, "trygdekontoret");
});
Deno.test(
"Verify search query `trygd` returns one result: 'Trygdekontoret'",
async () => {
const result = await nrkRadio.search("trygd");
assertExists(result);
assertEquals(result.length, 1);
assertEquals(result[0].seriesId, "trygdekontoret");
},
);
Deno.test("Verify empty search query yields `null`", async () => {
const result = await nrkRadio.search("");
@ -40,13 +43,83 @@ Deno.test("Verify getting series data for 'trygd' does not works", async () => {
assertEquals(result, null);
});
Deno.test("Verify getting episodeId 'l_0bc5e55a-46b5-48a5-85e5-5a46b5d8a562' for 'trygdekontoret' works", async () => {
const result = await nrkRadio.getEpisode("trygdekontoret", "l_0bc5e55a-46b5-48a5-85e5-5a46b5d8a562");
Deno.test(
"Verify getting episodeId 'l_0bc5e55a-46b5-48a5-85e5-5a46b5d8a562' for 'trygdekontoret' works",
async () => {
const result = await nrkRadio.getEpisode(
"trygdekontoret",
"l_0bc5e55a-46b5-48a5-85e5-5a46b5d8a562",
);
assertExists(result);
assertEquals(result.duration.seconds, 4152);
},
);
Deno.test(
"Verify getting episodeId 'null' for 'trygdekontoret' yields `null`",
async () => {
const result = await nrkRadio.getEpisode("trygdekontoret", "null");
assertEquals(result, null);
},
);
Deno.test("Can get episodes for seriesnakk", async () => {
const result = await nrkRadio.getSeries("seriesnakk");
assertExists(result);
assertEquals(result.duration.seconds, 4152);
});
Deno.test("Verify getting episodeId 'null' for 'trygdekontoret' yields `null`", async () => {
const result = await nrkRadio.getEpisode("trygdekontoret", "null");
assertEquals(result, null);
Deno.test("Seriesnakk, an umbrella series yields all seasons", async () => {
const seriesData = await forTestingOnly.getSeriesData("seriesnakk");
assertExists(seriesData);
assertGreaterOrEqual(seriesData.episodes.length, 1);
const titles = seriesData.episodes.map((e) => e.titles.title);
const thereseTitle = "Therese-saken: Hva kan et vitne huske? (1:5)";
assertEquals(
titles.includes(thereseTitle),
true,
`Titles include "${thereseTitle}"`,
);
const maktaTitle = " Vi er ikke idioter og tror at det var elsparkesykler i 1975";
assertEquals(
titles.includes(maktaTitle),
true,
`Titles include "${maktaTitle}"`,
);
const soLongMarianneTitle = "- Se og hør sov på dørmatta hennes! (1:8)";
assertEquals(
titles.includes(soLongMarianneTitle),
true,
`Titles include "${soLongMarianneTitle}"`,
);
const nerdrumTitle = "Seriesnakk: Familien Nerdrum";
assertEquals(
titles.includes(nerdrumTitle),
true,
`Titles include "${nerdrumTitle}"`,
);
const selinaTitle = " Som å se meg selv (1:6)";
assertEquals(
titles.includes(selinaTitle),
true,
`Titles include "${selinaTitle}"`,
);
const skamTitle = " William må svare (4:9)";
assertEquals(
titles.includes(skamTitle),
true,
`Titles include "${skamTitle}"`,
);
const natoTitle = " Jeg ville slite ut Jens (1:2)";
assertEquals(
titles.includes(natoTitle),
true,
`Titles include "${natoTitle}"`,
);
});

View File

@ -1,8 +1,8 @@
import { get, STATUS_CODE } from "https://deno.land/x/kall@v2.0.0/mod.ts";
import { components as searchComponents } from "./nrk-search.ts";
import { Series } from "../storage.ts";
import { components as catalogComponents } from "./nrk-catalog.ts";
import { external as playbackComponents } from "./nrk-playback.ts";
import { Series } from "../storage.ts";
import { components as searchComponents } from "./nrk-search.ts";
type ArrayElement<A> = A extends readonly (infer T)[] ? T : never;
type PodcastEpisodes = catalogComponents["schemas"]["EpisodesHalResource"];
@ -10,6 +10,7 @@ type Podcast = catalogComponents["schemas"]["SeriesHalResource"];
type PodcastEpisodesSingle = catalogComponents["schemas"]["EpisodeHalResource"];
type RadioSeriesEpisode = catalogComponents["schemas"]["EpisodesHalResource"];
type RadioSeries = catalogComponents["schemas"]["SeriesHalResource"];
type SeasonEpisodes = catalogComponents["schemas"]["PodcastSeasonHalResource"];
export type NrkSerie = catalogComponents["schemas"]["SeriesViewModel"];
export type NrkOriginalEpisode = PodcastEpisodesSingle & { url: string };
@ -18,11 +19,15 @@ export type NrkSearchResultList = searchComponents["schemas"]["seriesResult"]["r
export type SearchResult = ArrayElement<NrkSearchResultList> & {
description?: string;
};
export type SeriesData = { episodes: NrkOriginalEpisode[] } & (RadioSeries["series"] | Podcast["series"]);
export type SeriesData =
& { episodes: NrkOriginalEpisode[] }
& (
| RadioSeries["series"]
| Podcast["series"]
);
function parseSeries(nrkSeriesData: SeriesData): Series {
const imageUrl = nrkSeriesData.squareImage?.at(-1)?.url ?? "";
return {
id: nrkSeriesData.id,
title: nrkSeriesData.titles.title,
@ -62,6 +67,41 @@ async function search(query: string): Promise<NrkSearchResultList | null> {
return null;
}
async function extractEpisodes(
serieResponse: RadioSeries,
episodeResponse?: PodcastEpisodes,
): Promise<NrkOriginalEpisode[]> {
if (serieResponse.seriesType === "umbrella") {
const seasons = await Promise.all(
serieResponse._links.seasons.map(async (season) => {
const response = await get<SeasonEpisodes>(
`https://psapi.nrk.no${season.href}`,
);
return response.body;
}),
);
const episodes = await Promise.all(
seasons.flatMap((season) => {
return (
season?._embedded.episodes?._embedded.episodes?.flatMap((episode) =>
getEpisodeWithDownloadLink(episode, serieResponse.type)
) ?? []
);
}),
);
return episodes;
} else {
const episodes = await Promise.all(
episodeResponse?._embedded.episodes?.map((episode) => getEpisodeWithDownloadLink(episode, serieResponse.type)) ??
[],
);
return episodes;
}
}
async function getSeriesData(seriesId: string): Promise<SeriesData | null> {
let [
{ status: episodeStatus, body: episodeResponse },
@ -70,9 +110,7 @@ async function getSeriesData(seriesId: string): Promise<SeriesData | null> {
get<PodcastEpisodes>(
`${nrkAPI}/radio/catalog/podcast/${seriesId}/episodes`,
),
get<Podcast>(
`${nrkAPI}/radio/catalog/podcast/${seriesId}`,
),
get<Podcast>(`${nrkAPI}/radio/catalog/podcast/${seriesId}`),
]);
if (episodeStatus !== STATUS_CODE.OK || seriesStatus !== STATUS_CODE.OK) {
@ -83,19 +121,17 @@ async function getSeriesData(seriesId: string): Promise<SeriesData | null> {
get<RadioSeriesEpisode>(
`https://psapi.nrk.no/radio/catalog/series/${seriesId}/episodes`,
),
get<RadioSeries>(
`https://psapi.nrk.no/radio/catalog/series/${seriesId}`,
),
get<RadioSeries>(`https://psapi.nrk.no/radio/catalog/series/${seriesId}`),
]);
}
if (
episodeStatus === STATUS_CODE.OK && seriesStatus === STATUS_CODE.OK &&
serieResponse?.series && episodeResponse?._embedded.episodes?.length
episodeStatus === STATUS_CODE.OK &&
seriesStatus === STATUS_CODE.OK &&
serieResponse?.series &&
episodeResponse?._embedded.episodes?.length
) {
const episodes = await Promise.all(
episodeResponse._embedded.episodes.map((episode) => getEpisodeWithDownloadLink(episode, serieResponse.type)),
);
const episodes = await extractEpisodes(serieResponse, episodeResponse);
const seriesData = {
...serieResponse.series,
episodes,
@ -117,13 +153,18 @@ async function getSeries(seriesId: string): Promise<Series | null> {
return parsedSeries;
}
async function getEpisode(seriesId: string, episodeId: string): Promise<NrkPodcastEpisode | null> {
async function getEpisode(
seriesId: string,
episodeId: string,
): Promise<NrkPodcastEpisode | null> {
const url = `${nrkAPI}/radio/catalog/podcast/${seriesId}/episodes/${episodeId}`;
const { status, body: episode } = await get<NrkPodcastEpisode>(url);
if (status === STATUS_CODE.OK && episode) {
return episode;
}
console.error(`Error getting episode ${episodeId}. Status: ${status}. Series: ${seriesId}`);
console.error(
`Error getting episode ${episodeId}. Status: ${status}. Series: ${seriesId}`,
);
return null;
}