diff --git a/components/SeriesCard.tsx b/components/SeriesCard.tsx new file mode 100644 index 0000000..525cebd --- /dev/null +++ b/components/SeriesCard.tsx @@ -0,0 +1,13 @@ +import { Serie } from "../lib/nrk.ts" + +export function SerieCard(props: { serie: Serie }) { + + return
+

{props.serie.title}

+

{props.serie.description}

+ +

+ Feed: {location.hostname}{`/api/feeds/${props.serie.seriesId}`} +

+
; +} diff --git a/deno.lock b/deno.lock index e2d2411..3541790 100644 --- a/deno.lock +++ b/deno.lock @@ -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", diff --git a/fresh.gen.ts b/fresh.gen.ts index a946743..8ef575a 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -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, diff --git a/index.ts b/index.ts deleted file mode 100644 index fde8330..0000000 --- a/index.ts +++ /dev/null @@ -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) { - - // 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 => { - 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) diff --git a/islands/Search.tsx b/islands/Search.tsx new file mode 100644 index 0000000..c724034 --- /dev/null +++ b/islands/Search.tsx @@ -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(""); + const [result, setResult] = useState([]); + + const onSearch = async (event) => { + + const [status, response] = await post("/api/search", { + query + }); + + setResult(response.series as Serie[]) + } + + return ( +
+ { setQuery(event.target.value) }} /> + +
+ {result.map(result => )} +
+
+ ); +} diff --git a/lib/nrk.ts b/lib/nrk.ts new file mode 100644 index 0000000..de14440 --- /dev/null +++ b/lib/nrk.ts @@ -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) { + + // 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 => { + 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); diff --git a/package.json b/package.json deleted file mode 100644 index be5b91f..0000000 --- a/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "nrss", - "version": "1.0.0", - "main": "index.js", - "author": "olaven ", - "license": "MIT", - "devDependencies": { - "typescript-deno-plugin": "^1.31.0" - } -} diff --git a/routes/[name].tsx b/routes/[name].tsx deleted file mode 100644 index 9c06827..0000000 --- a/routes/[name].tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { PageProps } from "$fresh/server.ts"; - -export default function Greet(props: PageProps) { - return
Hello {props.params.name}
; -} diff --git a/routes/api/feeds/[seriesId].ts b/routes/api/feeds/[seriesId].ts new file mode 100644 index 0000000..6be70f1 --- /dev/null +++ b/routes/api/feeds/[seriesId].ts @@ -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 => { + const seriesId = _ctx.params.seriesId; + const feedContent = await buildFeed(seriesId); + + return new Response(feedContent, { + headers: { + "Content-Type": "application/xml" + } + }); +}; diff --git a/routes/api/joke.ts b/routes/api/joke.ts deleted file mode 100644 index a3f4243..0000000 --- a/routes/api/joke.ts +++ /dev/null @@ -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); -}; diff --git a/routes/api/search.ts b/routes/api/search.ts new file mode 100644 index 0000000..c6e8929 --- /dev/null +++ b/routes/api/search.ts @@ -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 => { + + + const { query } = await req.json(); + const series = await nrkRadio.search(query); + + return new Response(JSON.stringify({ series }), { + headers: { "Content-Type": "application/json" } + }); +}; diff --git a/routes/index.tsx b/routes/index.tsx index 0f74419..555d1c8 100644 --- a/routes/index.tsx +++ b/routes/index.tsx @@ -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 ( <> NRSS -
- the fresh logo: a sliced lemon dripping with juice -

- Welcome to `fresh`. Try updating this message in the ./routes/index.tsx - file, and refresh. -

- +
+

NRSS - NRK-podcast som RSS

+
); diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index d27149c..0000000 --- a/yarn.lock +++ /dev/null @@ -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"