Basic RSS generation works!

This commit is contained in:
olaven 2023-03-09 14:27:44 +01:00
parent e066032e61
commit 14fc1fbce6
13 changed files with 234 additions and 241 deletions

13
components/SeriesCard.tsx Normal file
View File

@ -0,0 +1,13 @@
import { Serie } from "../lib/nrk.ts"
export function SerieCard(props: { serie: Serie }) {
return <div className="border-2 my-2 mx-4">
<h2 className="text-4xl">{props.serie.title}</h2>
<p className="text-2xl">{props.serie.description}</p>
<img src={props.serie.images[0].uri} />
<p>
Feed: {location.hostname}{`/api/feeds/${props.serie.seriesId}`}
</p>
</div >;
}

View File

@ -92,7 +92,14 @@
"https://deno.land/x/fresh@1.1.4/src/server/types.ts": "dde992ab4ee635df71a7fc96fe4cd85943c1a9286ea8eb586563d5f5ca154955",
"https://deno.land/x/importmap@0.2.1/_util.ts": "ada9a9618b537e6c0316c048a898352396c882b9f2de38aba18fd3f2950ede89",
"https://deno.land/x/importmap@0.2.1/mod.ts": "ae3d1cd7eabd18c01a4960d57db471126b020f23b37ef14e1359bbb949227ade",
"https://deno.land/x/kall@v0.1.0/mod.ts": "7379f599251db0b86e08d25ed74965d7f00f55491c13ffbb524a4d42bd97f0fe",
"https://deno.land/x/kall@v0.1.0/source/codes.ts": "67f72d771a039f96e4d0ddd23a54b27caf2c28d207dd37a1fe2e66b69bb8a0aa",
"https://deno.land/x/kall@v0.1.0/source/filters.ts": "8fd499ca3e4cb3d7e40963107710fec04d2179771128434bb3e7419b2d204dc7",
"https://deno.land/x/kall@v0.1.0/source/methods.ts": "01f7d90f3509da9a46122f17ae9510d8ed14ec252082d3e61b971ad410b6caf2",
"https://deno.land/x/kall@v0.1.0/source/support.ts": "bb32b6c2274255d95413600e67f8705f53656b5e8bbad4165ab95c442c31543f",
"https://deno.land/x/rutt@0.0.14/mod.ts": "5027b8e8b12acca48b396a25aee74ad7ee94a25c24cda75571d7839cbd41113c",
"https://deno.land/x/serializexml@v0.1.0/mod.ts": "922e83b6897ae004a39e14c726e5f04d3056d58cdd2c206c4cd659e959400f17",
"https://deno.land/x/serializexml@v0.3.2/mod.ts": "eb29afa7c8a03e692086a58abb87047a91fa5e51841c4c757807a58eb04e0730",
"https://deno.land/x/ts_morph@16.0.0/common/DenoRuntime.ts": "537800e840d0994f9055164e11bf33eadf96419246af0d3c453793c3ae67bdb3",
"https://deno.land/x/ts_morph@16.0.0/common/mod.ts": "01985d2ee7da8d1caee318a9d07664774fbee4e31602bc2bb6bb62c3489555ed",
"https://deno.land/x/ts_morph@16.0.0/common/ts_morph_common.d.ts": "39f2ddefd4995e4344236c44c2bf296069149f45ef6f00440b56e7b32cb2b3bd",

View File

@ -3,19 +3,21 @@
// This file is automatically updated during development when running `dev.ts`.
import config from "./deno.json" assert { type: "json" };
import * as $0 from "./routes/[name].tsx";
import * as $1 from "./routes/api/joke.ts";
import * as $0 from "./routes/api/feeds/[seriesId].ts";
import * as $1 from "./routes/api/search.ts";
import * as $2 from "./routes/index.tsx";
import * as $$0 from "./islands/Counter.tsx";
import * as $$1 from "./islands/Search.tsx";
const manifest = {
routes: {
"./routes/[name].tsx": $0,
"./routes/api/joke.ts": $1,
"./routes/api/feeds/[seriesId].ts": $0,
"./routes/api/search.ts": $1,
"./routes/index.tsx": $2,
},
islands: {
"./islands/Counter.tsx": $$0,
"./islands/Search.tsx": $$1,
},
baseUrl: import.meta.url,
config,

View File

@ -1,67 +0,0 @@
import { get, OK } from "https://deno.land/x/kall@v0.1.0/mod.ts";
// incomplete type of Series
type Series = {
id: string, //random id
seriesId: string, // readable id
title: string,
description: string,
images: any,
}
// incomplete type of Episode
type Episode = {
id: string,
episodeId: string,
url: string,
}
async function withDownloadLink(withoutUrl: Omit<Episode, "url">) {
// getting stream link
const [playbackStatus, playbackResponse] = await get(`https://psapi.nrk.no/playback/manifest/podcast/${withoutUrl.episodeId}`)
if (playbackStatus === OK) {
const url = playbackResponse.playable.assets[0].url;
return {
...withoutUrl, url
}
} else {
throw `Error getting downloadLink for ${withoutUrl.episodeId}`
}
}
const nrkRadio = {
search: async (query: string): Promise<Series[]> => {
const [status, response] = await get(`https://psapi.nrk.no/radio/search/search?q=${query}`);
if (status === OK) {
const series = response.results.series.results
console.log("SERIES TYPES ", series)
return series as Series[];
} else {
throw `Something went wrong with ${query} - got status ${status}`
}
},
getEpisodes: async (seriesId: string) => {
const [status, response] = await get(`https://psapi.nrk.no/radio/catalog/podcast/${seriesId}/episodes`);
if (status === OK) {
const episodes = response._embedded.episodes;
return await Promise.all(episodes.map(withDownloadLink))
} else {
throw `Error getting episodes for ${seriesId}: Status: ${status}`
}
},
streamEpisode: async (id: string) => {
// not sure how to do this
`https://nrk-od-43.akamaized.net/world/1639243/0/hls/${id}/playlist.m3u8?bw_low=32&bw_high=194`
}
}
const [serie] = await nrkRadio.search("kladden");
const episodes = await nrkRadio.getEpisodes(serie.seriesId)
console.log("Episodes", episodes)

37
islands/Search.tsx Normal file
View File

@ -0,0 +1,37 @@
import { Head } from "$fresh/runtime.ts";
import { useState } from "preact/hooks";
import { post, OK } from "https://deno.land/x/kall@v0.1.0/mod.ts";
import { Serie } from "../lib/nrk.ts";
import { SerieCard } from "../components/SeriesCard.tsx";
export default function Search() {
const [query, setQuery] = useState<string>("");
const [result, setResult] = useState<Serie[]>([]);
const onSearch = async (event) => {
const [status, response] = await post("/api/search", {
query
});
setResult(response.series as Serie[])
}
return (
<div className="flex flex-col w-1/2 mx-auto my-4">
<input
placeholder="NRK-podcast"
className="border-2"
value={query}
onInput={(event) => { setQuery(event.target.value) }} />
<button
className="my-2 bg-gray-100"
onClick={onSearch}>Søk</button>
<div>
{result.map(result => <SerieCard serie={result}></SerieCard>)}
</div>
</div>
);
}

82
lib/nrk.ts Normal file
View File

@ -0,0 +1,82 @@
import { get, OK } from "https://deno.land/x/kall@v0.1.0/mod.ts";
// incomplete type of Series
export type Serie = {
id: string, //random id
seriesId: string, // readable id
title: string,
description: string,
images: any,
}
// incomplete type of Episode
export type Episode = {
id: string,
episodeId: string,
url: string,
titles: {
title: string,
description: string,
},
durationInSeconds: number,
date: string,
}
async function withDownloadLink(withoutUrl: Omit<Episode, "url">) {
// getting stream link
const [playbackStatus, playbackResponse] = await get(`https://psapi.nrk.no/playback/manifest/podcast/${withoutUrl.episodeId}`)
if (playbackStatus === OK) {
const url = playbackResponse.playable.assets[0].url;
return {
...withoutUrl, url
}
} else {
throw `Error getting downloadLink for ${withoutUrl.episodeId}`
}
}
export const nrkRadio = {
search: async (query: string): Promise<Serie[]> => {
const [status, response] = await get(`https://psapi.nrk.no/radio/search/search?q=${query}`);
if (status === OK) {
const series = response.results.series.results
return series as Serie[];
} else {
throw `Something went wrong with ${query} - got status ${status}`
}
},
getSerieData: async (seriesId: string) => {
const [
[episodeStatus, episodeResponse],
[seriesStatus, serieResponse],
] = await Promise.all([
get(`https://psapi.nrk.no/radio/catalog/podcast/${seriesId}/episodes`),
get(`https://psapi.nrk.no/radio/catalog/podcast/${seriesId}`)
]);
if (episodeStatus === OK && seriesStatus === OK) {
const episodes: Episode[] = await Promise.all(episodeResponse._embedded.episodes.map(withDownloadLink));
return {
id: serieResponse.series.id,
title: serieResponse.series.titles.title,
subtitle: serieResponse.series.titles.subtitle,
image: serieResponse.series.image[1],
episodes,
}
} else {
throw `Error getting episodes for ${seriesId}: EpisodeStatus: ${episodeStatus}. SerieStatus: ${seriesStatus}`
}
}
}
const [dynga] = await nrkRadio.search("Dynga");
const info = await nrkRadio.getSerieData(dynga.seriesId);

View File

@ -1,10 +0,0 @@
{
"name": "nrss",
"version": "1.0.0",
"main": "index.js",
"author": "olaven <olav@sundfoer.com>",
"license": "MIT",
"devDependencies": {
"typescript-deno-plugin": "^1.31.0"
}
}

View File

@ -1,5 +0,0 @@
import { PageProps } from "$fresh/server.ts";
export default function Greet(props: PageProps) {
return <div>Hello {props.params.name}</div>;
}

View File

@ -0,0 +1,68 @@
import { nrkRadio, Serie, Episode } from "../../../lib/nrk.ts"
import { HandlerContext } from "$fresh/server.ts";
import { serialize, tag, declaration } from "https://deno.land/x/serializexml@v0.3.2/mod.ts";
function toItemTag(episode: Episode): string {
return tag("item", [
tag("title", episode.title),
tag("link", episode.url),
tag("description", episode.description),
tag("itunes:summary", episode.description),
tag("guid", episode.id, ["isPermaLink", "false"]),
tag("pubDate", episode.date),
tag("enclosure", "", [
["url", episode.url],
["length", episode.durationInSeconds],
["type", "audio/mpeg3"],
]),
])
}
async function buildFeed(seriesId) {
const serie = await nrkRadio.getSerieData(seriesId)
// Quickly adapted from https://raw.githubusercontent.com/olaven/paperpod/1cde9abd3174b26e126aa74fc5a3b63fd078c0fd/packages/converter/src/rss.ts
return serialize(
declaration([
["version", "1.0"],
["encoding", "UTF-8"],
]),
tag(
"rss",
[
tag("channel", [
tag("title", serie.title),
tag("link", `https://radio.nrk.no/podkast/${serie.id}`),
tag(
"description",
serie.subtitle
),
tag("ttl", "60"), //60 minutes
tag("image", [
tag("url", serie.image.uri),
tag("title", serie.title),
]),
...serie.episodes.map(toItemTag),
]),
],
[
["version", "2.0"],
["xmlns:itunes", "http://www.itunes.com/dtds/podcast-1.0.dtd"],
["xmlns:content", "http://purl.org/rss/1.0/modules/content/"],
]
)
);
}
export const handler = async (req: Request, _ctx: HandlerContext): Promise<Response> => {
const seriesId = _ctx.params.seriesId;
const feedContent = await buildFeed(seriesId);
return new Response(feedContent, {
headers: {
"Content-Type": "application/xml"
}
});
};

View File

@ -1,21 +0,0 @@
import { HandlerContext } from "$fresh/server.ts";
// Jokes courtesy of https://punsandoneliners.com/randomness/programmer-jokes/
const JOKES = [
"Why do Java developers often wear glasses? They can't C#.",
"A SQL query walks into a bar, goes up to two tables and says “can I join you?”",
"Wasn't hard to crack Forrest Gump's password. 1forrest1.",
"I love pressing the F5 key. It's refreshing.",
"Called IT support and a chap from Australia came to fix my network connection. I asked “Do you come from a LAN down under?”",
"There are 10 types of people in the world. Those who understand binary and those who don't.",
"Why are assembly programmers often wet? They work below C level.",
"My favourite computer based band is the Black IPs.",
"What programme do you use to predict the music tastes of former US presidential candidates? An Al Gore Rhythm.",
"An SEO expert walked into a bar, pub, inn, tavern, hostelry, public house.",
];
export const handler = (_req: Request, _ctx: HandlerContext): Response => {
const randomIndex = Math.floor(Math.random() * JOKES.length);
const body = JOKES[randomIndex];
return new Response(body);
};

14
routes/api/search.ts Normal file
View File

@ -0,0 +1,14 @@
import { nrkRadio } from "../../lib/nrk.ts"
import { HandlerContext } from "$fresh/server.ts";
export const handler = async (req: Request, _ctx: HandlerContext): Promise<Response> => {
const { query } = await req.json();
const series = await nrkRadio.search(query);
return new Response(JSON.stringify({ series }), {
headers: { "Content-Type": "application/json" }
});
};

View File

@ -1,23 +1,18 @@
import { Head } from "$fresh/runtime.ts";
import Counter from "../islands/Counter.tsx";
import { useState } from "preact/hooks";
import { post, OK } from "https://deno.land/x/kall@v0.1.0/mod.ts";
import Search from "../islands/Search.tsx"
export default function Home() {
return (
<>
<Head>
<title>NRSS</title>
</Head>
<div class="p-4 mx-auto max-w-screen-md">
<img
src="/logo.svg"
class="w-32 h-32"
alt="the fresh logo: a sliced lemon dripping with juice"
/>
<p class="my-6 bg-blue-400 text-red">
Welcome to `fresh`. Try updating this message in the ./routes/index.tsx
file, and refresh.
</p>
<Counter start={3} />
<div className="p-4 mx-auto max-w-screen-md bg-blue-400">
<h1 className="text-4xl text-center">NRSS - NRK-podcast som RSS</h1>
<Search />
</div>
</>
);

122
yarn.lock
View File

@ -1,122 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
arr-union@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
integrity sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==
clone-deep@^0.2.4:
version "0.2.4"
resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-0.2.4.tgz#4e73dd09e9fb971cc38670c5dced9c1896481cc6"
integrity sha512-we+NuQo2DHhSl+DP6jlUiAhyAjBQrYnpOk15rN6c6JSPScjiCLh8IbSU+VTcph6YS3o7mASE8a0+gbZ7ChLpgg==
dependencies:
for-own "^0.1.3"
is-plain-object "^2.0.1"
kind-of "^3.0.2"
lazy-cache "^1.0.3"
shallow-clone "^0.1.2"
for-in@^0.1.3:
version "0.1.8"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1"
integrity sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g==
for-in@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
integrity sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==
for-own@^0.1.3:
version "0.1.5"
resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce"
integrity sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==
dependencies:
for-in "^1.0.1"
import-maps@^0.2.1:
version "0.2.4"
resolved "https://registry.yarnpkg.com/import-maps/-/import-maps-0.2.4.tgz#7404dc331acb13bbf98151d742a5ce238e8b6d41"
integrity sha512-31ZDO53RS3HcZUDIj8OMNuFr6gXkc6wsK+lTZosZuT8CDF/Y1Hk2gAJ6X+bdiEZoj8GkRN+nS5hFZQuOl0f29g==
is-buffer@^1.0.2, is-buffer@^1.1.5:
version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==
is-extendable@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==
is-plain-object@^2.0.1:
version "2.0.4"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
dependencies:
isobject "^3.0.1"
isobject@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==
kind-of@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5"
integrity sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg==
dependencies:
is-buffer "^1.0.2"
kind-of@^3.0.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
integrity sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==
dependencies:
is-buffer "^1.1.5"
lazy-cache@^0.2.3:
version "0.2.7"
resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-0.2.7.tgz#7feddf2dcb6edb77d11ef1d117ab5ffdf0ab1b65"
integrity sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==
lazy-cache@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e"
integrity sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==
merge-deep@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/merge-deep/-/merge-deep-3.0.3.tgz#1a2b2ae926da8b2ae93a0ac15d90cd1922766003"
integrity sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==
dependencies:
arr-union "^3.1.0"
clone-deep "^0.2.4"
kind-of "^3.0.2"
mixin-object@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e"
integrity sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA==
dependencies:
for-in "^0.1.3"
is-extendable "^0.1.1"
shallow-clone@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-0.1.2.tgz#5909e874ba77106d73ac414cfec1ffca87d97060"
integrity sha512-J1zdXCky5GmNnuauESROVu31MQSnLoYvlyEn6j2Ztk6Q5EHFIhxkMhYcv6vuDzl2XEzoRr856QwzMgWM/TmZgw==
dependencies:
is-extendable "^0.1.1"
kind-of "^2.0.1"
lazy-cache "^0.2.3"
mixin-object "^2.0.1"
typescript-deno-plugin@^1.31.0:
version "1.31.0"
resolved "https://registry.yarnpkg.com/typescript-deno-plugin/-/typescript-deno-plugin-1.31.0.tgz#7e354c1b30328707ac1d2ea724355d24db5c72f2"
integrity sha512-IoF5c8EubyNgjxxarguZfMx8Ph9Av9XqjySop0orepXv8wr02ND3DNNDFFdQyZfQAAcv1/v/u14MQIp8Os4c1w==
dependencies:
import-maps "^0.2.1"
merge-deep "^3.0.2"