Feature/types (#4)
This commit is contained in:
parent
5d1dc5ced2
commit
500f1532b5
|
|
@ -1,17 +1,15 @@
|
|||
import { Serie } from "../lib/nrk.ts"
|
||||
import { SearchResult } from "../lib/nrk.ts"
|
||||
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);
|
||||
|
||||
console.log(feedUrl.toString())
|
||||
|
||||
const image = props.serie.images[0];
|
||||
return (
|
||||
<div className="border-2 my-2 p-2">
|
||||
<div className="mx-auto h-full w-full">
|
||||
<h2 className="text-xl">{props.serie.title}</h2>
|
||||
<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()}>
|
||||
Kopier URL
|
||||
</CopyButton>
|
||||
|
|
|
|||
|
|
@ -4,15 +4,13 @@
|
|||
|
||||
import config from "./deno.json" assert { type: "json" };
|
||||
import * as $0 from "./routes/api/feeds/[seriesId].ts";
|
||||
import * as $1 from "./routes/api/search.ts";
|
||||
import * as $2 from "./routes/index.tsx";
|
||||
import * as $1 from "./routes/index.tsx";
|
||||
import * as $$0 from "./islands/CopyButton.tsx";
|
||||
|
||||
const manifest = {
|
||||
routes: {
|
||||
"./routes/api/feeds/[seriesId].ts": $0,
|
||||
"./routes/api/search.ts": $1,
|
||||
"./routes/index.tsx": $2,
|
||||
"./routes/index.tsx": $1,
|
||||
},
|
||||
islands: {
|
||||
"./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 {components as searchComponents} from './nrk-search.ts';
|
||||
import {components as catalogComponents} from './nrk-catalog.ts';
|
||||
|
||||
// incomplete type of Series
|
||||
export type Serie = {
|
||||
id: string, //random id
|
||||
seriesId: string, // readable id
|
||||
title: string,
|
||||
description: string,
|
||||
images: any,
|
||||
|
||||
type ArrayElement<A> = A extends readonly (infer T)[] ? T : never
|
||||
export type Serie = catalogComponents["schemas"]["SeriesViewModel"]
|
||||
type OriginalEpisode = catalogComponents["schemas"]["EpisodeHalResource"];
|
||||
export type Episode = OriginalEpisode & { url: string; }
|
||||
export type SearchResultList = searchComponents["schemas"]["seriesResult"]["results"];
|
||||
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
|
||||
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">) {
|
||||
|
||||
async function withDownloadLink(episode: OriginalEpisode): Promise<Episode> {
|
||||
// getting stream link
|
||||
const [playbackStatus, playbackResponse] = await get<any>(`https://psapi.nrk.no/playback/manifest/podcast/${withoutUrl.episodeId}`)
|
||||
|
||||
if (playbackStatus === OK) {
|
||||
|
||||
const url = playbackResponse.playable.assets[0].url;
|
||||
return {
|
||||
...withoutUrl, url
|
||||
}
|
||||
const [playbackStatus, playbackResponse] = await get<Manifest>(`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 ${withoutUrl.episodeId}`
|
||||
throw `Error getting downloadLink for ${episode.episodeId}`
|
||||
}
|
||||
}
|
||||
|
||||
export const nrkRadio = {
|
||||
search: async (query: string): Promise<Serie[]> => {
|
||||
const [status, response] = await get<any>(`https://psapi.nrk.no/radio/search/search?q=${query}`);
|
||||
if (status === OK) {
|
||||
const series = response.results.series.results
|
||||
return series as Serie[];
|
||||
search: async (query: string): Promise<SearchResultList> => {
|
||||
|
||||
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}`
|
||||
}
|
||||
|
|
@ -55,23 +51,17 @@ export const nrkRadio = {
|
|||
[episodeStatus, episodeResponse],
|
||||
[seriesStatus, serieResponse],
|
||||
] = await Promise.all([
|
||||
get<any>(`https://psapi.nrk.no/radio/catalog/podcast/${seriesId}/episodes`),
|
||||
get<any>(`https://psapi.nrk.no/radio/catalog/podcast/${seriesId}`)
|
||||
get<catalogComponents["schemas"]["EpisodesHalResource"]>(`https://psapi.nrk.no/radio/catalog/podcast/${seriesId}/episodes`),
|
||||
get<catalogComponents["schemas"]["SeriesHalResource"]>(`https://psapi.nrk.no/radio/catalog/podcast/${seriesId}`)
|
||||
]);
|
||||
|
||||
|
||||
if (episodeStatus === OK && seriesStatus === OK) {
|
||||
|
||||
const episodes: Episode[] = await Promise.all(episodeResponse._embedded.episodes.map(withDownloadLink));
|
||||
|
||||
if (episodeStatus === OK && seriesStatus === OK && serieResponse?.series && episodeResponse?._embedded.episodes?.length) {
|
||||
const episodes = await Promise.all(episodeResponse._embedded.episodes.map(episode => withDownloadLink(episode)));
|
||||
return {
|
||||
id: serieResponse.series.id,
|
||||
title: serieResponse.series.titles.title,
|
||||
subtitle: serieResponse.series.titles.subtitle,
|
||||
image: serieResponse.series.image[1],
|
||||
...serieResponse.series,
|
||||
episodes,
|
||||
}
|
||||
|
||||
} else {
|
||||
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 { serialize, tag, declaration } from "https://deno.land/x/serializexml@v0.3.2/mod.ts";
|
||||
|
||||
function toItemTag(episode: Episode) {
|
||||
const description = episode.titles.description || episode.titles.subtitle;
|
||||
const description = episode.titles.subtitle || "";
|
||||
return tag("item", [
|
||||
tag("title", episode.titles.title),
|
||||
tag("link", episode.url),
|
||||
tag("description", description),
|
||||
tag("itunes:summary", description),
|
||||
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", "", [
|
||||
["url", episode.url],
|
||||
["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 imageUrl = serie.squareImage.at(-1)?.url;
|
||||
|
||||
// Quickly adapted from https://raw.githubusercontent.com/olaven/paperpod/1cde9abd3174b26e126aa74fc5a3b63fd078c0fd/packages/converter/src/rss.ts
|
||||
return serialize(
|
||||
|
|
@ -33,20 +35,23 @@ async function buildFeed(seriesId: any) {
|
|||
"rss",
|
||||
[
|
||||
tag("channel", [
|
||||
tag("title", serie.title),
|
||||
tag("title", serie.titles.title),
|
||||
tag("link", `https://radio.nrk.no/podkast/${serie.id}`),
|
||||
tag("itunes:author", "NRK"),
|
||||
tag(
|
||||
"description",
|
||||
serie.subtitle
|
||||
serie.titles.subtitle || ""
|
||||
),
|
||||
tag("ttl", "60"), //60 minutes
|
||||
tag("itunes:image", "", [
|
||||
["href", serie.image.url],
|
||||
]),
|
||||
tag("image", [
|
||||
tag("url", serie.image.url),
|
||||
tag("title", serie.title),
|
||||
]),
|
||||
...(imageUrl ? [
|
||||
tag("itunes:image", "", [
|
||||
["href", imageUrl],
|
||||
]),
|
||||
tag("image", [
|
||||
tag("url", imageUrl),
|
||||
tag("title", serie.titles.title),
|
||||
])
|
||||
] : []),
|
||||
...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 Search from "../components/Search.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 {
|
||||
query: string;
|
||||
result?: Serie[];
|
||||
result?: SearchResultList;
|
||||
origin: string;
|
||||
}
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ export const handler: Handlers<HandlerData> = {
|
|||
async GET(request, ctx) {
|
||||
const url = new URL(request.url);
|
||||
const query = url.searchParams.get("query");
|
||||
let result: Serie[] | undefined;
|
||||
let result: SearchResultList | undefined;
|
||||
if (query) {
|
||||
result = await nrkRadio.search(query);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue