Merge remote-tracking branch 'upstream/main' into bugfix-allow-series
Signed-off-by: Tim Hårek Andreassen <tim@harek.no>
This commit is contained in:
commit
d6dacd15ee
22
README.md
22
README.md
|
|
@ -1,19 +1,21 @@
|
||||||
# NRSS - RSS Feeds for NRK sin podcaster
|
# NRSS - RSS feeds for NRK's podcasts
|
||||||
|
|
||||||
https://nrss.deno.dev/
|
Live version: [nrss.deno.dev](https://nrss.deno.dev/)
|
||||||
|
|
||||||
## Local devlopment
|
A webapp built with Deno's [Fresh](https://fresh.deno.dev/) that generates
|
||||||
|
public accessible RSS-feeds for their produced podcasts via their
|
||||||
|
[API](https://psapi.nrk.no/documentation/).
|
||||||
|
|
||||||
install Deno
|
## Local development
|
||||||
https://deno.land/manual/getting_started/installation
|
|
||||||
|
|
||||||
### Run the application
|
1. [Install Deno](https://deno.land/manual/getting_started/installation)
|
||||||
|
1. Run the app: `deno task start`
|
||||||
|
1. Open [localhost:8000](http://localhost:8000) in your browser
|
||||||
|
|
||||||
```shell
|
## What is this?
|
||||||
deno task start
|
|
||||||
```
|
|
||||||
|
|
||||||
Open http://localhost:8000/ to view it in the browser
|
This is made as a reaction that the goverment funded NRK is closing their own
|
||||||
|
content to their own app instead of building under open standards like RSS.
|
||||||
|
|
||||||
## Known Problems
|
## Known Problems
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
"lock": false,
|
"lock": false,
|
||||||
"tasks": {
|
"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"
|
"check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
|
||||||
|
"types:nrk": "deno run -A generate-types.ts && deno fmt ./lib"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"$fresh/": "https://deno.land/x/fresh@1.6.8/",
|
"$fresh/": "https://deno.land/x/fresh@1.6.8/",
|
||||||
|
|
@ -12,7 +13,8 @@
|
||||||
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.2",
|
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.2",
|
||||||
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1",
|
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1",
|
||||||
"twind": "https://esm.sh/twind@0.16.17",
|
"twind": "https://esm.sh/twind@0.16.17",
|
||||||
"twind/": "https://esm.sh/twind@0.16.17/"
|
"twind/": "https://esm.sh/twind@0.16.17/",
|
||||||
|
"openapi-typescript": "npm:openapi-typescript@6.7.5"
|
||||||
},
|
},
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import openapiTS from "openapi-typescript";
|
||||||
|
|
||||||
|
const nrkTypes: { filename: string; url: URL }[] = [
|
||||||
|
{
|
||||||
|
filename: "./lib/nrk-search.ts",
|
||||||
|
url: new URL("https://psapi.nrk.no/documentation/openapi/search-radio/v1/openapi.yml"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filename: "./lib/nrk-catalog.ts",
|
||||||
|
url: new URL("https://psapi.nrk.no/documentation/openapi/programsider-radio/openapi.yml"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filename: "./lib/nrk-playback.ts",
|
||||||
|
url: new URL("https://psapi.nrk.no/documentation/openapi/playback/2.0/openapi.json"),
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
async function generateTypes() {
|
||||||
|
for (const type of nrkTypes) {
|
||||||
|
const contents = await openapiTS(type.url);
|
||||||
|
|
||||||
|
await Deno.writeTextFile(new URL(type.filename, import.meta.url), contents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
await generateTypes();
|
||||||
|
}
|
||||||
1914
lib/nrk-catalog.ts
1914
lib/nrk-catalog.ts
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -5,35 +5,59 @@
|
||||||
|
|
||||||
export interface paths {
|
export interface paths {
|
||||||
"/radio/search/title": {
|
"/radio/search/title": {
|
||||||
/** Gives results for titles */
|
/**
|
||||||
|
* Title search
|
||||||
|
* @description Gives results for titles
|
||||||
|
*/
|
||||||
get: operations["TitleSearch"];
|
get: operations["TitleSearch"];
|
||||||
};
|
};
|
||||||
"/radio/search/title/suggest": {
|
"/radio/search/title/suggest": {
|
||||||
/** Gives suggestion for misspelling on titles only */
|
/**
|
||||||
|
* Title suggest search
|
||||||
|
* @description Gives suggestion for misspelling on titles only
|
||||||
|
*/
|
||||||
get: operations["TitleSuggestSearch"];
|
get: operations["TitleSuggestSearch"];
|
||||||
};
|
};
|
||||||
"/radio/search/suggest": {
|
"/radio/search/suggest": {
|
||||||
/** Deprecated. Use '/radio/search/title/suggest'. Gives suggestion for misspelling */
|
/**
|
||||||
|
* Deprecated
|
||||||
|
* @deprecated
|
||||||
|
* @description Deprecated. Use '/radio/search/title/suggest'. Gives suggestion for misspelling
|
||||||
|
*/
|
||||||
get: operations["TitleSuggestSearchDeprecated"];
|
get: operations["TitleSuggestSearchDeprecated"];
|
||||||
};
|
};
|
||||||
"/radio/search/search": {
|
"/radio/search/search": {
|
||||||
/** Search endpoint for all content */
|
/**
|
||||||
|
* Search
|
||||||
|
* @description Search endpoint for all content
|
||||||
|
*/
|
||||||
get: operations["Search"];
|
get: operations["Search"];
|
||||||
};
|
};
|
||||||
"/radio/search/search/suggest": {
|
"/radio/search/search/suggest": {
|
||||||
/** Gives suggestion for misspelling */
|
/**
|
||||||
|
* Search suggest search
|
||||||
|
* @description Gives suggestion for misspelling
|
||||||
|
*/
|
||||||
get: operations["RadioSuggestSearch"];
|
get: operations["RadioSuggestSearch"];
|
||||||
};
|
};
|
||||||
"/radio/search/categories/{category}": {
|
"/radio/search/categories/{category}": {
|
||||||
/** Alphabetical listing of all series, podcasts and umbrella seasons that are not excluded from search results in given category. Categories correspond to those in pages-api, including 'alt-innhold'. For the category 'podcast', all podcasts are listed, also those excluded from search results. */
|
/**
|
||||||
|
* List all series, podcast and umbrella seasons in category
|
||||||
|
* @description Alphabetical listing of all series, podcasts and umbrella seasons that are not excluded from search results in given category. Categories correspond to those in pages-api, including 'alt-innhold'. For the category 'podcast', all podcasts are listed, also those excluded from search results.
|
||||||
|
*/
|
||||||
get: operations["RadioListAllForCategory"];
|
get: operations["RadioListAllForCategory"];
|
||||||
};
|
};
|
||||||
"/radio/search/int/search": {
|
"/radio/search/int/search": {
|
||||||
/** Search endpoint for `internal use only`. Searches series and episodes. */
|
/**
|
||||||
|
* Internal search
|
||||||
|
* @description Search endpoint for `internal use only`. Searches series and episodes.
|
||||||
|
*/
|
||||||
get: operations["InternalSearch"];
|
get: operations["InternalSearch"];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type webhooks = Record<string, never>;
|
||||||
|
|
||||||
export interface components {
|
export interface components {
|
||||||
schemas: {
|
schemas: {
|
||||||
/** Series search result */
|
/** Series search result */
|
||||||
|
|
@ -47,8 +71,6 @@ export interface components {
|
||||||
description?: string;
|
description?: string;
|
||||||
trimmedDescription?: string;
|
trimmedDescription?: string;
|
||||||
images?: components["schemas"]["image"][];
|
images?: components["schemas"]["image"][];
|
||||||
} & {
|
|
||||||
seriesId: unknown;
|
|
||||||
};
|
};
|
||||||
/** Episode search result */
|
/** Episode search result */
|
||||||
episodesearchintresult: {
|
episodesearchintresult: {
|
||||||
|
|
@ -57,11 +79,7 @@ export interface components {
|
||||||
/** @description If type is `customSeasonEpisode` */
|
/** @description If type is `customSeasonEpisode` */
|
||||||
seasonId?: string;
|
seasonId?: string;
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type:
|
type: "seriesEpisode" | "podcastEpisode" | "singleProgram" | "customSeasonEpisode";
|
||||||
| "seriesEpisode"
|
|
||||||
| "podcastEpisode"
|
|
||||||
| "singleProgram"
|
|
||||||
| "customSeasonEpisode";
|
|
||||||
title: string;
|
title: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
trimmedSubtitle?: string;
|
trimmedSubtitle?: string;
|
||||||
|
|
@ -88,8 +106,6 @@ export interface components {
|
||||||
/** @description if `isSuggestResult` is true, `suggestQuery` displays the suggested value which is used in the current result set */
|
/** @description if `isSuggestResult` is true, `suggestQuery` displays the suggested value which is used in the current result set */
|
||||||
suggestQuery?: string;
|
suggestQuery?: string;
|
||||||
};
|
};
|
||||||
} & {
|
|
||||||
isSuggestResult: unknown;
|
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Channel result
|
* Channel result
|
||||||
|
|
@ -131,7 +147,7 @@ export interface components {
|
||||||
*/
|
*/
|
||||||
seriesResult: {
|
seriesResult: {
|
||||||
links?: components["schemas"]["nextLink"];
|
links?: components["schemas"]["nextLink"];
|
||||||
results?: {
|
results?: ({
|
||||||
/** @description Unique id */
|
/** @description Unique id */
|
||||||
id: string;
|
id: string;
|
||||||
seriesId: string;
|
seriesId: string;
|
||||||
|
|
@ -145,7 +161,7 @@ export interface components {
|
||||||
images: components["schemas"]["image"][];
|
images: components["schemas"]["image"][];
|
||||||
images_1_1?: components["schemas"]["image"][];
|
images_1_1?: components["schemas"]["image"][];
|
||||||
highlights?: components["schemas"]["seriesHighlight"];
|
highlights?: components["schemas"]["seriesHighlight"];
|
||||||
}[];
|
})[];
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Episode results
|
* Episode results
|
||||||
|
|
@ -153,7 +169,7 @@ export interface components {
|
||||||
*/
|
*/
|
||||||
episodeResult: {
|
episodeResult: {
|
||||||
links?: components["schemas"]["nextLink"];
|
links?: components["schemas"]["nextLink"];
|
||||||
results?: {
|
results?: ({
|
||||||
/** @description Unique id */
|
/** @description Unique id */
|
||||||
id: string;
|
id: string;
|
||||||
episodeId: string;
|
episodeId: string;
|
||||||
|
|
@ -162,11 +178,7 @@ export interface components {
|
||||||
/** @description Only included in type `customSeasonEpisode` */
|
/** @description Only included in type `customSeasonEpisode` */
|
||||||
seasonId?: string;
|
seasonId?: string;
|
||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type:
|
type: "seriesEpisode" | "podcastEpisode" | "customSeasonEpisode" | "singleProgram";
|
||||||
| "seriesEpisode"
|
|
||||||
| "podcastEpisode"
|
|
||||||
| "customSeasonEpisode"
|
|
||||||
| "singleProgram";
|
|
||||||
title: string;
|
title: string;
|
||||||
/** @description Not included in type `singleProgram` */
|
/** @description Not included in type `singleProgram` */
|
||||||
seriesTitle?: string;
|
seriesTitle?: string;
|
||||||
|
|
@ -175,7 +187,7 @@ export interface components {
|
||||||
/** Format: date */
|
/** Format: date */
|
||||||
date: string;
|
date: string;
|
||||||
highlights?: components["schemas"]["episodeHighlight"];
|
highlights?: components["schemas"]["episodeHighlight"];
|
||||||
}[];
|
})[];
|
||||||
};
|
};
|
||||||
nextLink: {
|
nextLink: {
|
||||||
/**
|
/**
|
||||||
|
|
@ -336,30 +348,48 @@ export interface components {
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
responses: never;
|
||||||
|
parameters: never;
|
||||||
|
requestBodies: never;
|
||||||
|
headers: never;
|
||||||
|
pathItems: never;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type $defs = Record<string, never>;
|
||||||
|
|
||||||
|
export type external = Record<string, never>;
|
||||||
|
|
||||||
export interface operations {
|
export interface operations {
|
||||||
/** Gives results for titles */
|
/**
|
||||||
|
* Title search
|
||||||
|
* @description Gives results for titles
|
||||||
|
*/
|
||||||
TitleSearch: {
|
TitleSearch: {
|
||||||
parameters: {
|
parameters: {
|
||||||
header: {
|
|
||||||
accept?: "application/json;api-version=2";
|
|
||||||
};
|
|
||||||
query: {
|
query: {
|
||||||
/** The query text */
|
/**
|
||||||
|
* @description The query text
|
||||||
|
* @example stanghelle
|
||||||
|
*/
|
||||||
q: string;
|
q: string;
|
||||||
/** Number of results to take */
|
/** @description Number of results to take */
|
||||||
take?: number;
|
take?: number;
|
||||||
/** Number of results to skip */
|
/**
|
||||||
|
* @description Number of results to skip
|
||||||
|
* @example 0
|
||||||
|
*/
|
||||||
skip?: number;
|
skip?: number;
|
||||||
/** Filters only playable content */
|
/** @description Filters only playable content */
|
||||||
filterNotPlayable?: boolean;
|
filterNotPlayable?: boolean;
|
||||||
/** Returns only the specified type */
|
/** @description Returns only the specified type */
|
||||||
type?: "series" | "episode";
|
type?: "series" | "episode";
|
||||||
};
|
};
|
||||||
|
header?: {
|
||||||
|
accept?: "application/json;api-version=2";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
/** OK */
|
/** @description OK */
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["titlesearchresult"];
|
"application/json": components["schemas"]["titlesearchresult"];
|
||||||
|
|
@ -367,16 +397,22 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** Gives suggestion for misspelling on titles only */
|
/**
|
||||||
|
* Title suggest search
|
||||||
|
* @description Gives suggestion for misspelling on titles only
|
||||||
|
*/
|
||||||
TitleSuggestSearch: {
|
TitleSuggestSearch: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query: {
|
query: {
|
||||||
/** The query text */
|
/**
|
||||||
|
* @description The query text
|
||||||
|
* @example smukker
|
||||||
|
*/
|
||||||
q: string;
|
q: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
/** OK */
|
/** @description OK */
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": string[];
|
"application/json": string[];
|
||||||
|
|
@ -384,18 +420,28 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** Deprecated. Use '/radio/search/title/suggest'. Gives suggestion for misspelling */
|
/**
|
||||||
|
* Deprecated
|
||||||
|
* @deprecated
|
||||||
|
* @description Deprecated. Use '/radio/search/title/suggest'. Gives suggestion for misspelling
|
||||||
|
*/
|
||||||
TitleSuggestSearchDeprecated: {
|
TitleSuggestSearchDeprecated: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query: {
|
query: {
|
||||||
/** The query text */
|
/**
|
||||||
|
* @description The query text
|
||||||
|
* @example stanghelle
|
||||||
|
*/
|
||||||
q: string;
|
q: string;
|
||||||
/** Defaults to 10 if not set */
|
/**
|
||||||
|
* @description Defaults to 10 if not set
|
||||||
|
* @example 10
|
||||||
|
*/
|
||||||
take?: number;
|
take?: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
/** OK */
|
/** @description OK */
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": string[];
|
"application/json": string[];
|
||||||
|
|
@ -403,26 +449,38 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** Search endpoint for all content */
|
/**
|
||||||
|
* Search
|
||||||
|
* @description Search endpoint for all content
|
||||||
|
*/
|
||||||
Search: {
|
Search: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query: {
|
query: {
|
||||||
/** The query text */
|
/**
|
||||||
|
* @description The query text
|
||||||
|
* @example stanghelle
|
||||||
|
*/
|
||||||
q: string;
|
q: string;
|
||||||
/** Number of results to take per type in the result set (see type parameter) */
|
/** @description Number of results to take per type in the result set (see type parameter) */
|
||||||
take?: number;
|
take?: number;
|
||||||
/** Number of results to skip */
|
/**
|
||||||
|
* @description Number of results to skip
|
||||||
|
* @example 0
|
||||||
|
*/
|
||||||
skip?: number;
|
skip?: number;
|
||||||
/** Which page of results to get */
|
/**
|
||||||
|
* @description Which page of results to get
|
||||||
|
* @example 2
|
||||||
|
*/
|
||||||
page?: number;
|
page?: number;
|
||||||
/** Filters only playable content, applies to `episodes` and `series` */
|
/** @description Filters only playable content, applies to `episodes` and `series` */
|
||||||
filterNotPlayable?: boolean;
|
filterNotPlayable?: boolean;
|
||||||
/** Returns only the specified type */
|
/** @description Returns only the specified type */
|
||||||
type?: "series" | "episode" | "channel" | "category";
|
type?: "series" | "episode" | "channel" | "category";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
/** OK */
|
/** @description OK */
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["searchresult"];
|
"application/json": components["schemas"]["searchresult"];
|
||||||
|
|
@ -430,16 +488,22 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** Gives suggestion for misspelling */
|
/**
|
||||||
|
* Search suggest search
|
||||||
|
* @description Gives suggestion for misspelling
|
||||||
|
*/
|
||||||
RadioSuggestSearch: {
|
RadioSuggestSearch: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query: {
|
query: {
|
||||||
/** The query text */
|
/**
|
||||||
|
* @description The query text
|
||||||
|
* @example smukker
|
||||||
|
*/
|
||||||
q: string;
|
q: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
/** OK */
|
/** @description OK */
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": string[];
|
"application/json": string[];
|
||||||
|
|
@ -447,25 +511,28 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** Alphabetical listing of all series, podcasts and umbrella seasons that are not excluded from search results in given category. Categories correspond to those in pages-api, including 'alt-innhold'. For the category 'podcast', all podcasts are listed, also those excluded from search results. */
|
/**
|
||||||
|
* List all series, podcast and umbrella seasons in category
|
||||||
|
* @description Alphabetical listing of all series, podcasts and umbrella seasons that are not excluded from search results in given category. Categories correspond to those in pages-api, including 'alt-innhold'. For the category 'podcast', all podcasts are listed, also those excluded from search results.
|
||||||
|
*/
|
||||||
RadioListAllForCategory: {
|
RadioListAllForCategory: {
|
||||||
parameters: {
|
parameters: {
|
||||||
path: {
|
query?: {
|
||||||
category: string;
|
|
||||||
};
|
|
||||||
query: {
|
|
||||||
take?: number;
|
take?: number;
|
||||||
skip?: number;
|
skip?: number;
|
||||||
};
|
};
|
||||||
|
path: {
|
||||||
|
category: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
/** OK */
|
/** @description OK */
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["categoriesResponse"];
|
"application/json": components["schemas"]["categoriesResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** Category not found */
|
/** @description Category not found */
|
||||||
404: {
|
404: {
|
||||||
content: {
|
content: {
|
||||||
"text/plain": unknown;
|
"text/plain": unknown;
|
||||||
|
|
@ -473,28 +540,41 @@ export interface operations {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** Search endpoint for `internal use only`. Searches series and episodes. */
|
/**
|
||||||
|
* Internal search
|
||||||
|
* @description Search endpoint for `internal use only`. Searches series and episodes.
|
||||||
|
*/
|
||||||
InternalSearch: {
|
InternalSearch: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query: {
|
query: {
|
||||||
/** The query text */
|
/**
|
||||||
|
* @description The query text
|
||||||
|
* @example stanghelle
|
||||||
|
*/
|
||||||
q: string;
|
q: string;
|
||||||
/** Defaults to 10 if not set */
|
/**
|
||||||
|
* @description Defaults to 10 if not set
|
||||||
|
* @example 10
|
||||||
|
*/
|
||||||
take?: number;
|
take?: number;
|
||||||
/** Defaults to `desc` if not set */
|
/**
|
||||||
|
* @description Defaults to `desc` if not set
|
||||||
|
* @example asc
|
||||||
|
*/
|
||||||
sort?: string;
|
sort?: string;
|
||||||
/** options `series` or `episode` */
|
/**
|
||||||
|
* @description options `series` or `episode`
|
||||||
|
* @example series
|
||||||
|
*/
|
||||||
type?: string;
|
type?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
/** OK */
|
/** @description OK */
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": (
|
"application/json":
|
||||||
| components["schemas"]["episodesearchintresult"]
|
(components["schemas"]["episodesearchintresult"] | components["schemas"]["seriessearchintresult"])[];
|
||||||
| components["schemas"]["seriessearchintresult"]
|
|
||||||
)[];
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
58
lib/nrk.ts
58
lib/nrk.ts
|
|
@ -1,43 +1,37 @@
|
||||||
import { get, OK } from "https://deno.land/x/kall@v0.1.0/mod.ts";
|
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 searchComponents } from "./nrk-search.ts";
|
||||||
import { components as catalogComponents } from "./nrk-catalog.ts";
|
import { components as catalogComponents } from "./nrk-catalog.ts";
|
||||||
|
import { external as playbackComponents } from "./nrk-playback.ts";
|
||||||
|
|
||||||
type ArrayElement<A> = A extends readonly (infer T)[] ? T : never;
|
type ArrayElement<A> = A extends readonly (infer T)[] ? T : never;
|
||||||
|
type PodcastEpisodes = catalogComponents["schemas"]["EpisodesHalResource"];
|
||||||
|
type Podcast = catalogComponents["schemas"]["SeriesHalResource"];
|
||||||
|
type PodcastEpisodesSingle = catalogComponents["schemas"]["EpisodeHalResource"];
|
||||||
|
|
||||||
export type Serie = catalogComponents["schemas"]["SeriesViewModel"];
|
export type Serie = catalogComponents["schemas"]["SeriesViewModel"];
|
||||||
type _OriginalEpisode = catalogComponents["schemas"]["EpisodeHalResource"];
|
export type OriginalEpisode = PodcastEpisodesSingle & { url: string };
|
||||||
export type OriginalEpisode = _OriginalEpisode & { url: string };
|
export type PodcastEpisode = catalogComponents["schemas"]["PodcastEpisodeHalResource"];
|
||||||
export type SearchResultList = searchComponents["schemas"]["seriesResult"]["results"];
|
export type SearchResultList = searchComponents["schemas"]["seriesResult"]["results"];
|
||||||
export type SearchResult = ArrayElement<SearchResultList> & {
|
export type SearchResult = ArrayElement<SearchResultList> & {
|
||||||
description?: string;
|
description?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Incomplete manifest types */
|
type Manifest = playbackComponents["schemas/playback-channel.json"]["components"]["schemas"]["PlayableManifest"];
|
||||||
interface Manifest {
|
|
||||||
id: string;
|
const nrkAPI = `https://psapi.nrk.no`;
|
||||||
playable: {
|
|
||||||
endSequenceStartTime: null;
|
|
||||||
duration: string;
|
|
||||||
assets: {
|
|
||||||
url: string;
|
|
||||||
format: string;
|
|
||||||
mimeType: string;
|
|
||||||
encrypted: boolean;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function withDownloadLink(
|
async function withDownloadLink(
|
||||||
episode: _OriginalEpisode,
|
episode: PodcastEpisodesSingle,
|
||||||
type: catalogComponents["schemas"]["Type"],
|
type: catalogComponents["schemas"]["Type"],
|
||||||
): Promise<OriginalEpisode> {
|
): Promise<OriginalEpisode> {
|
||||||
// getting stream link
|
// getting stream link
|
||||||
let [playbackStatus, playbackResponse] = await get<Manifest>(
|
let [playbackStatus, playbackResponse] = await get<Manifest>(
|
||||||
`https://psapi.nrk.no/playback/manifest/podcast/${episode.episodeId}`,
|
`${nrkAPI}/playback/manifest/podcast/${episode.episodeId}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (type === "series") {
|
if (type === "series") {
|
||||||
[playbackStatus, playbackResponse] = await get<Manifest>(
|
[playbackStatus, playbackResponse] = await get<Manifest>(
|
||||||
`https://psapi.nrk.no/playback/manifest/program/${episode.episodeId}`,
|
`${nrkAPI}/playback/manifest/program/${episode.episodeId}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,23 +46,22 @@ export const nrkRadio = {
|
||||||
search: async (query: string): Promise<SearchResultList> => {
|
search: async (query: string): Promise<SearchResultList> => {
|
||||||
const [status, response] = await get<
|
const [status, response] = await get<
|
||||||
searchComponents["schemas"]["searchresult"]
|
searchComponents["schemas"]["searchresult"]
|
||||||
>(`https://psapi.nrk.no/radio/search/search?q=${query}`);
|
>(`${nrkAPI}/radio/search/search?q=${query}`);
|
||||||
if (status === OK && response) {
|
if (status === OK && response) {
|
||||||
return response.results.series?.results;
|
return response.results.series?.results;
|
||||||
} else {
|
|
||||||
throw `Something went wrong with ${query} - got status ${status}`;
|
|
||||||
}
|
}
|
||||||
|
throw `Something went wrong with ${query} - got status ${status}`;
|
||||||
},
|
},
|
||||||
getSerieData: async (seriesId: string) => {
|
getSerieData: async (seriesId: string) => {
|
||||||
let [
|
let [
|
||||||
[episodeStatus, episodeResponse],
|
[episodeStatus, episodeResponse],
|
||||||
[seriesStatus, serieResponse],
|
[seriesStatus, serieResponse],
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
get<catalogComponents["schemas"]["EpisodesHalResource"]>(
|
get<PodcastEpisodes>(
|
||||||
`https://psapi.nrk.no/radio/catalog/podcast/${seriesId}/episodes`,
|
`${nrkAPI}/radio/catalog/podcast/${seriesId}/episodes`,
|
||||||
),
|
),
|
||||||
get<catalogComponents["schemas"]["SeriesHalResource"]>(
|
get<Podcast>(
|
||||||
`https://psapi.nrk.no/radio/catalog/podcast/${seriesId}`,
|
`${nrkAPI}/radio/catalog/podcast/${seriesId}`,
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -100,13 +93,12 @@ export const nrkRadio = {
|
||||||
}
|
}
|
||||||
throw `Error getting episodes for ${seriesId}: EpisodeStatus: ${episodeStatus}. SerieStatus: ${seriesStatus}`;
|
throw `Error getting episodes for ${seriesId}: EpisodeStatus: ${episodeStatus}. SerieStatus: ${seriesStatus}`;
|
||||||
},
|
},
|
||||||
getEpisode: async (seriesId: string, episodeId: string): Promise<OriginalEpisode | null> => {
|
getEpisode: async (seriesId: string, episodeId: string): Promise<PodcastEpisode | null> => {
|
||||||
const url = `https://psapi.nrk.no/radio/catalog/podcast/${seriesId}/episodes/${episodeId}`;
|
const url = `${nrkAPI}/radio/catalog/podcast/${seriesId}/episodes/${episodeId}`;
|
||||||
const [status, episode] = await get<OriginalEpisode>(url);
|
const [status, episode] = await get<PodcastEpisode>(url);
|
||||||
if (status !== OK) {
|
if (status === OK && episode) {
|
||||||
throw `Error getting episode ${episodeId}. Status: ${status}. Series: ${seriesId}`;
|
return episode;
|
||||||
}
|
}
|
||||||
|
throw `Error getting episode ${episodeId}. Status: ${status}. Series: ${seriesId}`;
|
||||||
return episode;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,19 @@
|
||||||
import { FreshContext } from "$fresh/server.ts";
|
import { FreshContext } from "$fresh/server.ts";
|
||||||
import { parse, toSeconds } from "https://esm.sh/iso8601-duration@2.1.1";
|
import { parse, toSeconds } from "https://esm.sh/iso8601-duration@2.1.1";
|
||||||
import { nrkRadio, OriginalEpisode } from "../../../../../lib/nrk.ts";
|
import { nrkRadio, PodcastEpisode } from "../../../../../lib/nrk.ts";
|
||||||
|
|
||||||
// TODO: This type `OriginalEpisode` is missing stuff.
|
type Chapter = {
|
||||||
function toChapters(episode: OriginalEpisode) {
|
title: string | undefined;
|
||||||
|
startTime: number | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
function toChapters(episode: PodcastEpisode): Chapter[] | null {
|
||||||
|
if (!episode.indexPoints) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return episode.indexPoints.map((indexPoint) => ({
|
return episode.indexPoints.map((indexPoint) => ({
|
||||||
title: indexPoint.title,
|
title: indexPoint.title,
|
||||||
startTime: toSeconds(parse(indexPoint.startPoint)),
|
startTime: indexPoint.startPoint ? toSeconds(parse(indexPoint.startPoint)) : undefined,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue