diff --git a/README.md b/README.md index 906778d..49a2ce0 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,21 @@ -# NRSS - RSS Feeds for NRK sin podcaster +# NRSS - RSS Feeds for NRK sin podcaster https://nrss.deno.dev/ ## Local devlopment + install Deno https://deno.land/manual/getting_started/installation ### Run the application -``` shell + +```shell deno task start ``` + Open http://localhost:8000/ to view it in the browser +## Known Problems -## Known Problems -- Some clients may not accept a feed hosted over HTTPS only. See @steinarb's workaround [here](https://github.com/olaven/NRSS/issues/5#issuecomment-1488840679) for a possible solution. +- Some clients may not accept a feed hosted over HTTPS only. See @steinarb's workaround [here](https://github.com/olaven/NRSS/issues/5#issuecomment-1488840679) for a possible solution. - The feeds only provide the latest episodes for a podcast, not the entire archive. I've not yet found any acceptable method of fetching all episodes without getting rate limited by NRK or introducing a storage layer. This is tracked in https://github.com/olaven/NRSS/issues/8. diff --git a/components/Search.tsx b/components/Search.tsx index 0fa606b..c643683 100644 --- a/components/Search.tsx +++ b/components/Search.tsx @@ -1,9 +1,17 @@ export default function Search(props: { defaultValue: string }) { - return ( -
- - - -
- ); + return ( +
+ + + +
+ ); } diff --git a/components/SeriesCard.tsx b/components/SeriesCard.tsx index e19603e..779186c 100644 --- a/components/SeriesCard.tsx +++ b/components/SeriesCard.tsx @@ -1,19 +1,19 @@ -import { SearchResult } from "../lib/nrk.ts" +import { SearchResult } from "../lib/nrk.ts"; import CopyButton from "../islands/CopyButton.tsx"; -export function SerieCard(props: { serie: SearchResult, origin: string }) { - const feedUrl = new URL(`/api/feeds/${props.serie.seriesId}`, props.origin); - const image = props.serie.images[0]; - return ( -
-
-

{props.serie.title}

-

{props.serie.description}

- - - Kopier URL - -
-
- ); +export function SerieCard(props: { serie: SearchResult; origin: string }) { + const feedUrl = new URL(`/api/feeds/${props.serie.seriesId}`, props.origin); + const image = props.serie.images[0]; + return ( +
+
+

{props.serie.title}

+

{props.serie.description}

+ + + Kopier URL + +
+
+ ); } diff --git a/deno.json b/deno.json index da509f3..25dbdce 100644 --- a/deno.json +++ b/deno.json @@ -1,7 +1,8 @@ { "lock": false, "tasks": { - "start": "deno run -A --watch=static/,routes/ dev.ts" + "start": "deno run -A --watch=static/,routes/ dev.ts", + "check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx" }, "imports": { "$fresh/": "https://deno.land/x/fresh@1.6.5/", @@ -22,8 +23,8 @@ "output" ], "useTabs": false, - "lineWidth": 80, - "indentWidth": 4, + "lineWidth": 120, + "indentWidth": 2, "proseWrap": "preserve" } -} \ No newline at end of file +} diff --git a/fresh.gen.ts b/fresh.gen.ts index 4d62744..036143b 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -1,24 +1,23 @@ -// DO NOT EDIT. This file is generated by fresh. +// DO NOT EDIT. This file is generated by Fresh. // This file SHOULD be checked into source version control. // This file is automatically updated during development when running `dev.ts`. -import config from "./deno.json" assert { type: "json" }; -import * as $0 from "./routes/api/feeds/[seriesId].ts"; -import * as $1 from "./routes/api/feeds/[seriesId]/[episodeId]/chapters.ts"; -import * as $2 from "./routes/index.tsx"; -import * as $$0 from "./islands/CopyButton.tsx"; +import * as $api_feeds_seriesId_ from "./routes/api/feeds/[seriesId].ts"; +import * as $api_feeds_seriesId_episodeId_chapters from "./routes/api/feeds/[seriesId]/[episodeId]/chapters.ts"; +import * as $index from "./routes/index.tsx"; +import * as $CopyButton from "./islands/CopyButton.tsx"; +import { type Manifest } from "$fresh/server.ts"; const manifest = { routes: { - "./routes/api/feeds/[seriesId].ts": $0, - "./routes/api/feeds/[seriesId]/[episodeId]/chapters.ts": $1, - "./routes/index.tsx": $2, + "./routes/api/feeds/[seriesId].ts": $api_feeds_seriesId_, + "./routes/api/feeds/[seriesId]/[episodeId]/chapters.ts": $api_feeds_seriesId_episodeId_chapters, + "./routes/index.tsx": $index, }, islands: { - "./islands/CopyButton.tsx": $$0, + "./islands/CopyButton.tsx": $CopyButton, }, baseUrl: import.meta.url, - config, -}; +} satisfies Manifest; export default manifest; diff --git a/islands/CopyButton.tsx b/islands/CopyButton.tsx index 82a32da..6037c40 100644 --- a/islands/CopyButton.tsx +++ b/islands/CopyButton.tsx @@ -1,28 +1,29 @@ import Preact from "preact"; -import { useState } from "preact/hooks" +import { useState } from "preact/hooks"; import { IS_BROWSER } from "$fresh/runtime.ts"; -export default function CopyButton(props: { textBefore: string, textAfter: string } & Preact.PropsWithChildren) { +export default function CopyButton( + props: { textBefore: string; textAfter: string } & Preact.PropsWithChildren, +) { + const [clicked, setClicked] = useState(false); - const [clicked, setClicked] = useState(false); + const startReset = () => { + setTimeout(() => { + setClicked(false); + }, 2_000); + }; - const startReset = () => { - setTimeout(() => { - setClicked(false); - }, 2_000); - } - - return ( - - ); + return ( + + ); } diff --git a/lib/nrk-catalog.ts b/lib/nrk-catalog.ts index aa67f93..889a656 100644 --- a/lib/nrk-catalog.ts +++ b/lib/nrk-catalog.ts @@ -508,7 +508,9 @@ export interface components { /** @description Aggregated info for clips */ ExtraMaterialClipsHalEmbedded: { metadata?: { - displayAspectRatio: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["DisplayAspectRatio"]; + displayAspectRatio: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["DisplayAspectRatio"]; /** * @description Clip length. The string is formatted as https://en.wikipedia.org/wiki/ISO_8601#Durations * @example PT2M0S @@ -517,7 +519,9 @@ export interface components { id: string; /** @enum {undefined} */ playability: "nonPlayable" | "playable"; - preplay: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["PreplayVm"]; + preplay: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["PreplayVm"]; /** @description Link to full metadata */ _links: { self: components["schemas"]["HalLink"]; @@ -886,36 +890,56 @@ export interface external { "https://psapi.nrk.no/documentation/openapi/playback/openapi.json": { paths: { "/playback/debug": { - get: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["operations"]["getPlaybackDebug"]; + get: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["operations"]["getPlaybackDebug"]; }; "/playback/dnslist": { - get: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["operations"]["getPlaybackDnsList"]; + get: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["operations"]["getPlaybackDnsList"]; }; "/playback/manifest/{id}": { /** This endpoint is just for clients that doesn't know its context */ - get: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["operations"]["getPlaybackManifestRedirect"]; + get: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["operations"]["getPlaybackManifestRedirect"]; }; "/playback/manifest/program/{programId}": { - get: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["operations"]["getPlaybackProgramManifest"]; + get: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["operations"]["getPlaybackProgramManifest"]; }; "/playback/manifest/channel/{channelId}": { - get: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["operations"]["getPlaybackChannelManifest"]; + get: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["operations"]["getPlaybackChannelManifest"]; }; "/playback/manifest/clip/{clipId}": { - get: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["operations"]["getPlaybackClipManifest"]; + get: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["operations"]["getPlaybackClipManifest"]; }; "/playback/metadata/{id}": { /** This endpoint is just for clients that doesn't know its context */ - get: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["operations"]["getPlaybackMetadataRedirect"]; + get: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["operations"]["getPlaybackMetadataRedirect"]; }; "/playback/metadata/program/{programId}": { - get: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["operations"]["getPlaybackProgramMetadata"]; + get: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["operations"]["getPlaybackProgramMetadata"]; }; "/playback/metadata/channel/{channelId}": { - get: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["operations"]["getPlaybackChannelMetadata"]; + get: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["operations"]["getPlaybackChannelMetadata"]; }; "/playback/metadata/clip/{clipId}": { - get: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["operations"]["getPlaybackClipMetadata"]; + get: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["operations"]["getPlaybackClipMetadata"]; }; }; components: { @@ -935,8 +959,12 @@ export interface external { href: string; }; }; - playbackData: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["PlaybackDataVm"]; - locationData: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["LocationDataVm"]; + playbackData: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["PlaybackDataVm"]; + locationData: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["LocationDataVm"]; }; PlaybackDataVm: { maxBitrate: number; @@ -951,9 +979,15 @@ export interface external { isBeta: boolean; originalUa: string; wurflId: string | null; - distributionFormat: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["DistributionFormat"]; - deviceGroupName: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["DeviceGroupName"]; - playerPlatform: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["PlayerPlatform"]; + distributionFormat: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["DistributionFormat"]; + deviceGroupName: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["DeviceGroupName"]; + playerPlatform: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["PlayerPlatform"]; customManifestParameters: ( | "AudioOnly" | "DolbyOnly" @@ -965,7 +999,9 @@ export interface external { | "NoSignalDiscontinuities" | "SignalDiscontinuities" )[]; - subtitlesDistribution: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["SubtitlesDistribution"]; + subtitlesDistribution: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["SubtitlesDistribution"]; }; /** @enum {string} */ DistributionFormat: @@ -975,7 +1011,12 @@ export interface external { | "ProgressiveDownload" | "ProgressiveMp3"; /** @enum {string} */ - DeviceGroupName: "Desktop" | "Mobile" | "SetTopUnit" | "Tablet" | "Tv"; + DeviceGroupName: + | "Desktop" + | "Mobile" + | "SetTopUnit" + | "Tablet" + | "Tv"; /** @enum {string} */ PlayerPlatform: | "Altibox" @@ -1021,28 +1062,51 @@ export interface external { forwardedFor: string; realIp: string; }; - ManifestResponse: Partial< - external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["PlayableManifest"] - > & - Partial< - external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["NonPlayableManifest"] + ManifestResponse: + & Partial< + external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["PlayableManifest"] + > + & Partial< + external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["NonPlayableManifest"] >; PlayableManifest: { /** @enum {string} */ playability: "playable"; - playable: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["PlayableElementVm"]; + playable: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["PlayableElementVm"]; statistics: { - scores: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["ScoresStatisticsVm"]; - ga: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["GoogleAnalyticsVm"]; - conviva: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["ConvivaStatisticsVm"]; - luna: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["LunaStatisticsVm"]; + scores: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["ScoresStatisticsVm"]; + ga: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["GoogleAnalyticsVm"]; + conviva: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["ConvivaStatisticsVm"]; + luna: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["LunaStatisticsVm"]; }; nonPlayable: unknown | null; id: string; - streamingMode: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["PlayableStreamingMode"]; - availability: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["AvailabilityVm"]; - displayAspectRatio: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["DisplayAspectRatio"]; - sourceMedium: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["PlayableSourceMedium"]; + streamingMode: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["PlayableStreamingMode"]; + availability: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["AvailabilityVm"]; + displayAspectRatio: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["DisplayAspectRatio"]; + sourceMedium: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["PlayableSourceMedium"]; _links: { self: { href: string; @@ -1063,12 +1127,22 @@ export interface external { ga: unknown | null; luna: unknown | null; }; - nonPlayable: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["NonPlayableElementVm"]; + nonPlayable: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["NonPlayableElementVm"]; id: string; - streamingMode: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["PlayableStreamingMode"]; - availability: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["AvailabilityVm"]; - displayAspectRatio: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["DisplayAspectRatio"]; - sourceMedium: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["PlayableSourceMedium"]; + streamingMode: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["PlayableStreamingMode"]; + availability: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["AvailabilityVm"]; + displayAspectRatio: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["DisplayAspectRatio"]; + sourceMedium: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["PlayableSourceMedium"]; _links: { self: { href: string; @@ -1084,19 +1158,31 @@ export interface external { endSequenceStartTime: string | null; duration: string | null; /** @description List of mediaAssets */ - assets: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["MediaUrlVm"][]; + assets: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["MediaUrlVm"][]; /** @description Livebuffer if element is live */ - liveBuffer: Partial< - external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["LiveBufferVm"] - > | null; + liveBuffer: + | Partial< + external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["LiveBufferVm"] + > + | null; /** @description List of subtitles */ - subtitles: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["SubtitlesVm"][]; + subtitles: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["SubtitlesVm"][]; }; MediaUrlVm: { /** @description The media URL itself. */ url: string; - format: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["PlayableFormat"]; - mimeType: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["PlayableMimeType"]; + format: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["PlayableFormat"]; + mimeType: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["PlayableMimeType"]; /** @description True if program is marked as HDSecure, false otherwise */ encrypted: boolean; }; @@ -1120,7 +1206,9 @@ export interface external { bufferStartTime: string | null; /** @description Live buffer length */ bufferDuration: string; - bufferType: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["LiveBufferType"]; + bufferType: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["LiveBufferType"]; }; /** * @description Type of liveBuffer @@ -1128,9 +1216,15 @@ export interface external { */ LiveBufferType: "fixed" | "growing" | "none" | "sliding"; SubtitlesVm: { - type: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["SubtitleType"]; - language: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["SubtitleLanguage"]; - label: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["SubtitleLabel"]; + type: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["SubtitleType"]; + language: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["SubtitleLanguage"]; + label: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["SubtitleLabel"]; defaultOn: boolean; webVtt: string; }; @@ -1139,7 +1233,10 @@ export interface external { /** @enum {string} */ SubtitleLanguage: "en" | "nb"; /** @enum {string} */ - SubtitleLabel: "English" | "Norsk tekst" | "Tekstet for hørselshemmede"; + SubtitleLabel: + | "English" + | "Norsk tekst" + | "Tekstet for hørselshemmede"; ScoresStatisticsVm: { springStreamSite: string; springStreamStream: string; @@ -1178,14 +1275,18 @@ export interface external { * When duration is unknown, eg channels: -1 */ duration: number; - streamType: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["ConvivaStreamType"]; + streamType: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["ConvivaStreamType"]; /** * @description The URL from where this video content will be downloaded. For * manifest-based streaming protocols, it should be the URL of the top-level * manifest. */ streamUrl: string; - custom: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["ConvivaCustomProperties"]; + custom: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["ConvivaCustomProperties"]; }; /** @enum {string} */ ConvivaStreamType: "LIVE" | "VOD"; @@ -1221,7 +1322,9 @@ export interface external { * Example: underholdning */ category: string; - mediaType: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["ConvivaMediaType"]; + mediaType: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["ConvivaMediaType"]; /** * @description Name of selected CDN * Example: Telenor-Cdn @@ -1239,7 +1342,9 @@ export interface external { * Example: tv.nrk.no, nrk.no, nrksuper.no */ serviceName: string; - contentType: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["ConvivaContentType"]; + contentType: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["ConvivaContentType"]; }; /** * @description MediaType for current content @@ -1257,8 +1362,12 @@ export interface external { | "Podcast" | "Program"; LunaStatisticsVm: { - config: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["LunaConfig"]; - data: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["LunaPayload"]; + config: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["LunaConfig"]; + data: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["LunaPayload"]; }; LunaConfig: { beacon: string; @@ -1288,27 +1397,37 @@ export interface external { * @description Only applicable for on-demand media elements. Will be null for live media * elements. */ - onDemand: Partial< - external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["OnDemandUsageRights"] - > | null; + onDemand: + | Partial< + external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["OnDemandUsageRights"] + > + | null; /** * @description Only applicable for live media elements. Will be null for on-demand media * elements. */ live: - | (Partial<{ + | ( + & Partial<{ /** @enum {string} */ type: "channel"; /** @enum {boolean} */ isOngoing: true; transmissionInterval: unknown | null; - }> & - Partial<{ - /** @enum {string} */ - type: "transmission"; - isOngoing: boolean; - transmissionInterval: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["LiveTransmissionIntervalVm"]; - }>) + }> + & Partial<{ + /** @enum {string} */ + type: "transmission"; + isOngoing: boolean; + transmissionInterval: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"][ + "LiveTransmissionIntervalVm" + ]; + }> + ) | null; externalEmbeddingAllowed: boolean; }; @@ -1326,8 +1445,12 @@ export interface external { /** @enum {string} */ PlayableSourceMedium: "audio" | "video"; NonPlayableElementVm: { - reason: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["NonPlayableReason"]; - messageType: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["NonPlayableMessageType"]; + reason: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["NonPlayableReason"]; + messageType: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["NonPlayableMessageType"]; /** * @description Standard message for end user describing why the media element is not * playable. @@ -1389,7 +1512,9 @@ export interface external { /** @description Image to show before play */ PosterVm: { /** @description List of images in different sizes */ - images: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["PosterImageVm"][]; + images: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["PosterImageVm"][]; }; /** @description Title to show before play */ TitlesVm: { @@ -1397,12 +1522,18 @@ export interface external { subtitle: string; }; PreplayVm: { - titles: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["TitlesVm"]; + titles: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["TitlesVm"]; /** @description Description to show before play */ description: string; - poster: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["PosterVm"]; + poster: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["PosterVm"]; /** @description List of index points */ - indexPoints: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["IndexPointVm"][]; + indexPoints: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["IndexPointVm"][]; }; LegalAgeRating: { code: string; @@ -1412,7 +1543,9 @@ export interface external { LegalAgeBodyRated: { /** @enum {string} */ status: "rated"; - rating: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["LegalAgeRating"]; + rating: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["LegalAgeRating"]; }; LegalAgeBodyExempt: { /** @enum {string} */ @@ -1420,11 +1553,16 @@ export interface external { }; LegalAgeVm: { legalReference: string; - body: Partial< - external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["LegalAgeBodyExempt"] - > & - Partial< - external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["LegalAgeBodyRated"] + body: + & Partial< + external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["LegalAgeBodyExempt"] + > + & Partial< + external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["LegalAgeBodyRated"] >; }; PlayableMetadataVm: { @@ -1434,33 +1572,63 @@ export interface external { PlayableMetadata: { /** @enum {string} */ playability: "playable"; - playable: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["PlayableMetadataVm"]; + playable: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["PlayableMetadataVm"]; nonPlayable: unknown | null; id: string; - streamingMode: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["PlayableStreamingMode"]; + streamingMode: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["PlayableStreamingMode"]; duration: string | null; - availability: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["AvailabilityVm"]; - legalAge: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["LegalAgeVm"]; - preplay: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["PreplayVm"]; - displayAspectRatio: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["DisplayAspectRatio"]; - skipDialogInfo: Partial< - external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["SkipDialogInfoVm"] - > | null; - sourceMedium: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["PlayableSourceMedium"]; + availability: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["AvailabilityVm"]; + legalAge: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["LegalAgeVm"]; + preplay: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["PreplayVm"]; + displayAspectRatio: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["DisplayAspectRatio"]; + skipDialogInfo: + | Partial< + external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["SkipDialogInfoVm"] + > + | null; + sourceMedium: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["PlayableSourceMedium"]; _links: { self: { href: string; }; - manifests: Partial< - { - href: string; - name: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["ManifestLinkName"]; - }[] - > | null; - next: Partial< - external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["NextLink"] - > | null; - series?: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["SeriesLink"]; + manifests: + | Partial< + { + href: string; + name: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"][ + "ManifestLinkName" + ]; + }[] + > + | null; + next: + | Partial< + external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["NextLink"] + > + | null; + series?: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["SeriesLink"]; progress?: { href: string; /** @enum {boolean} */ @@ -1468,50 +1636,90 @@ export interface external { }; }; _embedded: { - manifests: Partial< - { - id: string; - availabilityLabel: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["AvailabilityLabel"]; - _links: { - self: { - href: string; - name: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["ManifestLinkName"]; + manifests: + | Partial< + { + id: string; + availabilityLabel: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"][ + "AvailabilityLabel" + ]; + _links: { + self: { + href: string; + name: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"][ + "ManifestLinkName" + ]; + }; }; - }; - }[] - > | null; + }[] + > + | null; }; }; NonPlayableMetadata: { /** @enum {string} */ playability: "nonPlayable"; playable: unknown | null; - nonPlayable: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["NonPlayableElementVm"]; + nonPlayable: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["NonPlayableElementVm"]; id: string; - streamingMode: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["PlayableStreamingMode"]; + streamingMode: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["PlayableStreamingMode"]; duration: string | null; - availability: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["AvailabilityVm"]; - legalAge: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["LegalAgeVm"]; - preplay: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["PreplayVm"]; - displayAspectRatio: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["DisplayAspectRatio"]; - skipDialogInfo: Partial< - external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["SkipDialogInfoVm"] - > | null; - sourceMedium: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["PlayableSourceMedium"]; + availability: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["AvailabilityVm"]; + legalAge: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["LegalAgeVm"]; + preplay: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["PreplayVm"]; + displayAspectRatio: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["DisplayAspectRatio"]; + skipDialogInfo: + | Partial< + external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["SkipDialogInfoVm"] + > + | null; + sourceMedium: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["PlayableSourceMedium"]; _links: { self: { href: string; }; - manifests: Partial< - { - href: string; - name: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["ManifestLinkName"]; - }[] - > | null; - next: Partial< - external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["NextLink"] - > | null; - series?: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["SeriesLink"]; + manifests: + | Partial< + { + href: string; + name: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"][ + "ManifestLinkName" + ]; + }[] + > + | null; + next: + | Partial< + external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["NextLink"] + > + | null; + series?: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["SeriesLink"]; progress?: { href: string; /** @enum {boolean} */ @@ -1519,25 +1727,40 @@ export interface external { }; }; _embedded: { - manifests: Partial< - { - id: string; - availabilityLabel: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["AvailabilityLabel"]; - _links: { - self: { - href: string; - name: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["ManifestLinkName"]; + manifests: + | Partial< + { + id: string; + availabilityLabel: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"][ + "AvailabilityLabel" + ]; + _links: { + self: { + href: string; + name: external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"][ + "ManifestLinkName" + ]; + }; }; - }; - }[] - > | null; + }[] + > + | null; }; }; - MetadataResponse: Partial< - external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["PlayableMetadata"] - > & - Partial< - external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["NonPlayableMetadata"] + MetadataResponse: + & Partial< + external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["PlayableMetadata"] + > + & Partial< + external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["NonPlayableMetadata"] >; }; }; @@ -1547,7 +1770,9 @@ export interface external { /** OK */ 200: { content: { - "application/json": external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["DebugData"]; + "application/json": external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["DebugData"]; }; }; }; @@ -1557,7 +1782,9 @@ export interface external { /** OK */ 200: { content: { - "application/json": external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["DnsList"]; + "application/json": external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["DnsList"]; }; }; }; @@ -1593,7 +1820,9 @@ export interface external { /** OK */ 200: { content: { - "application/json": external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["ManifestResponse"]; + "application/json": external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["ManifestResponse"]; }; }; }; @@ -1623,7 +1852,9 @@ export interface external { /** OK */ 200: { content: { - "application/json": external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["ManifestResponse"]; + "application/json": external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["ManifestResponse"]; }; }; }; @@ -1651,7 +1882,9 @@ export interface external { /** OK */ 200: { content: { - "application/json": external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["ManifestResponse"]; + "application/json": external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["ManifestResponse"]; }; }; }; @@ -1679,7 +1912,9 @@ export interface external { /** OK */ 200: { content: { - "application/json": external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["ManifestResponse"]; + "application/json": external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["ManifestResponse"]; }; }; }; @@ -1709,7 +1944,9 @@ export interface external { /** OK */ 200: { content: { - "application/json": external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["MetadataResponse"]; + "application/json": external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["MetadataResponse"]; }; }; }; @@ -1729,7 +1966,9 @@ export interface external { /** OK */ 200: { content: { - "application/json": external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["MetadataResponse"]; + "application/json": external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["MetadataResponse"]; }; }; }; @@ -1749,7 +1988,9 @@ export interface external { /** OK */ 200: { content: { - "application/json": external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["MetadataResponse"]; + "application/json": external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["MetadataResponse"]; }; }; }; @@ -1771,7 +2012,9 @@ export interface external { /** OK */ 200: { content: { - "application/json": external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["MetadataResponse"]; + "application/json": external[ + "https://psapi.nrk.no/documentation/openapi/playback/openapi.json" + ]["components"]["schemas"]["MetadataResponse"]; }; }; }; diff --git a/lib/nrk.ts b/lib/nrk.ts index 1a97606..d53e98a 100644 --- a/lib/nrk.ts +++ b/lib/nrk.ts @@ -1,82 +1,95 @@ import { get, OK } from "https://deno.land/x/kall@v0.1.0/mod.ts"; -import { components as searchComponents } from './nrk-search.ts'; -import { components as catalogComponents } from './nrk-catalog.ts'; +import { components as searchComponents } from "./nrk-search.ts"; +import { components as catalogComponents } from "./nrk-catalog.ts"; - -type ArrayElement = A extends readonly (infer T)[] ? T : never -export type Serie = catalogComponents["schemas"]["SeriesViewModel"] +type ArrayElement = A extends readonly (infer T)[] ? T : never; +export type Serie = catalogComponents["schemas"]["SeriesViewModel"]; type OriginalEpisode = catalogComponents["schemas"]["EpisodeHalResource"]; -export type OriginalEpisode = OriginalEpisode & { url: string; } +export type OriginalEpisode = OriginalEpisode & { url: string }; export type SearchResultList = searchComponents["schemas"]["seriesResult"]["results"]; -export type SearchResult = ArrayElement & { description?: string } +export type SearchResult = ArrayElement & { + description?: string; +}; /* Incomplete manifest types */ interface Manifest { - id: string; - playable: { - endSequenceStartTime: null, - duration: string; - assets: { - url: string; - format: string; - mimeType: string; - encrypted: boolean; - }[]; - }; + id: string; + playable: { + endSequenceStartTime: null; + duration: string; + assets: { + url: string; + format: string; + mimeType: string; + encrypted: boolean; + }[]; + }; } -async function withDownloadLink(episode: OriginalEpisode): Promise { - // getting stream link - const [playbackStatus, playbackResponse] = await get(`https://psapi.nrk.no/playback/manifest/podcast/${episode.episodeId}`) +async function withDownloadLink( + episode: OriginalEpisode, +): Promise { + // getting stream link + const [playbackStatus, playbackResponse] = await get( + `https://psapi.nrk.no/playback/manifest/podcast/${episode.episodeId}`, + ); - 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 === OK && playbackResponse) { + return { ...episode, url: playbackResponse.playable.assets[0].url }; + } else { + throw `Error getting downloadLink for ${episode.episodeId}, serie: ${episode.originalTitle}. Status: ${playbackStatus}`; + } } 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 && response) { - return response.results.series?.results; - } 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 && serieResponse?.series && episodeResponse?._embedded.episodes?.length) { - const episodes = await Promise.all(episodeResponse._embedded.episodes.map(episode => withDownloadLink(episode))); - return { - ...serieResponse.series, - episodes, - } - } else { - throw `Error getting episodes for ${seriesId}: EpisodeStatus: ${episodeStatus}. SerieStatus: ${seriesStatus}` - } - - }, - getEpisode: async (seriesId: string, episodeId: string): Promise => { - - - const url = `https://psapi.nrk.no/radio/catalog/podcast/${seriesId}/episodes/${episodeId}` - const [status, episode] = await get(url); - if (status !== OK) { - throw `Error getting episode ${episodeId}. Status: ${status}. Series: ${seriesId}`; - } - - return episode; + search: async (query: string): Promise => { + const [status, response] = await get< + searchComponents["schemas"]["searchresult"] + >(`https://psapi.nrk.no/radio/search/search?q=${query}`); + if (status === OK && response) { + return response.results.series?.results; + } 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 && + serieResponse?.series && episodeResponse?._embedded.episodes?.length + ) { + const episodes = await Promise.all( + episodeResponse._embedded.episodes.map((episode) => withDownloadLink(episode)), + ); + return { + ...serieResponse.series, + episodes, + }; + } else { + throw `Error getting episodes for ${seriesId}: EpisodeStatus: ${episodeStatus}. SerieStatus: ${seriesStatus}`; + } + }, + getEpisode: async ( + seriesId: string, + episodeId: string, + ): Promise => { + const url = `https://psapi.nrk.no/radio/catalog/podcast/${seriesId}/episodes/${episodeId}`; + const [status, episode] = await get(url); + if (status !== OK) { + throw `Error getting episode ${episodeId}. Status: ${status}. Series: ${seriesId}`; + } + + return episode; + }, +}; diff --git a/routes/api/feeds/[seriesId].ts b/routes/api/feeds/[seriesId].ts index c59fa29..731a2b2 100644 --- a/routes/api/feeds/[seriesId].ts +++ b/routes/api/feeds/[seriesId].ts @@ -1,97 +1,102 @@ -import { nrkRadio, Episode } from "../../../lib/nrk.ts" +import { Episode, nrkRadio } from "../../../lib/nrk.ts"; import { HandlerContext } from "$fresh/server.ts"; -import { serialize, tag, declaration } from "https://raw.githubusercontent.com/olaven/serialize-xml/v0.4.0/mod.ts"; +import { declaration, serialize, tag } from "https://raw.githubusercontent.com/olaven/serialize-xml/v0.4.0/mod.ts"; import { getHostName } from "../../../utils.ts"; function toItemTag(seriesId: string, episode: Episode) { - - const description = episode.titles.subtitle || ""; - return tag("item", [ - tag("title", episode.titles.title), - tag("link", episode._links.share.href), - tag("description", description), - tag("itunes:summary", description), - tag("guid", episode.id, [["isPermaLink", "false"]]), - tag("pubDate", new Date(episode.date).toUTCString()), - tag("itunes:duration", episode.durationInSeconds.toString()), - tag("podcast:chapters", "", [ - ["url", `${getHostName()}/api/feeds/${seriesId}/${episode.episodeId}/chapters`], - ["type", "application/json+chapters"], - ]), - tag("enclosure", "", [ - ["url", episode.url], - ["length", episode.durationInSeconds.toString()], - ["type", "audio/mpeg3"], - ]), - ]) + const description = episode.titles.subtitle || ""; + return tag("item", [ + tag("title", episode.titles.title), + tag("link", episode._links.share.href), + tag("description", description), + tag("itunes:summary", description), + tag("guid", episode.id, [["isPermaLink", "false"]]), + tag("pubDate", new Date(episode.date).toUTCString()), + tag("itunes:duration", episode.durationInSeconds.toString()), + tag("podcast:chapters", "", [ + [ + "url", + `${getHostName()}/api/feeds/${seriesId}/${episode.episodeId}/chapters`, + ], + ["type", "application/json+chapters"], + ]), + tag("enclosure", "", [ + ["url", episode.url], + ["length", episode.durationInSeconds.toString()], + ["type", "audio/mpeg3"], + ]), + ]); } async function buildFeed(seriesId: string) { - const serie = await nrkRadio.getSerieData(seriesId) - const imageUrl = serie.squareImage.at(-1)?.url; - const linkValue = `https://radio.nrk.no/podkast/${serie.id}`; + const serie = await nrkRadio.getSerieData(seriesId); + const imageUrl = serie.squareImage.at(-1)?.url; + const linkValue = `https://radio.nrk.no/podkast/${serie.id}`; - // 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.titles.title), - tag("link", linkValue), - tag("itunes:author", "NRK"), - /* serie.category.id does not overlap with Apple's supported categories.. + // 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.titles.title), + tag("link", linkValue), + tag("itunes:author", "NRK"), + /* serie.category.id does not overlap with Apple's supported categories.. These podcast feeds are not going to be indexed in itunes anyways, so a static, valid category is fine. The point is simply to pass third party - podcast feed validation. + podcast feed validation. */ - tag("itunes:category", "", [["text", "Government"]]), - tag("itunes:owner", - [ - tag("itunes:name", "NRK"), - tag("itunes:email", "nrkpodcast@nrk.no") - ] - ), - tag( - "description", - serie.titles.subtitle || "" - ), - tag("ttl", "60"), //60 minutes - ...(imageUrl ? [ - tag("itunes:image", "", [ - ["href", imageUrl], - ]), - tag("image", [ - tag("url", imageUrl), - tag("title", serie.titles.title), - tag("link", linkValue), - ]) - ] : []), - ...serie.episodes.map(episode => toItemTag(seriesId, episode)), - ]), - ], - [ - ["version", "2.0"], - ["xmlns:itunes", "http://www.itunes.com/dtds/podcast-1.0.dtd"], - ["xmlns:content", "http://purl.org/rss/1.0/modules/content/"], - ["xmlns:podcast", "https://podcastindex.org/namespace/1.0"], + tag("itunes:category", "", [["text", "Government"]]), + tag("itunes:owner", [ + tag("itunes:name", "NRK"), + tag("itunes:email", "nrkpodcast@nrk.no"), + ]), + tag( + "description", + serie.titles.subtitle || "", + ), + tag("ttl", "60"), //60 minutes + ...(imageUrl + ? [ + tag("itunes:image", "", [ + ["href", imageUrl], + ]), + tag("image", [ + tag("url", imageUrl), + tag("title", serie.titles.title), + tag("link", linkValue), + ]), ] - ) - ); + : []), + ...serie.episodes.map((episode) => toItemTag(seriesId, episode)), + ]), + ], + [ + ["version", "2.0"], + ["xmlns:itunes", "http://www.itunes.com/dtds/podcast-1.0.dtd"], + ["xmlns:content", "http://purl.org/rss/1.0/modules/content/"], + ["xmlns:podcast", "https://podcastindex.org/namespace/1.0"], + ], + ), + ); } -export const handler = async (req: Request, _ctx: HandlerContext): Promise => { - const seriesId = _ctx.params.seriesId; - const feedContent = await buildFeed(seriesId); +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" - } - }); + return new Response(feedContent, { + headers: { + "Content-Type": "application/xml", + }, + }); }; diff --git a/routes/api/feeds/[seriesId]/[episodeId]/chapters.ts b/routes/api/feeds/[seriesId]/[episodeId]/chapters.ts index 18cd600..55b4879 100644 --- a/routes/api/feeds/[seriesId]/[episodeId]/chapters.ts +++ b/routes/api/feeds/[seriesId]/[episodeId]/chapters.ts @@ -2,26 +2,29 @@ import { parse, toSeconds } from "https://esm.sh/iso8601-duration@2.1.1"; import { nrkRadio, OriginalEpisode } from "../../../../../lib/nrk.ts"; function toChapters(episode: OriginalEpisode) { - return episode.indexPoints.map(indexPoint => ({ - title: indexPoint.title, - startTime: toSeconds(parse(indexPoint.startPoint)) - })) + return episode.indexPoints.map((indexPoint) => ({ + title: indexPoint.title, + startTime: toSeconds(parse(indexPoint.startPoint)), + })); } -export const handler = async (req: Request, _ctx: HandlerContext): Promise => { - const seriesId = _ctx.params.seriesId; - const episodeId = _ctx.params.episodeId; +export const handler = async ( + req: Request, + _ctx: HandlerContext, +): Promise => { + const seriesId = _ctx.params.seriesId; + const episodeId = _ctx.params.episodeId; - console.log("going to get ep for ", episodeId); - const episode = await nrkRadio.getEpisode(seriesId, episodeId); - const chapters = toChapters(episode); - const body = { - version: "1.2.0", - chapters, - } + console.log("going to get ep for ", episodeId); + const episode = await nrkRadio.getEpisode(seriesId, episodeId); + const chapters = toChapters(episode); + const body = { + version: "1.2.0", + chapters, + }; - return new Response(JSON.stringify(body), { - headers: { - "Content-Type": "application/json" - } - }); + return new Response(JSON.stringify(body), { + headers: { + "Content-Type": "application/json", + }, + }); }; diff --git a/routes/index.tsx b/routes/index.tsx index 6772409..1521d06 100644 --- a/routes/index.tsx +++ b/routes/index.tsx @@ -1,49 +1,76 @@ import { Head } from "$fresh/runtime.ts"; import { Handlers, PageProps } from "$fresh/server.ts"; -import Search from "../components/Search.tsx" +import Search from "../components/Search.tsx"; import { SerieCard } from "../components/SeriesCard.tsx"; import { nrkRadio, SearchResult, SearchResultList, Serie } from "../lib/nrk.ts"; interface HandlerData { - query: string; - result?: SearchResultList; - origin: string; + query: string; + result?: SearchResultList; + origin: string; } export const handler: Handlers = { - async GET(request, ctx) { - const url = new URL(request.url); - const query = url.searchParams.get("query"); - let result: SearchResultList | undefined; - if (query) { - result = await nrkRadio.search(query); - } - return ctx.render({ query: query || "", result, origin: url.origin }); - }, + async GET(request, ctx) { + const url = new URL(request.url); + const query = url.searchParams.get("query"); + let result: SearchResultList | undefined; + if (query) { + result = await nrkRadio.search(query); + } + return ctx.render({ query: query || "", result, origin: url.origin }); + }, }; export default function Home({ data }: PageProps) { - return ( - <> - - NRSS - -
-
-

NRSS

-

NRK-podcast som RSS

-
- - {data.result ? ( -
- {data.result.map(result => )} -
- ) : null} -

Hva er dette?

-

Hei! Denne løsningen er laget som en reaksjon på at statsfinansierte NRK lukker ned innholdet sitt til sin egen app fremfor å bygge oppunder åpne standarder som RSS. Denne siden er laget fort og gæli - den er ustabil og kommer til å krasje :) Sjekk gjerne ut kildekoden.

-

Hvordan bruker jeg dette?

-

Søk på NRK-podcasten du vil høre på. Kopier deretter URL-en under bildet. Lim denne inn i akkurat den podcastspilleren du måtte foretrekke! Se her for en oversikt over hvordan det gjøres i populære podcastspillere.

+ return ( + <> + + NRSS + +
+
+

NRSS

+

NRK-podcast som RSS

+
+ + {data.result + ? ( +
+ {data.result.map((result) => ( + + ))}
- - ); + ) + : null} +

Hva er dette?

+

+ Hei! Denne løsningen er laget som en reaksjon på at statsfinansierte NRK lukker ned innholdet sitt til sin + egen app fremfor å bygge oppunder åpne standarder som RSS. Denne siden er laget fort og gæli - den er ustabil + og kommer til å krasje :) Sjekk gjerne ut{" "} + + kildekoden + . +

+

Hvordan bruker jeg dette?

+

+ Søk på NRK-podcasten du vil høre på. Kopier deretter URL-en under bildet. Lim denne inn i akkurat den + podcastspilleren du måtte foretrekke! Se{" "} + + her + {" "} + for en oversikt over hvordan det gjøres i populære podcastspillere. +

+
+ + ); } diff --git a/utils.ts b/utils.ts index 5df399d..654e90a 100644 --- a/utils.ts +++ b/utils.ts @@ -1,11 +1,9 @@ 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'; - } + const deploymentId = Deno.env.get("DENO_DEPLOYMENT_ID"); + if (deploymentId) { + return `https://nrss-${deploymentId}.deno.dev`; + } else { + // assume env + return "http://localhost:8000"; + } } - -