Feature/types #4
|
|
@ -1,17 +1,15 @@
|
||||||
import { Serie } from "../lib/nrk.ts"
|
import { SearchResult } from "../lib/nrk.ts"
|
||||||
import CopyButton from "../islands/CopyButton.tsx";
|
import CopyButton from "../islands/CopyButton.tsx";
|
||||||
|
|
||||||
export function SerieCard(props: { serie: Serie, origin: string }) {
|
export function SerieCard(props: { serie: SearchResult, origin: string }) {
|
||||||
const feedUrl = new URL(`/api/feeds/${props.serie.seriesId}`, props.origin);
|
const feedUrl = new URL(`/api/feeds/${props.serie.seriesId}`, props.origin);
|
||||||
|
const image = props.serie.images[0];
|
||||||
console.log(feedUrl.toString())
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-2 my-2 p-2">
|
<div className="border-2 my-2 p-2">
|
||||||
<div className="mx-auto h-full w-full">
|
<div className="mx-auto h-full w-full">
|
||||||
<h2 className="text-xl">{props.serie.title}</h2>
|
<h2 className="text-xl">{props.serie.title}</h2>
|
||||||
<p className="text-md">{props.serie.description}</p>
|
<p className="text-md">{props.serie.description}</p>
|
||||||
<img src={props.serie.images[0].uri} />
|
<img src={image.uri} width={image.width} />
|
||||||
<CopyButton text={feedUrl.toString()}>
|
<CopyButton text={feedUrl.toString()}>
|
||||||
Kopier URL
|
Kopier URL
|
||||||
</CopyButton>
|
</CopyButton>
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,13 @@
|
||||||
|
|
||||||
import config from "./deno.json" assert { type: "json" };
|
import config from "./deno.json" assert { type: "json" };
|
||||||
import * as $0 from "./routes/api/feeds/[seriesId].ts";
|
import * as $0 from "./routes/api/feeds/[seriesId].ts";
|
||||||
import * as $1 from "./routes/api/search.ts";
|
import * as $1 from "./routes/index.tsx";
|
||||||
import * as $2 from "./routes/index.tsx";
|
|
||||||
import * as $$0 from "./islands/CopyButton.tsx";
|
import * as $$0 from "./islands/CopyButton.tsx";
|
||||||
|
|
||||||
const manifest = {
|
const manifest = {
|
||||||
routes: {
|
routes: {
|
||||||
"./routes/api/feeds/[seriesId].ts": $0,
|
"./routes/api/feeds/[seriesId].ts": $0,
|
||||||
"./routes/api/search.ts": $1,
|
"./routes/index.tsx": $1,
|
||||||
"./routes/index.tsx": $2,
|
|
||||||
},
|
},
|
||||||
islands: {
|
islands: {
|
||||||
"./islands/CopyButton.tsx": $$0,
|
"./islands/CopyButton.tsx": $$0,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,504 @@
|
||||||
|
/**
|
||||||
|
* This file was auto-generated by openapi-typescript.
|
||||||
|
* Do not make direct changes to the file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface paths {
|
||||||
|
"/radio/search/title": {
|
||||||
|
/** Gives results for titles */
|
||||||
|
get: operations["TitleSearch"];
|
||||||
|
};
|
||||||
|
"/radio/search/title/suggest": {
|
||||||
|
/** Gives suggestion for misspelling on titles only */
|
||||||
|
get: operations["TitleSuggestSearch"];
|
||||||
|
};
|
||||||
|
"/radio/search/suggest": {
|
||||||
|
/** Deprecated. Use '/radio/search/title/suggest'. Gives suggestion for misspelling */
|
||||||
|
get: operations["TitleSuggestSearchDeprecated"];
|
||||||
|
};
|
||||||
|
"/radio/search/search": {
|
||||||
|
/** Search endpoint for all content */
|
||||||
|
get: operations["Search"];
|
||||||
|
};
|
||||||
|
"/radio/search/search/suggest": {
|
||||||
|
/** Gives suggestion for misspelling */
|
||||||
|
get: operations["RadioSuggestSearch"];
|
||||||
|
};
|
||||||
|
"/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. */
|
||||||
|
get: operations["RadioListAllForCategory"];
|
||||||
|
};
|
||||||
|
"/radio/search/int/search": {
|
||||||
|
/** Search endpoint for `internal use only`. Searches series and episodes. */
|
||||||
|
get: operations["InternalSearch"];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface components {
|
||||||
|
schemas: {
|
||||||
|
/** Series search result */
|
||||||
|
seriessearchintresult: {
|
||||||
|
id: string;
|
||||||
|
/** @description If type is `customSeason` */
|
||||||
|
seasonId?: string;
|
||||||
|
/** @enum {string} */
|
||||||
|
type: "series" | "podcast" | "customSeason";
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
trimmedDescription?: string;
|
||||||
|
images?: components["schemas"]["image"][];
|
||||||
|
} & {
|
||||||
|
seriesId: unknown;
|
||||||
|
};
|
||||||
|
/** Episode search result */
|
||||||
|
episodesearchintresult: {
|
||||||
|
id: string;
|
||||||
|
seriesId: string;
|
||||||
|
/** @description If type is `customSeasonEpisode` */
|
||||||
|
seasonId?: string;
|
||||||
|
/** @enum {string} */
|
||||||
|
type:
|
||||||
|
| "seriesEpisode"
|
||||||
|
| "podcastEpisode"
|
||||||
|
| "singleProgram"
|
||||||
|
| "customSeasonEpisode";
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
trimmedSubtitle?: string;
|
||||||
|
description?: string;
|
||||||
|
trimmedDescription?: string;
|
||||||
|
images?: components["schemas"]["image"][];
|
||||||
|
/**
|
||||||
|
* Format: date
|
||||||
|
* @description Date in UTC
|
||||||
|
*/
|
||||||
|
date?: string;
|
||||||
|
};
|
||||||
|
searchresult: {
|
||||||
|
/** @description Gives the total count of hits */
|
||||||
|
count: number;
|
||||||
|
takeCount: components["schemas"]["searchTakeCount"];
|
||||||
|
totalCount: components["schemas"]["searchTotalCount"];
|
||||||
|
results: {
|
||||||
|
channels?: components["schemas"]["channelResult"];
|
||||||
|
categories?: components["schemas"]["categoryResult"];
|
||||||
|
series?: components["schemas"]["seriesResult"];
|
||||||
|
episodes?: components["schemas"]["episodeResult"];
|
||||||
|
isSuggestResult?: boolean;
|
||||||
|
/** @description if `isSuggestResult` is true, `suggestQuery` displays the suggested value which is used in the current result set */
|
||||||
|
suggestQuery?: string;
|
||||||
|
};
|
||||||
|
} & {
|
||||||
|
isSuggestResult: unknown;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Channel result
|
||||||
|
* @description Radio channels
|
||||||
|
*/
|
||||||
|
channelResult: {
|
||||||
|
links?: components["schemas"]["nextLink"];
|
||||||
|
results?: {
|
||||||
|
/** @description the radio channel id */
|
||||||
|
id: string;
|
||||||
|
/** @enum {string} */
|
||||||
|
type: "channel";
|
||||||
|
title: string;
|
||||||
|
images: components["schemas"]["image"][];
|
||||||
|
images_1_1?: components["schemas"]["image"][];
|
||||||
|
highlights?: components["schemas"]["titleHighlight"];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Category results
|
||||||
|
* @description Categories that should redirect to a category page
|
||||||
|
*/
|
||||||
|
categoryResult: {
|
||||||
|
links?: components["schemas"]["nextLink"];
|
||||||
|
results?: {
|
||||||
|
/** @description the category id */
|
||||||
|
id: string;
|
||||||
|
/** @enum {string} */
|
||||||
|
type: "category";
|
||||||
|
title: string;
|
||||||
|
images: components["schemas"]["image"][];
|
||||||
|
images_1_1?: components["schemas"]["image"][];
|
||||||
|
highlights?: components["schemas"]["titleHighlight"];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Series results
|
||||||
|
* @description ODM series, podcast series, custom podcast seasons
|
||||||
|
*/
|
||||||
|
seriesResult: {
|
||||||
|
links?: components["schemas"]["nextLink"];
|
||||||
|
results?: {
|
||||||
|
/** @description Unique id */
|
||||||
|
id: string;
|
||||||
|
seriesId: string;
|
||||||
|
/** @description Only included in type `customSeason` */
|
||||||
|
seasonId?: string;
|
||||||
|
/** @enum {string} */
|
||||||
|
type: "series" | "podcast" | "customSeason";
|
||||||
|
title: string;
|
||||||
|
/** @description Only included in type `customSeason` */
|
||||||
|
mainTitle?: string;
|
||||||
|
images: components["schemas"]["image"][];
|
||||||
|
images_1_1?: components["schemas"]["image"][];
|
||||||
|
highlights?: components["schemas"]["seriesHighlight"];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Episode results
|
||||||
|
* @description ODM series episodes, podcast series episodes, single programs, custom podcast seasons episodes
|
||||||
|
*/
|
||||||
|
episodeResult: {
|
||||||
|
links?: components["schemas"]["nextLink"];
|
||||||
|
results?: {
|
||||||
|
/** @description Unique id */
|
||||||
|
id: string;
|
||||||
|
episodeId: string;
|
||||||
|
/** @description Not included in type `singleProgram` */
|
||||||
|
seriesId?: string;
|
||||||
|
/** @description Only included in type `customSeasonEpisode` */
|
||||||
|
seasonId?: string;
|
||||||
|
/** @enum {string} */
|
||||||
|
type:
|
||||||
|
| "seriesEpisode"
|
||||||
|
| "podcastEpisode"
|
||||||
|
| "customSeasonEpisode"
|
||||||
|
| "singleProgram";
|
||||||
|
title: string;
|
||||||
|
/** @description Not included in type `singleProgram` */
|
||||||
|
seriesTitle?: string;
|
||||||
|
images: components["schemas"]["image"][];
|
||||||
|
images_1_1?: components["schemas"]["image"][];
|
||||||
|
/** Format: date */
|
||||||
|
date: string;
|
||||||
|
highlights?: components["schemas"]["episodeHighlight"];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
nextLink: {
|
||||||
|
/**
|
||||||
|
* Format: url
|
||||||
|
* @description If available, the `next` links retrieves more results for the given section
|
||||||
|
*/
|
||||||
|
next?: string;
|
||||||
|
};
|
||||||
|
episodeHighlight: {
|
||||||
|
/** @enum {string} */
|
||||||
|
field?: "title";
|
||||||
|
/** @description The highlighted text. `{` marks the start of the highlight, `}` marks the end of the highlight */
|
||||||
|
text?: string;
|
||||||
|
};
|
||||||
|
seriesHighlight: {
|
||||||
|
/** @enum {string} */
|
||||||
|
field?: "title" | "contributor";
|
||||||
|
/** @description The highlighted text. `{` marks the start of the highlight, `}` marks the end of the highlight */
|
||||||
|
text?: string;
|
||||||
|
};
|
||||||
|
titleHighlight: {
|
||||||
|
/** @enum {string} */
|
||||||
|
field?: "title";
|
||||||
|
/** @description The highlighted text. `{` marks the start of the highlight, `}` marks the end of the highlight */
|
||||||
|
text?: string;
|
||||||
|
};
|
||||||
|
/** @description Gives the count for the current result scope */
|
||||||
|
searchTakeCount: {
|
||||||
|
all: number;
|
||||||
|
series: number;
|
||||||
|
episodes: number;
|
||||||
|
categories: number;
|
||||||
|
channels: number;
|
||||||
|
};
|
||||||
|
/** @description Gives the count for the total matching set of results in the index. */
|
||||||
|
searchTotalCount: {
|
||||||
|
all: number;
|
||||||
|
series: number;
|
||||||
|
episodes: number;
|
||||||
|
categories: number;
|
||||||
|
channels: number;
|
||||||
|
};
|
||||||
|
titlesearchresult: {
|
||||||
|
/** @description Gives the total count of hits for pagination */
|
||||||
|
count?: number;
|
||||||
|
takeCount?: components["schemas"]["takeCount"];
|
||||||
|
totalCount?: components["schemas"]["totalCount"];
|
||||||
|
results?: components["schemas"]["resultobject"][];
|
||||||
|
};
|
||||||
|
resultobject: {
|
||||||
|
/** @description A hashed unique value for the result item */
|
||||||
|
id?: string;
|
||||||
|
/** @description Id of the series */
|
||||||
|
seriesId?: string;
|
||||||
|
/** @description Id of the episode */
|
||||||
|
episodeId?: string;
|
||||||
|
/**
|
||||||
|
* @description `customSeason` and `customSeasonEpisode` are included in `V2` for this endpoint
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
type?:
|
||||||
|
| "podcast"
|
||||||
|
| "series"
|
||||||
|
| "podcastEpisode"
|
||||||
|
| "seriesEpisode"
|
||||||
|
| "singleProgram"
|
||||||
|
| "customSeason"
|
||||||
|
| "customSeasonEpisode";
|
||||||
|
/** @description series title */
|
||||||
|
seriesTitle?: string;
|
||||||
|
/** @description Episode title */
|
||||||
|
episodeTitle?: string;
|
||||||
|
/** @description The series description */
|
||||||
|
seriesDescription?: string;
|
||||||
|
/** @description The episode description */
|
||||||
|
episodeDescription?: string;
|
||||||
|
images?: components["schemas"]["image"][];
|
||||||
|
/** @description First transmission date for episodes */
|
||||||
|
firstTransmissionDate?: string;
|
||||||
|
/** @description Latest transmission date for episodes */
|
||||||
|
lastTransmissionDate?: string;
|
||||||
|
/** @description Indicates if the episode is available on CDN and playable. */
|
||||||
|
playbackAvailable?: boolean;
|
||||||
|
};
|
||||||
|
/** @description Gives the count for the current result scope */
|
||||||
|
takeCount: {
|
||||||
|
all?: number;
|
||||||
|
series?: number;
|
||||||
|
episodes?: number;
|
||||||
|
};
|
||||||
|
/** @description Gives the count for the total matching set of results in the index. */
|
||||||
|
totalCount: {
|
||||||
|
all?: number;
|
||||||
|
series?: number;
|
||||||
|
episodes?: number;
|
||||||
|
};
|
||||||
|
image: {
|
||||||
|
/**
|
||||||
|
* @description The image uri
|
||||||
|
* @example https://gfx.nrk.no/bGUIqql3v_BzrJBHxGnQHwxeHk-T5QHVJpOLdq5zsUFQ.jpg
|
||||||
|
*/
|
||||||
|
uri?: string;
|
||||||
|
/**
|
||||||
|
* @description The image width
|
||||||
|
* @example 300
|
||||||
|
*/
|
||||||
|
width?: number;
|
||||||
|
};
|
||||||
|
/** @description Describe link to other resource */
|
||||||
|
halLink: {
|
||||||
|
/** @description the link */
|
||||||
|
href: string;
|
||||||
|
/** @description Is href a template string. */
|
||||||
|
templated?: boolean;
|
||||||
|
};
|
||||||
|
/** @description Only the link corresponding to the seriesListItem type is set */
|
||||||
|
seriesListItemLinks: {
|
||||||
|
series?: components["schemas"]["halLink"];
|
||||||
|
podcast?: components["schemas"]["halLink"];
|
||||||
|
customSeason?: components["schemas"]["halLink"];
|
||||||
|
singleProgram?: components["schemas"]["halLink"];
|
||||||
|
};
|
||||||
|
serieslistitem: {
|
||||||
|
_links: components["schemas"]["seriesListItemLinks"];
|
||||||
|
id: string;
|
||||||
|
/** @enum {string} */
|
||||||
|
type: "series" | "podcast" | "singleProgram" | "customSeason";
|
||||||
|
/** @description Id for series or podcast. Not set for singleProgram */
|
||||||
|
seriesId?: string;
|
||||||
|
/** @description Id for season. Only set for customSeason */
|
||||||
|
seasonId?: string;
|
||||||
|
title: string;
|
||||||
|
/** @description First character in title. # is used for titles starting on numerals */
|
||||||
|
initialCharacter: string;
|
||||||
|
images: components["schemas"]["image"][];
|
||||||
|
};
|
||||||
|
letterlistitem: {
|
||||||
|
/** @description List of all initialCharacters for series/podcasts/seasons in category. Current page might not include all letters listed here */
|
||||||
|
letter: string;
|
||||||
|
/** @description Number of items for letter */
|
||||||
|
count: number;
|
||||||
|
/** @description Link to paginated page starting at letter */
|
||||||
|
link: string;
|
||||||
|
};
|
||||||
|
categoriesLinks: {
|
||||||
|
nextPage?: components["schemas"]["halLink"];
|
||||||
|
prevPage?: components["schemas"]["halLink"];
|
||||||
|
nextLetter?: components["schemas"]["halLink"];
|
||||||
|
prevLetter?: components["schemas"]["halLink"];
|
||||||
|
};
|
||||||
|
categoriesResponse: {
|
||||||
|
_links: components["schemas"]["categoriesLinks"];
|
||||||
|
letters: components["schemas"]["letterlistitem"][];
|
||||||
|
/** @description Title of category */
|
||||||
|
title: string;
|
||||||
|
series: components["schemas"]["serieslistitem"][];
|
||||||
|
/** @description Total number of items for category and letter if set */
|
||||||
|
totalCount: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface operations {
|
||||||
|
/** Gives results for titles */
|
||||||
|
TitleSearch: {
|
||||||
|
parameters: {
|
||||||
|
header: {
|
||||||
|
accept?: "application/json;api-version=2";
|
||||||
|
};
|
||||||
|
query: {
|
||||||
|
/** The query text */
|
||||||
|
q: string;
|
||||||
|
/** Number of results to take */
|
||||||
|
take?: number;
|
||||||
|
/** Number of results to skip */
|
||||||
|
skip?: number;
|
||||||
|
/** Filters only playable content */
|
||||||
|
filterNotPlayable?: boolean;
|
||||||
|
/** Returns only the specified type */
|
||||||
|
type?: "series" | "episode";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** OK */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["titlesearchresult"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** Gives suggestion for misspelling on titles only */
|
||||||
|
TitleSuggestSearch: {
|
||||||
|
parameters: {
|
||||||
|
query: {
|
||||||
|
/** The query text */
|
||||||
|
q: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** OK */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** Deprecated. Use '/radio/search/title/suggest'. Gives suggestion for misspelling */
|
||||||
|
TitleSuggestSearchDeprecated: {
|
||||||
|
parameters: {
|
||||||
|
query: {
|
||||||
|
/** The query text */
|
||||||
|
q: string;
|
||||||
|
/** Defaults to 10 if not set */
|
||||||
|
take?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** OK */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** Search endpoint for all content */
|
||||||
|
Search: {
|
||||||
|
parameters: {
|
||||||
|
query: {
|
||||||
|
/** The query text */
|
||||||
|
q: string;
|
||||||
|
/** Number of results to take per type in the result set (see type parameter) */
|
||||||
|
take?: number;
|
||||||
|
/** Number of results to skip */
|
||||||
|
skip?: number;
|
||||||
|
/** Which page of results to get */
|
||||||
|
page?: number;
|
||||||
|
/** Filters only playable content, applies to `episodes` and `series` */
|
||||||
|
filterNotPlayable?: boolean;
|
||||||
|
/** Returns only the specified type */
|
||||||
|
type?: "series" | "episode" | "channel" | "category";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** OK */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["searchresult"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** Gives suggestion for misspelling */
|
||||||
|
RadioSuggestSearch: {
|
||||||
|
parameters: {
|
||||||
|
query: {
|
||||||
|
/** The query text */
|
||||||
|
q: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** OK */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** 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: {
|
||||||
|
parameters: {
|
||||||
|
path: {
|
||||||
|
category: string;
|
||||||
|
};
|
||||||
|
query: {
|
||||||
|
take?: number;
|
||||||
|
skip?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** OK */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["categoriesResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** Category not found */
|
||||||
|
404: {
|
||||||
|
content: {
|
||||||
|
"text/plain": unknown;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** Search endpoint for `internal use only`. Searches series and episodes. */
|
||||||
|
InternalSearch: {
|
||||||
|
parameters: {
|
||||||
|
query: {
|
||||||
|
/** The query text */
|
||||||
|
q: string;
|
||||||
|
/** Defaults to 10 if not set */
|
||||||
|
take?: number;
|
||||||
|
/** Defaults to `desc` if not set */
|
||||||
|
sort?: string;
|
||||||
|
/** options `series` or `episode` */
|
||||||
|
type?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** OK */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": (
|
||||||
|
| components["schemas"]["episodesearchintresult"]
|
||||||
|
| components["schemas"]["seriessearchintresult"]
|
||||||
|
)[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface external {}
|
||||||
86
lib/nrk.ts
86
lib/nrk.ts
|
|
@ -1,50 +1,46 @@
|
||||||
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 catalogComponents} from './nrk-catalog.ts';
|
||||||
|
|
||||||
// incomplete type of Series
|
|
||||||
export type Serie = {
|
type ArrayElement<A> = A extends readonly (infer T)[] ? T : never
|
||||||
id: string, //random id
|
export type Serie = catalogComponents["schemas"]["SeriesViewModel"]
|
||||||
seriesId: string, // readable id
|
type OriginalEpisode = catalogComponents["schemas"]["EpisodeHalResource"];
|
||||||
title: string,
|
export type Episode = OriginalEpisode & { url: string; }
|
||||||
description: string,
|
export type SearchResultList = searchComponents["schemas"]["seriesResult"]["results"];
|
||||||
images: any,
|
export type SearchResult = ArrayElement<SearchResultList> & { description?: string }
|
||||||
|
|
||||||
|
/* Incomplete manifest types */
|
||||||
|
interface Manifest {
|
||||||
|
id: string;
|
||||||
|
playable: {
|
||||||
|
endSequenceStartTime: null,
|
||||||
|
duration: string;
|
||||||
|
assets: {
|
||||||
|
url: string;
|
||||||
|
format: string;
|
||||||
|
mimeType: string;
|
||||||
|
encrypted: boolean;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// incomplete type of Episode
|
async function withDownloadLink(episode: OriginalEpisode): Promise<Episode> {
|
||||||
export type Episode = {
|
|
||||||
id: string,
|
|
||||||
episodeId: string,
|
|
||||||
url: string,
|
|
||||||
titles: {
|
|
||||||
title: string,
|
|
||||||
description: string,
|
|
||||||
subtitle: string,
|
|
||||||
},
|
|
||||||
durationInSeconds: number,
|
|
||||||
date: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
async function withDownloadLink(withoutUrl: Omit<Episode, "url">) {
|
|
||||||
|
|
||||||
// getting stream link
|
// getting stream link
|
||||||
const [playbackStatus, playbackResponse] = await get<any>(`https://psapi.nrk.no/playback/manifest/podcast/${withoutUrl.episodeId}`)
|
const [playbackStatus, playbackResponse] = await get<Manifest>(`https://psapi.nrk.no/playback/manifest/podcast/${episode.episodeId}`)
|
||||||
|
if (playbackStatus === OK && playbackResponse) {
|
||||||
if (playbackStatus === OK) {
|
return {...episode, url: playbackResponse.playable.assets[0].url};
|
||||||
|
|
||||||
const url = playbackResponse.playable.assets[0].url;
|
|
||||||
return {
|
|
||||||
...withoutUrl, url
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
throw `Error getting downloadLink for ${withoutUrl.episodeId}`
|
throw `Error getting downloadLink for ${episode.episodeId}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const nrkRadio = {
|
export const nrkRadio = {
|
||||||
search: async (query: string): Promise<Serie[]> => {
|
search: async (query: string): Promise<SearchResultList> => {
|
||||||
const [status, response] = await get<any>(`https://psapi.nrk.no/radio/search/search?q=${query}`);
|
|
||||||
if (status === OK) {
|
const [status, response] = await get<searchComponents["schemas"]["searchresult"]>(`https://psapi.nrk.no/radio/search/search?q=${query}`);
|
||||||
const series = response.results.series.results
|
if (status === OK && response) {
|
||||||
return series as Serie[];
|
return response.results.series?.results;
|
||||||
} else {
|
} else {
|
||||||
throw `Something went wrong with ${query} - got status ${status}`
|
throw `Something went wrong with ${query} - got status ${status}`
|
||||||
}
|
}
|
||||||
|
|
@ -55,23 +51,17 @@ export const nrkRadio = {
|
||||||
[episodeStatus, episodeResponse],
|
[episodeStatus, episodeResponse],
|
||||||
[seriesStatus, serieResponse],
|
[seriesStatus, serieResponse],
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
get<any>(`https://psapi.nrk.no/radio/catalog/podcast/${seriesId}/episodes`),
|
get<catalogComponents["schemas"]["EpisodesHalResource"]>(`https://psapi.nrk.no/radio/catalog/podcast/${seriesId}/episodes`),
|
||||||
get<any>(`https://psapi.nrk.no/radio/catalog/podcast/${seriesId}`)
|
get<catalogComponents["schemas"]["SeriesHalResource"]>(`https://psapi.nrk.no/radio/catalog/podcast/${seriesId}`)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
if (episodeStatus === OK && seriesStatus === OK) {
|
if (episodeStatus === OK && seriesStatus === OK && serieResponse?.series && episodeResponse?._embedded.episodes?.length) {
|
||||||
|
const episodes = await Promise.all(episodeResponse._embedded.episodes.map(episode => withDownloadLink(episode)));
|
||||||
const episodes: Episode[] = await Promise.all(episodeResponse._embedded.episodes.map(withDownloadLink));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: serieResponse.series.id,
|
...serieResponse.series,
|
||||||
title: serieResponse.series.titles.title,
|
|
||||||
subtitle: serieResponse.series.titles.subtitle,
|
|
||||||
image: serieResponse.series.image[1],
|
|
||||||
episodes,
|
episodes,
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
throw `Error getting episodes for ${seriesId}: EpisodeStatus: ${episodeStatus}. SerieStatus: ${seriesStatus}`
|
throw `Error getting episodes for ${seriesId}: EpisodeStatus: ${episodeStatus}. SerieStatus: ${seriesStatus}`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
import { nrkRadio, Serie, Episode } from "../../../lib/nrk.ts"
|
import { nrkRadio, Episode } from "../../../lib/nrk.ts"
|
||||||
import { HandlerContext } from "$fresh/server.ts";
|
import { HandlerContext } from "$fresh/server.ts";
|
||||||
|
|
||||||
import { serialize, tag, declaration } from "https://deno.land/x/serializexml@v0.3.2/mod.ts";
|
import { serialize, tag, declaration } from "https://deno.land/x/serializexml@v0.3.2/mod.ts";
|
||||||
|
|
||||||
function toItemTag(episode: Episode) {
|
function toItemTag(episode: Episode) {
|
||||||
const description = episode.titles.description || episode.titles.subtitle;
|
const description = episode.titles.subtitle || "";
|
||||||
return tag("item", [
|
return tag("item", [
|
||||||
tag("title", episode.titles.title),
|
tag("title", episode.titles.title),
|
||||||
tag("link", episode.url),
|
tag("link", episode.url),
|
||||||
tag("description", description),
|
tag("description", description),
|
||||||
tag("itunes:summary", description),
|
tag("itunes:summary", description),
|
||||||
tag("guid", episode.id, [["isPermaLink", "false"]]),
|
tag("guid", episode.id, [["isPermaLink", "false"]]),
|
||||||
tag("pubDate", episode.date),
|
tag("pubDate", new Date(episode.date).toUTCString()),
|
||||||
|
tag("itunes:duration", episode.durationInSeconds.toString()),
|
||||||
tag("enclosure", "", [
|
tag("enclosure", "", [
|
||||||
["url", episode.url],
|
["url", episode.url],
|
||||||
["length", episode.durationInSeconds.toString()],
|
["length", episode.durationInSeconds.toString()],
|
||||||
|
|
@ -20,8 +21,9 @@ function toItemTag(episode: Episode) {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildFeed(seriesId: any) {
|
async function buildFeed(seriesId: string) {
|
||||||
const serie = await nrkRadio.getSerieData(seriesId)
|
const serie = await nrkRadio.getSerieData(seriesId)
|
||||||
|
const imageUrl = serie.squareImage.at(-1)?.url;
|
||||||
|
|
||||||
// Quickly adapted from https://raw.githubusercontent.com/olaven/paperpod/1cde9abd3174b26e126aa74fc5a3b63fd078c0fd/packages/converter/src/rss.ts
|
// Quickly adapted from https://raw.githubusercontent.com/olaven/paperpod/1cde9abd3174b26e126aa74fc5a3b63fd078c0fd/packages/converter/src/rss.ts
|
||||||
return serialize(
|
return serialize(
|
||||||
|
|
@ -33,20 +35,23 @@ async function buildFeed(seriesId: any) {
|
||||||
"rss",
|
"rss",
|
||||||
[
|
[
|
||||||
tag("channel", [
|
tag("channel", [
|
||||||
tag("title", serie.title),
|
tag("title", serie.titles.title),
|
||||||
tag("link", `https://radio.nrk.no/podkast/${serie.id}`),
|
tag("link", `https://radio.nrk.no/podkast/${serie.id}`),
|
||||||
|
tag("itunes:author", "NRK"),
|
||||||
tag(
|
tag(
|
||||||
"description",
|
"description",
|
||||||
serie.subtitle
|
serie.titles.subtitle || ""
|
||||||
),
|
),
|
||||||
tag("ttl", "60"), //60 minutes
|
tag("ttl", "60"), //60 minutes
|
||||||
tag("itunes:image", "", [
|
...(imageUrl ? [
|
||||||
["href", serie.image.url],
|
tag("itunes:image", "", [
|
||||||
]),
|
["href", imageUrl],
|
||||||
tag("image", [
|
]),
|
||||||
tag("url", serie.image.url),
|
tag("image", [
|
||||||
tag("title", serie.title),
|
tag("url", imageUrl),
|
||||||
]),
|
tag("title", serie.titles.title),
|
||||||
|
])
|
||||||
|
] : []),
|
||||||
...serie.episodes.map(toItemTag),
|
...serie.episodes.map(toItemTag),
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import { nrkRadio } from "../../lib/nrk.ts"
|
|
||||||
import { HandlerContext } from "$fresh/server.ts";
|
|
||||||
|
|
||||||
|
|
||||||
export const handler = async (req: Request, _ctx: HandlerContext): Promise<Response> => {
|
|
||||||
|
|
||||||
|
|
||||||
const { query } = await req.json();
|
|
||||||
const series = await nrkRadio.search(query);
|
|
||||||
|
|
||||||
return new Response(JSON.stringify({ series }), {
|
|
||||||
headers: { "Content-Type": "application/json" }
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
@ -2,11 +2,11 @@ import { Head } from "$fresh/runtime.ts";
|
||||||
import { Handlers, PageProps } from "$fresh/server.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 { SerieCard } from "../components/SeriesCard.tsx";
|
||||||
import { nrkRadio, Serie } from "../lib/nrk.ts";
|
import { nrkRadio, SearchResult, SearchResultList, Serie } from "../lib/nrk.ts";
|
||||||
|
|
||||||
interface HandlerData {
|
interface HandlerData {
|
||||||
query: string;
|
query: string;
|
||||||
result?: Serie[];
|
result?: SearchResultList;
|
||||||
origin: string;
|
origin: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@ export const handler: Handlers<HandlerData> = {
|
||||||
async GET(request, ctx) {
|
async GET(request, ctx) {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const query = url.searchParams.get("query");
|
const query = url.searchParams.get("query");
|
||||||
let result: Serie[] | undefined;
|
let result: SearchResultList | undefined;
|
||||||
if (query) {
|
if (query) {
|
||||||
result = await nrkRadio.search(query);
|
result = await nrkRadio.search(query);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue