Skip to content
Merged
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
97 changes: 85 additions & 12 deletions lib/nrk/nrk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("");
Expand Down Expand Up @@ -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}"`,
);
});
75 changes: 58 additions & 17 deletions lib/nrk/nrk.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
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"];
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 };
Expand All @@ -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,
Expand Down Expand Up @@ -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 },
Expand All @@ -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) {
Expand All @@ -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,
Expand All @@ -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;
}

Expand Down