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 { nrkRadio } from "./nrk.ts";
import { forTestingOnly } from "./nrk.ts"; import { forTestingOnly } from "./nrk.ts";
Deno.test( Deno.test("Verify search query `trygd` returns one result: 'Trygdekontoret'", async () => {
"Verify search query `trygd` returns one result: 'Trygdekontoret'", const result = await nrkRadio.search("trygd");
async () => { assertExists(result);
const result = await nrkRadio.search("trygd"); assertEquals(result.length, 1);
assertExists(result); assertEquals(result[0].seriesId, "trygdekontoret");
assertEquals(result.length, 1); });
assertEquals(result[0].seriesId, "trygdekontoret");
},
);
Deno.test("Verify empty search query yields `null`", async () => { Deno.test("Verify empty search query yields `null`", async () => {
const result = await nrkRadio.search(""); const result = await nrkRadio.search("");
@ -43,83 +40,13 @@ Deno.test("Verify getting series data for 'trygd' does not works", async () => {
assertEquals(result, null); assertEquals(result, null);
}); });
Deno.test( Deno.test("Verify getting episodeId 'l_0bc5e55a-46b5-48a5-85e5-5a46b5d8a562' for 'trygdekontoret' works", async () => {
"Verify getting episodeId 'l_0bc5e55a-46b5-48a5-85e5-5a46b5d8a562' for 'trygdekontoret' works", const result = await nrkRadio.getEpisode("trygdekontoret", "l_0bc5e55a-46b5-48a5-85e5-5a46b5d8a562");
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); assertExists(result);
assertEquals(result.duration.seconds, 4152);
}); });
Deno.test("Seriesnakk, an umbrella series yields all seasons", async () => { Deno.test("Verify getting episodeId 'null' for 'trygdekontoret' yields `null`", async () => {
const seriesData = await forTestingOnly.getSeriesData("seriesnakk"); const result = await nrkRadio.getEpisode("trygdekontoret", "null");
assertExists(seriesData); assertEquals(result, null);
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 { 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 { components as catalogComponents } from "./nrk-catalog.ts";
import { external as playbackComponents } from "./nrk-playback.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 ArrayElement<A> = A extends readonly (infer T)[] ? T : never;
type PodcastEpisodes = catalogComponents["schemas"]["EpisodesHalResource"]; type PodcastEpisodes = catalogComponents["schemas"]["EpisodesHalResource"];
@ -10,7 +10,6 @@ type Podcast = catalogComponents["schemas"]["SeriesHalResource"];
type PodcastEpisodesSingle = catalogComponents["schemas"]["EpisodeHalResource"]; type PodcastEpisodesSingle = catalogComponents["schemas"]["EpisodeHalResource"];
type RadioSeriesEpisode = catalogComponents["schemas"]["EpisodesHalResource"]; type RadioSeriesEpisode = catalogComponents["schemas"]["EpisodesHalResource"];
type RadioSeries = catalogComponents["schemas"]["SeriesHalResource"]; type RadioSeries = catalogComponents["schemas"]["SeriesHalResource"];
type SeasonEpisodes = catalogComponents["schemas"]["PodcastSeasonHalResource"];
export type NrkSerie = catalogComponents["schemas"]["SeriesViewModel"]; export type NrkSerie = catalogComponents["schemas"]["SeriesViewModel"];
export type NrkOriginalEpisode = PodcastEpisodesSingle & { url: string }; export type NrkOriginalEpisode = PodcastEpisodesSingle & { url: string };
@ -19,15 +18,11 @@ export type NrkSearchResultList = searchComponents["schemas"]["seriesResult"]["r
export type SearchResult = ArrayElement<NrkSearchResultList> & { export type SearchResult = ArrayElement<NrkSearchResultList> & {
description?: string; description?: string;
}; };
export type SeriesData = export type SeriesData = { episodes: NrkOriginalEpisode[] } & (RadioSeries["series"] | Podcast["series"]);
& { episodes: NrkOriginalEpisode[] }
& (
| RadioSeries["series"]
| Podcast["series"]
);
function parseSeries(nrkSeriesData: SeriesData): Series { function parseSeries(nrkSeriesData: SeriesData): Series {
const imageUrl = nrkSeriesData.squareImage?.at(-1)?.url ?? ""; const imageUrl = nrkSeriesData.squareImage?.at(-1)?.url ?? "";
return { return {
id: nrkSeriesData.id, id: nrkSeriesData.id,
title: nrkSeriesData.titles.title, title: nrkSeriesData.titles.title,
@ -67,41 +62,6 @@ async function search(query: string): Promise<NrkSearchResultList | null> {
return 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> { async function getSeriesData(seriesId: string): Promise<SeriesData | null> {
let [ let [
{ status: episodeStatus, body: episodeResponse }, { status: episodeStatus, body: episodeResponse },
@ -110,7 +70,9 @@ async function getSeriesData(seriesId: string): Promise<SeriesData | null> {
get<PodcastEpisodes>( get<PodcastEpisodes>(
`${nrkAPI}/radio/catalog/podcast/${seriesId}/episodes`, `${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) { if (episodeStatus !== STATUS_CODE.OK || seriesStatus !== STATUS_CODE.OK) {
@ -121,17 +83,19 @@ async function getSeriesData(seriesId: string): Promise<SeriesData | null> {
get<RadioSeriesEpisode>( get<RadioSeriesEpisode>(
`https://psapi.nrk.no/radio/catalog/series/${seriesId}/episodes`, `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 ( if (
episodeStatus === STATUS_CODE.OK && episodeStatus === STATUS_CODE.OK && seriesStatus === STATUS_CODE.OK &&
seriesStatus === STATUS_CODE.OK && serieResponse?.series && episodeResponse?._embedded.episodes?.length
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 = { const seriesData = {
...serieResponse.series, ...serieResponse.series,
episodes, episodes,
@ -153,18 +117,13 @@ async function getSeries(seriesId: string): Promise<Series | null> {
return parsedSeries; return parsedSeries;
} }
async function getEpisode( async function getEpisode(seriesId: string, episodeId: string): Promise<NrkPodcastEpisode | null> {
seriesId: string,
episodeId: string,
): Promise<NrkPodcastEpisode | null> {
const url = `${nrkAPI}/radio/catalog/podcast/${seriesId}/episodes/${episodeId}`; const url = `${nrkAPI}/radio/catalog/podcast/${seriesId}/episodes/${episodeId}`;
const { status, body: episode } = await get<NrkPodcastEpisode>(url); const { status, body: episode } = await get<NrkPodcastEpisode>(url);
if (status === STATUS_CODE.OK && episode) { if (status === STATUS_CODE.OK && episode) {
return episode; return episode;
} }
console.error( console.error(`Error getting episode ${episodeId}. Status: ${status}. Series: ${seriesId}`);
`Error getting episode ${episodeId}. Status: ${status}. Series: ${seriesId}`,
);
return null; return null;
} }