feat: Add unit-tests (#25)

This commit is contained in:
Tim Hårek Andreassen 2024-04-11 21:10:16 +02:00 committed by GitHub
parent 0c2baf356b
commit 33dbdfefcc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 384 additions and 173 deletions

18
.github/workflows/test.yaml vendored Normal file
View File

@ -0,0 +1,18 @@
name: Test
on:
push:
jobs:
run-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Setup deno
uses: denoland/setup-deno@v1
with:
deno-version: v1.x
- name: Run checks
run: deno task check
- name: Run test
run: deno task test

View File

@ -1,4 +1,4 @@
export default function Search(props: { defaultValue: string }) {
export default function Search(props: { defaultValue: string | null }) {
return (
<form className="flex flex-col w-full lg:w-2/3 mx-auto my-4">
<label className="sr-only" htmlFor="query">Program</label>
@ -8,7 +8,7 @@ export default function Search(props: { defaultValue: string }) {
className="border-2 rounded-md px-2"
name="query"
id="query"
defaultValue={props.defaultValue}
defaultValue={props.defaultValue ?? ""}
/>
<button type="submit" className="my-2 bg-gray-100 border-2 border-gray-400 rounded-md">
Søk

View File

@ -6,7 +6,8 @@
"tasks": {
"start": "deno run -A --watch=static/,routes/ dev.ts",
"check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
"types:nrk": "deno run -A generate-types.ts && deno fmt ./lib"
"types:nrk": "deno run -A generate-types.ts && deno fmt ./lib",
"test": "deno test -A"
},
"unstable": [
"kv"
@ -20,9 +21,8 @@
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1",
"$gfm": "https://deno.land/x/gfm@0.6.0/mod.ts",
"openapi-typescript": "npm:openapi-typescript@6.7.5",
"datetime": "https://deno.land/std@0.221.0/datetime/mod.ts",
"asserts": "https://deno.land/std@0.221.0/assert/mod.ts",
"serialize-xml": "https://raw.githubusercontent.com/olaven/serialize-xml/v0.4.0/mod.ts"
"serialize-xml": "https://raw.githubusercontent.com/olaven/serialize-xml/v0.4.0/mod.ts",
"$std/": "https://deno.land/std@0.221.0/"
},
"compilerOptions": {
"jsx": "react-jsx",

View File

@ -2,15 +2,15 @@ import openapiTS from "openapi-typescript";
const nrkTypes: { filename: string; url: URL }[] = [
{
filename: "./lib/nrk-search.ts",
filename: "./lib/nrk/nrk-search.ts",
url: new URL("https://psapi.nrk.no/documentation/openapi/search-radio/v1/openapi.yml"),
},
{
filename: "./lib/nrk-catalog.ts",
filename: "./lib/nrk/nrk-catalog.ts",
url: new URL("https://psapi.nrk.no/documentation/openapi/programsider-radio/openapi.yml"),
},
{
filename: "./lib/nrk-playback.ts",
filename: "./lib/nrk/nrk-playback.ts",
url: new URL("https://psapi.nrk.no/documentation/openapi/playback/2.0/openapi.json"),
},
] as const;

51
lib/caching.test.ts Normal file
View File

@ -0,0 +1,51 @@
import { assertEquals } from "$std/assert/mod.ts";
import { forTestingOnly } from "./caching.ts";
import { testUtils } from "./test-utils.ts";
Deno.test("Verify getTimeSinceLastFetch for correct difference", () => {
const earlyDate = new Date("2024-04-07T20:24:32Z");
const date = new Date("2024-04-07T23:24:32Z");
const timeSinceLastFetch = forTestingOnly.getTimeSinceLastFetch(date, earlyDate);
assertEquals(timeSinceLastFetch, 3);
});
Deno.test("Verify getTimeSinceLastFetch for bad date", () => {
const earlyDate = new Date("2024-04-07T20:24:32Z");
const date = new Date("bad date");
const timeSinceLastFetch = forTestingOnly.getTimeSinceLastFetch(date, earlyDate);
assertEquals(timeSinceLastFetch, NaN);
});
Deno.test("Verify getTimeSinceLastFetch for bad dates (plural)", () => {
const earlyDate = new Date("badest date");
const date = new Date("bad date");
const timeSinceLastFetch = forTestingOnly.getTimeSinceLastFetch(date, earlyDate);
assertEquals(timeSinceLastFetch, NaN);
});
Deno.test("Verify seriesFromStorage is old", () => {
const earlyDate = new Date("2024-04-07T21:24:32Z");
const date = new Date("2024-04-07T23:24:32Z");
const timeSinceLastFetch = forTestingOnly.getTimeSinceLastFetch(date, earlyDate);
const series = testUtils.generateSeries({ lastFetchedAt: date });
const syncInterval = 2;
const isNew = forTestingOnly.isSeriesFromStorageNew(series, syncInterval, timeSinceLastFetch);
assertEquals(isNew, true);
});
Deno.test("Verify seriesFromStorage is new", () => {
const earlyDate = new Date("2024-04-07T20:24:32Z");
const date = new Date("2024-04-07T23:24:32Z");
const timeSinceLastFetch = forTestingOnly.getTimeSinceLastFetch(date, earlyDate);
const series = testUtils.generateSeries({ lastFetchedAt: date });
const syncInterval = 2;
const isNew = forTestingOnly.isSeriesFromStorageNew(series, syncInterval, timeSinceLastFetch);
assertEquals(isNew, false);
});

View File

@ -1,21 +1,33 @@
import { nrkRadio } from "./nrk/nrk.ts";
import { Series, storage } from "./storage.ts";
import * as datetime from "datetime";
import * as datetime from "$std/datetime/mod.ts";
import { Episode } from "./storage.ts";
const SYNC_INTERVAL_HOURS = 1;
async function initialFetch(options: { id: string }) {
async function initialFetch(options: { id: string }): Promise<Series | null> {
const series = await nrkRadio.getSeries(options.id);
if (!series) {
return null;
}
const stored = storage.write(series);
if (!stored) {
throw new Error(`Failed to store series ${options.id}`);
console.error(`Failed to store series ${options.id}`);
return null;
}
return series;
}
async function updateFetch(existingSeries: Series) {
type UpdatedSeries = {
lastFetch: Date;
episodes: Episode[];
} & Series;
async function updateFetch(existingSeries: Series): Promise<UpdatedSeries | Series | null> {
const series = await nrkRadio.getSeries(existingSeries.id);
if (!series) {
return null;
}
const newEpisodes = series.episodes.filter((episode) => {
return !existingSeries.episodes.find((serieEpisode) => serieEpisode.id === episode.id);
});
@ -24,27 +36,46 @@ async function updateFetch(existingSeries: Series) {
return existingSeries;
}
/**
* Since we don't control the API,
* we should not make assumptions about the order,
* but rather sort the episode to what we want.
*/
const episodesSortedDescending = [...newEpisodes, ...existingSeries.episodes]
.sort((a, b) => a.date.getTime() > b.date.getTime() ? -1 : 1);
const updated = {
...existingSeries,
lastFetch: new Date(),
/**
* Since we don't control the API,
* we should not make assumptions about the order,
* but rather sort the episode to what we want.
*/
episodes: [...newEpisodes, ...existingSeries.episodes]
.sort((a, b) => a.date.getTime() > b.date.getTime() ? -1 : 1),
episodes: episodesSortedDescending,
};
const updateSuccessful = await storage.write(updated);
if (!updateSuccessful) {
throw new Error(`Failed to update series ${existingSeries.id}`);
console.log(`Failed to update series ${existingSeries.id}`);
return null;
}
return updated;
}
async function getSeries(options: { id: string }): Promise<Series> {
function getTimeSinceLastFetch(inputDate: Date, againstDate = new Date()): number | null {
return datetime.difference(inputDate, againstDate, { units: ["hours"] }).hours ?? null;
}
function isSeriesFromStorageNew(
seriesFromStorage: Series,
syncInterval = SYNC_INTERVAL_HOURS,
timeSinceLastFetch = getTimeSinceLastFetch(seriesFromStorage.lastFetchedAt),
) {
return !Number.isNaN(timeSinceLastFetch) &&
seriesFromStorage !== null &&
timeSinceLastFetch !== null &&
timeSinceLastFetch !== undefined &&
timeSinceLastFetch <= syncInterval;
}
async function getSeries(options: { id: string }): Promise<Series | null> {
const seriesFromStorage = await storage.read(options);
/**
@ -56,16 +87,8 @@ async function getSeries(options: { id: string }): Promise<Series> {
return await initialFetch(options);
}
const timeSinceLastFetch =
datetime.difference(seriesFromStorage.lastFetchedAt, new Date(), { units: ["hours"] }).hours;
// we have the feed in storage and it's not too old
if (
seriesFromStorage !== null &&
timeSinceLastFetch !== null &&
timeSinceLastFetch !== undefined &&
timeSinceLastFetch <= SYNC_INTERVAL_HOURS
) {
if (isSeriesFromStorageNew(seriesFromStorage)) {
return seriesFromStorage;
}
@ -80,3 +103,8 @@ async function getSeries(options: { id: string }): Promise<Series> {
export const caching = {
getSeries,
};
export const forTestingOnly = {
getTimeSinceLastFetch,
isSeriesFromStorageNew,
};

52
lib/nrk/nrk.test.ts Normal file
View File

@ -0,0 +1,52 @@
import { assertEquals, assertExists, assertGreaterOrEqual } from "$std/assert/mod.ts";
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 empty search query yields `null`", async () => {
const result = await nrkRadio.search("");
assertEquals(result, null);
});
Deno.test("Verify getting series data for 'trygdekontoret' works", async () => {
const result = await forTestingOnly.getSeriesData("trygdekontoret");
assertExists(result);
assertGreaterOrEqual(result.episodes.length, 1);
});
Deno.test("Verify getting series data for 'trygdekontoret' works", async () => {
const seriesData = await forTestingOnly.getSeriesData("trygdekontoret");
assertExists(seriesData);
const result = nrkRadio.parseSeries(seriesData);
assertExists(result);
assertGreaterOrEqual(result.episodes.length, 1);
assertEquals(result.title, "Trygdekontoret");
});
Deno.test("Verify getting series for 'trygdekontoret' works", async () => {
const result = await nrkRadio.getSeries("trygdekontoret");
assertExists(result);
assertGreaterOrEqual(result.episodes.length, 1);
});
Deno.test("Verify getting series data for 'trygd' does not works", async () => {
const result = await nrkRadio.getSeries("trygd");
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);
});

View File

@ -1,4 +1,4 @@
import { get, OK } from "https://deno.land/x/kall@v0.1.0/mod.ts";
import { get, STATUS_CODE } from "https://deno.land/x/kall@v2.0.0/mod.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";
@ -8,6 +8,8 @@ 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"];
export type NrkSerie = catalogComponents["schemas"]["SeriesViewModel"];
export type NrkOriginalEpisode = PodcastEpisodesSingle & { url: string };
@ -16,21 +18,19 @@ export type NrkSearchResultList = searchComponents["schemas"]["seriesResult"]["r
export type SearchResult = ArrayElement<NrkSearchResultList> & {
description?: string;
};
export type SeriesData = { episodes: NrkOriginalEpisode[] } & (RadioSeries["series"] | Podcast["series"]);
type Manifest = playbackComponents["schemas/playback-channel.json"]["components"]["schemas"]["PlayableManifest"];
type NrkSerieData = Awaited<ReturnType<typeof nrkRadio.getSeries>>;
function parseSeries(nrkSeries: NrkSerieData): Series {
const imageUrl = nrkSeries.squareImage?.at(-1)?.url ?? "";
function parseSeries(nrkSeriesData: SeriesData): Series {
const imageUrl = nrkSeriesData.squareImage?.at(-1)?.url ?? "";
return {
id: nrkSeries.id,
title: nrkSeries.titles.title,
subtitle: nrkSeries.titles.subtitle ?? null,
link: `https://radio.nrk.no/podkast/${nrkSeries.id}`,
id: nrkSeriesData.id,
title: nrkSeriesData.titles.title,
subtitle: nrkSeriesData.titles.subtitle ?? null,
link: `https://radio.nrk.no/podkast/${nrkSeriesData.id}`,
imageUrl: imageUrl,
lastFetchedAt: new Date(),
episodes: nrkSeries.episodes.map((episode) => {
episodes: nrkSeriesData.episodes.map((episode) => {
return {
id: episode.id,
title: episode.titles.title,
@ -46,89 +46,121 @@ function parseSeries(nrkSeries: NrkSerieData): Series {
const nrkAPI = `https://psapi.nrk.no`;
async function withDownloadLink(
async function search(query: string): Promise<NrkSearchResultList | null> {
if (query === "") {
console.error("Empty search query.");
return null;
}
const { status, body } = await get<
searchComponents["schemas"]["searchresult"]
>(`${nrkAPI}/radio/search/search?q=${query}`);
if (status === STATUS_CODE.OK && body) {
return body.results.series?.results;
}
console.error(`Something went wrong with ${query} - got status ${status}`);
return null;
}
async function getSeriesData(seriesId: string): Promise<SeriesData | null> {
let [
{ status: episodeStatus, body: episodeResponse },
{ status: seriesStatus, body: serieResponse },
] = await Promise.all([
get<PodcastEpisodes>(
`${nrkAPI}/radio/catalog/podcast/${seriesId}/episodes`,
),
get<Podcast>(
`${nrkAPI}/radio/catalog/podcast/${seriesId}`,
),
]);
if (episodeStatus !== STATUS_CODE.OK || seriesStatus !== STATUS_CODE.OK) {
[
{ status: episodeStatus, body: episodeResponse },
{ status: seriesStatus, body: serieResponse },
] = await Promise.all([
get<RadioSeriesEpisode>(
`https://psapi.nrk.no/radio/catalog/series/${seriesId}/episodes`,
),
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
) {
const episodes = await Promise.all(
episodeResponse._embedded.episodes.map((episode) => getEpisodeWithDownloadLink(episode, serieResponse.type)),
);
const seriesData = {
...serieResponse.series,
episodes,
};
return seriesData;
}
console.error(
`Error getting episodes for ${seriesId}: EpisodeStatus: ${episodeStatus}. SerieStatus: ${seriesStatus}`,
);
return null;
}
async function getSeries(seriesId: string): Promise<Series | null> {
const seriesData = await getSeriesData(seriesId);
if (!seriesData) {
return null;
}
const parsedSeries = parseSeries(seriesData);
return parsedSeries;
}
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}`);
return null;
}
type Manifest = playbackComponents["schemas/playback-channel.json"]["components"]["schemas"]["PlayableManifest"];
async function getEpisodeWithDownloadLink(
episode: PodcastEpisodesSingle,
type: catalogComponents["schemas"]["Type"],
): Promise<NrkOriginalEpisode> {
// getting stream link
let [playbackStatus, playbackResponse] = await get<Manifest>(
let { status: playbackStatus, body: playbackResponse } = await get<Manifest>(
`${nrkAPI}/playback/manifest/podcast/${episode.episodeId}`,
);
if (type === "series") {
[playbackStatus, playbackResponse] = await get<Manifest>(
const { status, body } = await get<Manifest>(
`${nrkAPI}/playback/manifest/program/${episode.episodeId}`,
);
playbackStatus = status;
playbackResponse = body;
}
if (playbackStatus === OK && playbackResponse) {
return { ...episode, url: playbackResponse.playable.assets[0].url };
} else {
throw `Error getting downloadLink for ${episode.episodeId}, serie: ${episode.originalTitle}. Status: ${playbackStatus}`;
if (playbackStatus !== STATUS_CODE.OK && !playbackResponse) {
throw new Error(
`Error getting downloadLink for ${episode.episodeId}, serie: ${episode.originalTitle}. Status: ${playbackStatus}`,
);
}
return { ...episode, url: playbackResponse.playable.assets[0].url };
}
export const nrkRadio = {
search: async (query: string): Promise<NrkSearchResultList> => {
const [status, response] = await get<
searchComponents["schemas"]["searchresult"]
>(`${nrkAPI}/radio/search/search?q=${query}`);
if (status === OK && response) {
return response.results.series?.results;
}
throw `Something went wrong with ${query} - got status ${status}`;
},
getSeries: async (seriesId: string) => {
let [
[episodeStatus, episodeResponse],
[seriesStatus, serieResponse],
] = await Promise.all([
get<PodcastEpisodes>(
`${nrkAPI}/radio/catalog/podcast/${seriesId}/episodes`,
),
get<Podcast>(
`${nrkAPI}/radio/catalog/podcast/${seriesId}`,
),
]);
if (episodeStatus !== OK || seriesStatus !== OK) {
[
[episodeStatus, episodeResponse],
[seriesStatus, serieResponse],
] = await Promise.all([
get<catalogComponents["schemas"]["EpisodesHalResource"]>(
`https://psapi.nrk.no/radio/catalog/series/${seriesId}/episodes`,
),
get<catalogComponents["schemas"]["SeriesHalResource"]>(
`https://psapi.nrk.no/radio/catalog/series/${seriesId}`,
),
]);
}
if (
episodeStatus === OK && seriesStatus === OK &&
serieResponse?.series && episodeResponse?._embedded.episodes?.length
) {
const episodes = await Promise.all(
episodeResponse._embedded.episodes.map((episode) => withDownloadLink(episode, serieResponse.type)),
);
const serieData = {
...serieResponse.series,
episodes,
};
const parsedSeries = parseSeries(serieData);
return parsedSeries;
}
throw `Error getting episodes for ${seriesId}: EpisodeStatus: ${episodeStatus}. SerieStatus: ${seriesStatus}`;
},
getEpisode: async (seriesId: string, episodeId: string): Promise<NrkPodcastEpisode | null> => {
const url = `${nrkAPI}/radio/catalog/podcast/${seriesId}/episodes/${episodeId}`;
const [status, episode] = await get<NrkPodcastEpisode>(url);
if (status === OK && episode) {
return episode;
}
throw `Error getting episode ${episodeId}. Status: ${status}. Series: ${seriesId}`;
},
search,
getSeries,
getEpisode,
parseSeries,
};
export const forTestingOnly = {
getSeriesData,
};

View File

@ -1,6 +1,14 @@
import { assertEquals, assertExists } from "asserts";
import { assertEquals, assertExists } from "$std/assert/mod.ts";
import { testUtils } from "./test-utils.ts";
import { rss } from "./rss.ts";
import { forTestingOnly } from "./rss.ts";
// NOTE: Could probably be expanded upon.
Deno.test("generate tag for episode", () => {
const episode = testUtils.generateEpisode();
const tag = forTestingOnly.assembleEpisode(episode, "someId");
assertExists(tag);
});
Deno.test("generated rss contains the series title", () => {
const series = testUtils.generateSeries();

View File

@ -1,8 +1,8 @@
import { declaration, serialize, tag } from "serialize-xml";
import { getHostName } from "../utils.ts";
import { declaration, serialize, Tag, tag } from "serialize-xml";
import { getHostName } from "./utils.ts";
import { Episode, Series } from "./storage.ts";
function assembleFeed(series: Series) {
function assembleFeed(series: Series): string {
// Originally adapted from https://raw.githubusercontent.com/olaven/paperpod/1cde9abd3174b26e126aa74fc5a3b63fd078c0fd/packages/converter/src/rss.ts
return serialize(
declaration([
@ -44,7 +44,7 @@ function assembleFeed(series: Series) {
]),
]
: []),
...series.episodes.map((episode) => assembleEpisode({ series, episode })),
...series.episodes.map((episode) => assembleEpisode(episode, series.id)),
]),
],
[
@ -57,9 +57,7 @@ function assembleFeed(series: Series) {
);
}
function assembleEpisode(options: { series: Series; episode: Episode }) {
const { series, episode } = options;
function assembleEpisode(episode: Episode, seriesId: Series["id"]): Tag {
const description = episode.subtitle || "";
return tag("item", [
@ -73,7 +71,7 @@ function assembleEpisode(options: { series: Series; episode: Episode }) {
tag("podcast:chapters", "", [
[
"url",
`${getHostName()}/api/feeds/${series.id}/${episode.id}/chapters`,
`${getHostName()}/api/feeds/${seriesId}/${episode.id}/chapters`,
],
["type", "application/json+chapters"],
]),
@ -88,3 +86,7 @@ function assembleEpisode(options: { series: Series; episode: Episode }) {
export const rss = {
assembleFeed,
};
export const forTestingOnly = {
assembleEpisode,
};

View File

@ -1,4 +1,4 @@
import { assertEquals } from "asserts";
import { assertEquals } from "$std/assert/mod.ts";
import { storage } from "./storage.ts";
import { testUtils } from "./test-utils.ts";

View File

@ -1,5 +1,5 @@
import { fakerNB_NO as faker } from "npm:@faker-js/faker";
import { Series } from "./storage.ts";
import { Episode, Series } from "./storage.ts";
function generateSeries(overrides: Partial<Series> = {}): Series {
return {
@ -10,22 +10,29 @@ function generateSeries(overrides: Partial<Series> = {}): Series {
imageUrl: faker.image.url(),
lastFetchedAt: faker.date.recent(),
episodes: new Array(faker.number.int({ min: 0, max: 100 }))
.fill(null).map(() => ({
title: faker.word.words(3),
subtitle: faker.word.words(3),
link: faker.internet.url(),
description: faker.lorem.paragraphs(2),
id: faker.string.uuid(),
date: faker.date.recent(),
durationInSeconds: faker.number.int({
min: 0,
max: 3600,
}),
...overrides,
})),
.fill(null).map(() => (generateEpisode(overrides))),
...overrides,
};
}
function generateEpisode(overrides: Partial<Series> = {}): Episode {
return {
title: faker.word.words(3),
subtitle: faker.word.words(3),
link: faker.internet.url(),
url: faker.internet.url(),
shareLink: faker.internet.url(),
id: faker.string.uuid(),
date: faker.date.recent(),
durationInSeconds: faker.number.int({
min: 0,
max: 3600,
}),
...overrides,
};
}
export const testUtils = {
generateSeries,
generateEpisode,
};

31
lib/utils.ts Normal file
View File

@ -0,0 +1,31 @@
import { STATUS_CODE } from "$fresh/server.ts";
export function getHostName() {
const deploymentId = Deno.env.get("DENO_DEPLOYMENT_ID");
if (deploymentId) {
return `https://nrss-${deploymentId}.deno.dev`;
} else {
// assume env
return "http://localhost:8000";
}
}
type EnumValues<T> = T[keyof T];
type Status = EnumValues<typeof STATUS_CODE>;
export function responseJSON(body: unknown | null, status: Status) {
return response(body, status, "json");
}
export function responseXML(body: unknown | null, status: Status) {
return response(body, status, "xml");
}
function response(body: unknown | null, status: number, type: "json" | "xml") {
return new Response(JSON.stringify(body), {
status,
headers: {
"Content-Type": `application/${type}`,
},
});
}

View File

@ -1,16 +1,16 @@
import { FreshContext } from "$fresh/server.ts";
import { FreshContext, STATUS_CODE } from "$fresh/server.ts";
import { caching } from "../../../lib/caching.ts";
import { rss } from "../../../lib/rss.ts";
import { responseJSON, responseXML } from "../../../lib/utils.ts";
export const handler = async (_req: Request, ctx: FreshContext): Promise<Response> => {
const seriesId = ctx.params.seriesId;
const series = await caching.getSeries({ id: seriesId });
if (!series) {
return responseJSON({ message: "Series not found" }, STATUS_CODE.NotFound);
}
const feed = rss.assembleFeed(series);
return new Response(feed, {
headers: {
"Content-Type": "application/xml",
},
});
return responseXML(feed, STATUS_CODE.OK);
};

View File

@ -1,6 +1,7 @@
import { FreshContext } from "$fresh/server.ts";
import { FreshContext, STATUS_CODE } from "$fresh/server.ts";
import { parse, toSeconds } from "https://esm.sh/iso8601-duration@2.1.1";
import { NrkPodcastEpisode, nrkRadio } from "../../../../../lib/nrk/nrk.ts";
import { responseJSON } from "../../../../../lib/utils.ts";
type Chapter = {
title: string | undefined;
@ -25,12 +26,7 @@ export const handler = async (_req: Request, ctx: FreshContext): Promise<Respons
const episode = await nrkRadio.getEpisode(seriesId, episodeId);
if (!episode) {
return new Response(JSON.stringify({ message: `Episode ${episodeId} is missing` }), {
headers: {
"Content-Type": "application/json",
},
status: 500,
});
return responseJSON({ message: `Episode ${episodeId} is missing` }, STATUS_CODE.NotFound);
}
const chapters = toChapters(episode);
const body = {
@ -38,9 +34,5 @@ export const handler = async (_req: Request, ctx: FreshContext): Promise<Respons
chapters,
};
return new Response(JSON.stringify(body), {
headers: {
"Content-Type": "application/json",
},
});
return responseJSON(body, STATUS_CODE.OK);
};

View File

@ -7,27 +7,26 @@ import SeriesCard from "../components/SeriesCard.tsx";
import { CSS, render } from "$gfm";
import { nrkRadio, NrkSearchResultList } from "../lib/nrk/nrk.ts";
interface HandlerData {
query: string;
origin: string;
type Props = {
query: string | null;
rawMarkdown: string;
result?: NrkSearchResultList;
}
result?: NrkSearchResultList | null;
};
export const handler: Handlers<HandlerData> = {
export const handler: Handlers<Props> = {
async GET(request, ctx) {
const url = new URL(request.url);
const query = url.searchParams.get("query");
let result: NrkSearchResultList | undefined;
let result: Props["result"];
if (query) {
result = await nrkRadio.search(query);
}
const rawMarkdown = await Deno.readTextFile(new URL("../docs/what.md", import.meta.url));
return ctx.render({ query: query || "", result, origin: url.origin, rawMarkdown });
return ctx.render({ query, result, rawMarkdown });
},
};
export default function Home({ data }: PageProps<HandlerData>) {
export default function Home({ data, url }: PageProps<Props>) {
return (
<>
<Head>
@ -46,7 +45,7 @@ export default function Home({ data }: PageProps<HandlerData>) {
<div className="p-4 mx-auto max-w-screen-md">
<Header />
<Search defaultValue={data.query} />
<SearchResult result={data.result} origin={data.origin} />
<SearchResult result={data.result} origin={url.origin} />
<div
class="markdown-body"
dangerouslySetInnerHTML={{ __html: render(data?.rawMarkdown) }}
@ -57,7 +56,7 @@ export default function Home({ data }: PageProps<HandlerData>) {
);
}
function SearchResult({ result, origin }: { result: NrkSearchResultList; origin: string }) {
function SearchResult({ result, origin }: { result: Props["result"]; origin: string }) {
if (!result) {
return null;
}

View File

@ -1,9 +0,0 @@
export function getHostName() {
const deploymentId = Deno.env.get("DENO_DEPLOYMENT_ID");
if (deploymentId) {
return `https://nrss-${deploymentId}.deno.dev`;
} else {
// assume env
return "http://localhost:8000";
}
}