Compare commits

..

1 Commits

Author SHA1 Message Date
Olav Sundfør aab292c353 Workaround for broken cache expiry handling
Deno's cache API does not respect expiry dates.
This meant that feeds were cached much longer than intended.

This PR implments a manual cache handling instead of relying on
Deno's implementation. Relevant issue: https://github.com/denoland/deno/issues/25795
2025-10-01 09:49:11 +02:00
2 changed files with 29 additions and 143 deletions

View File

@ -2,15 +2,12 @@ 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("");
@ -43,83 +40,13 @@ 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",
);
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");
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("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}"`,
);
Deno.test("Verify getting episodeId 'null' for 'trygdekontoret' yields `null`", async () => {
const result = await nrkRadio.getEpisode("trygdekontoret", "null");
assertEquals(result, null);
});

View File

@ -1,8 +1,8 @@
import { get, STATUS_CODE } from "https://deno.land/x/kall@v2.0.0/mod.ts";
import { Series } from "../storage.ts";
import { components as searchComponents } from "./nrk-search.ts";
import { components as catalogComponents } from "./nrk-catalog.ts";
import { external as playbackComponents } from "./nrk-playback.ts";
import { components as searchComponents } from "./nrk-search.ts";
import { Series } from "../storage.ts";
type ArrayElement<A> = A extends readonly (infer T)[] ? T : never;
type PodcastEpisodes = catalogComponents["schemas"]["EpisodesHalResource"];
@ -10,7 +10,6 @@ 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 };
@ -19,15 +18,11 @@ 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,
@ -67,41 +62,6 @@ 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 },
@ -110,7 +70,9 @@ 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) {
@ -121,17 +83,19 @@ 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 extractEpisodes(serieResponse, episodeResponse);
const episodes = await Promise.all(
episodeResponse._embedded.episodes.map((episode) => getEpisodeWithDownloadLink(episode, serieResponse.type)),
);
const seriesData = {
...serieResponse.series,
episodes,
@ -153,18 +117,13 @@ 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;
}