From 500f1532b598ebe39a10a0dcee490d64a0e10e17 Mon Sep 17 00:00:00 2001 From: August Skare Date: Sat, 11 Mar 2023 10:46:55 +0100 Subject: [PATCH] Feature/types (#4) --- components/SeriesCard.tsx | 10 +- fresh.gen.ts | 6 +- lib/nrk-catalog.ts | 1781 ++++++++++++++++++++++++++++++++ lib/nrk-search.ts | 504 +++++++++ lib/nrk.ts | 86 +- routes/api/feeds/[seriesId].ts | 31 +- routes/api/search.ts | 14 - routes/index.tsx | 6 +- 8 files changed, 2350 insertions(+), 88 deletions(-) create mode 100644 lib/nrk-catalog.ts create mode 100644 lib/nrk-search.ts delete mode 100644 routes/api/search.ts diff --git a/components/SeriesCard.tsx b/components/SeriesCard.tsx index 0d9701a..e19603e 100644 --- a/components/SeriesCard.tsx +++ b/components/SeriesCard.tsx @@ -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 (

{props.serie.title}

{props.serie.description}

- + Kopier URL diff --git a/fresh.gen.ts b/fresh.gen.ts index 2b0a226..93cf232 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -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, diff --git a/lib/nrk-catalog.ts b/lib/nrk-catalog.ts new file mode 100644 index 0000000..aa67f93 --- /dev/null +++ b/lib/nrk-catalog.ts @@ -0,0 +1,1781 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/radio/catalog/episode/context/{episodeId}": { + /** Takes a episode id and returns the context. Supports ODM and podcast episodes. */ + get: operations["GetEpisodeContext"]; + }; + "/radio/catalog/programsContext/{programId}": { + /** Gets navigation aid for a programId. */ + get: operations["GetProgramContext"]; + }; + "/radio/catalog/programs/{programId}": { + /** Gets the program page for a program id */ + get: operations["GetProgram"]; + }; + "/radio/catalog/series/{seriesId}/type": { + /** Gets the radio series type */ + get: operations["GetSeriesType"]; + }; + "/radio/catalog/series/{seriesId}": { + /** Gets a radio series page */ + get: operations["GetSeries"]; + }; + "/radio/catalog/series/{seriesId}/episodes": { + /** Gets episodes for a radio series */ + get: operations["GetSeriesepisodes"]; + }; + "/radio/catalog/series/{seriesId}/seasons/{seasonId}": { + /** Gets a season for a radio series */ + get: operations["GetSeriesSeason"]; + }; + "/radio/catalog/series/{seriesId}/seasons/{seasonId}/episodes": { + /** Gets episodes for a radio series season */ + get: operations["GetSeriesSeasonEpisodes"]; + }; + "/radio/catalog/extramaterial/{id}/clips": { + /** Gets clip extramaterials for a radio or podcast series */ + get: operations["GetExtramaterialClips"]; + }; + "/radio/catalog/version/{id}": { + /** Gets the preferred series type (ODM or podcast) */ + get: operations["GetVersion"]; + }; + "/radio/catalog/podcast/{podcastId}": { + /** Gets a podcast page */ + get: operations["GetPodcast"]; + }; + "/radio/catalog/podcast/{podcastId}/episodes": { + /** Gets episodes for a podcast series */ + get: operations["GetPodcastepisodes"]; + }; + "/radio/catalog/podcast/{podcastId}/episodes/{podcastEpisodeId}": { + /** Gets a podcast episode */ + get: operations["GetPodcastEpisode"]; + }; + "/radio/catalog/podcast/{podcastId}/seasons/{seasonId}": { + /** Gets a season for a podcast series */ + get: operations["GetPodcastSeason"]; + }; + "/radio/catalog/podcast/{podcastId}/seasons/{seasonId}/episodes": { + /** Gets episodes for a podcast series season */ + get: operations["GetPodcastSeasonEpisodes"]; + }; +} + +export interface components { + schemas: { + EpisodeContext: { + type: components["schemas"]["Type"]; + _links: { + self: components["schemas"]["HalLink"]; + series?: components["schemas"]["HalLinkNameAndTitle"]; + season?: components["schemas"]["EpisodeSeasonHalLink"]; + episode: components["schemas"]["HalLink"]; + }; + }; + SeriesHalResource: { + _links: components["schemas"]["SeriesHalLinks"]; + seriesType: components["schemas"]["SeriesType"]; + type: components["schemas"]["Type"]; + seasonDisplayType: components["schemas"]["SeasonDisplayType"]; + series: components["schemas"]["SeriesViewModel"]; + _embedded: components["schemas"]["SeriesHalEmbedded"]; + }; + SeriesViewModel: { + id: string; + highlightedEpisode?: string; + nextEpisode?: components["schemas"]["NextEpisode"]; + titles: components["schemas"]["Titles"]; + category: components["schemas"]["Category"]; + /** @description The array is empty if no images are present */ + image: components["schemas"]["Image"][]; + /** @description The array is empty if no images are present */ + posterImage: components["schemas"]["Image"][]; + /** @description The array is empty if no images are present */ + backdropImage: components["schemas"]["Image"][]; + /** @description The array is empty if no images are present */ + squareImage: components["schemas"]["Image"][]; + }; + /** @description `StandardSeries` only gets episodes embedded. `SequentialSeries` gets seasons included. `UmbrellaSeries` gets both episodes and seasons included. */ + SeriesHalEmbedded: { + episodes?: components["schemas"]["EpisodesSeriesHalEmbedded"]; + /** @description By default 1 season is embedded. Umbrella series with seasonDisplayType 'manual' gets all seasons embedded. */ + seasons?: components["schemas"]["SeasonSeriesHalEmbedded"][]; + }; + SeasonSeriesHalEmbedded: { + _links?: components["schemas"]["SeasonSeriesHalEmbeddedLinks"]; + id: string; + titles: components["schemas"]["Titles"]; + hasAvailableEpisodes: boolean; + episodeCount: number; + /** @description The array is empty if no images are present */ + image?: components["schemas"]["Image"][]; + episodes?: components["schemas"]["EpisodesSeriesHalEmbedded"]; + /** @description Array of badges for this season. Only "new"-badge currently supported. Only applicable for seasons in Umbrella-podcasts. Otherwise not in response. */ + badges?: components["schemas"]["Badge"][]; + /** @description Number of new episodes in this season. Used in conjunction with `progressesForNewEpisodes`-link to check progresses for new episodes in season. New-badge should be removed if the number of new episodes matches the number of episodes with started/finished progress. Only applicable for seasons in Umbrella-podcasts. Otherwise not in response. */ + newEpisodesCount?: number; + } & { + links: unknown; + }; + SeasonSeriesHalEmbeddedLinks: { + self: components["schemas"]["HalLink"]; + /** @description Links to check progresses for new episodes in season. Used in conjuction with `newEpisodesCount`. Only applicable for seasons in Umbrella-podcasts. Otherwise not in response. */ + progressesForNewEpisodes?: components["schemas"]["HalLinkTemplated"][]; + }; + SeasonHalLinks: { + self: components["schemas"]["HalLink"]; + series?: components["schemas"]["HalLinkNameAndTitle"]; + podcast?: components["schemas"]["HalLinkNameAndTitle"]; + episodes: components["schemas"]["HalLink"]; + extraMaterial?: components["schemas"]["HalLink"]; + }; + PodcastSeasonHalLinks: { + self: components["schemas"]["HalLink"]; + series?: components["schemas"]["HalLinkNameAndTitle"]; + podcast?: components["schemas"]["HalLinkNameAndTitle"]; + episodes: components["schemas"]["HalLink"]; + extraMaterial?: components["schemas"]["HalLink"]; + favourite?: components["schemas"]["HalLinkTemplated"]; + share?: components["schemas"]["HalLinkShare"]; + }; + EpisodesSeriesHalEmbedded: { + _links: components["schemas"]["EpisodesHalLinks"]; + _embedded: components["schemas"]["EpisodesEpisodesHalEmbedded"]; + }; + EpisodesEpisodesHalEmbedded: { + episodes?: components["schemas"]["EpisodeHalResource"][]; + }; + EpisodesHalLinks: { + self: components["schemas"]["HalLink"]; + next?: components["schemas"]["HalLink"]; + prev?: components["schemas"]["HalLink"]; + progresses?: components["schemas"]["ProgressesHalLinks"]; + }; + EpisodesHalResource: { + _links: components["schemas"]["EpisodesHalLinks"]; + seriesType: components["schemas"]["SeriesType"]; + _embedded: components["schemas"]["EpisodesEpisodesHalEmbedded"]; + }; + EpisodeHalResource: { + _links: components["schemas"]["EpisodeHalLinks"]; + id: string; + episodeId: string; + date: string; + titles: components["schemas"]["Titles"]; + originalTitle?: string; + /** @description The array is empty if no images are present */ + image?: components["schemas"]["Image"][]; + /** @description Duration of the episode. We use the [ISO-8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) format for duration. */ + duration: string; + /** Format: int32 */ + durationInSeconds: number; + usageRights: components["schemas"]["UsageRightsVm"]; + /** Format: int32 */ + productionYear?: number; + availability: components["schemas"]["AvailabilityVm"]; + contributors?: components["schemas"]["ContributorVm"][]; + /** @description Array of badges indication statuses */ + badges?: components["schemas"]["Badge"][]; + }; + EpisodeHalLinks: { + self: components["schemas"]["HalLink"]; + playback: components["schemas"]["HalLink"]; + series: components["schemas"]["HalLinkNameAndTitle"]; + season?: components["schemas"]["EpisodeSeasonHalLink"]; + favourite?: components["schemas"]["HalLinkTemplated"]; + share?: components["schemas"]["HalLinkShare"]; + progress?: components["schemas"]["HalLinkTemplated"]; + }; + PodcastSeasonHalResource: { + _links: components["schemas"]["PodcastSeasonHalLinks"]; + seriesType: components["schemas"]["SeriesType"]; + type: components["schemas"]["Type"]; + titles: components["schemas"]["Titles"]; + category: components["schemas"]["Category"]; + /** @description The array is empty if no images are present */ + image?: components["schemas"]["Image"][]; + /** @description The array is empty if no images are present */ + posterImage?: components["schemas"]["Image"][]; + /** @description The array is empty if no images are present */ + backdropImage?: components["schemas"]["Image"][]; + /** @description The array is empty if no images are present */ + squareImage?: components["schemas"]["Image"][]; + hasAvailableEpisodes: boolean; + episodeCount: number; + _embedded: { + episodes?: components["schemas"]["EpisodesSeriesHalEmbedded"]; + }; + }; + SeasonHalResource: { + _links: components["schemas"]["SeasonHalLinks"]; + seriesType: components["schemas"]["SeriesType"]; + type: components["schemas"]["Type"]; + titles: components["schemas"]["Titles"]; + /** @description The array is empty if no images are present */ + image?: components["schemas"]["Image"][]; + /** @description The array is empty if no images are present */ + posterImage?: components["schemas"]["Image"][]; + /** @description The array is empty if no images are present */ + backdropImage?: components["schemas"]["Image"][]; + /** @description The array is empty if no images are present */ + squareImage?: components["schemas"]["Image"][]; + hasAvailableEpisodes: boolean; + episodeCount: number; + _embedded: { + episodes?: components["schemas"]["EpisodesSeriesHalEmbedded"]; + }; + }; + SeriesTypeHalResource: { + _links: components["schemas"]["SeriesTypeHalLinks"]; + seriesType: components["schemas"]["SeriesType"]; + }; + SeriesTypeHalLinks: { + self: components["schemas"]["HalLink"]; + }; + SeriesHalLinks: { + self: components["schemas"]["HalLink"]; + highlightedEpisode?: components["schemas"]["HalLinkTemplated"]; + favourite?: components["schemas"]["HalLinkTemplated"]; + seasons: components["schemas"]["HalLinkNameAndTitle"][]; + podcast?: components["schemas"]["HalLink"]; + episodes?: components["schemas"]["EpisodesHalLink"]; + userData?: components["schemas"]["HalLinkTemplated"]; + extraMaterial?: components["schemas"]["HalLink"]; + preferred?: components["schemas"]["HalLink"]; + }; + OdmProgramsHalResource: { + _links: components["schemas"]["ProgramsHalLinks"]; + id: string; + episodeId: string; + date: string; + programInformation: components["schemas"]["ProgramInformation"]; + contributors?: components["schemas"]["OdmContributorVm"][]; + /** @description The array is empty if no images are present */ + image: components["schemas"]["Image"][]; + titles: components["schemas"]["Titles"]; + availability: components["schemas"]["AvailabilityVm"]; + category: components["schemas"]["OdmCategory"]; + usageRights?: components["schemas"]["UsageRightsVm"]; + /** Format: int32 */ + productionYear?: number; + duration: components["schemas"]["Duration"]; + indexPoints?: components["schemas"]["IndexPoint"][]; + /** @description Playlist only applies to ODM episodes - podcast episodes does not have playlists. */ + playlist?: components["schemas"]["LiveElement"][]; + }; + PodcastEpisodeHalResource: { + _links: components["schemas"]["ProgramsHalLinks"]; + id: string; + episodeId: string; + date: string; + category: components["schemas"]["Category"]; + programInformation: components["schemas"]["ProgramInformation"]; + /** @description The array is empty if no images are present */ + image: components["schemas"]["Image"][]; + titles: components["schemas"]["Titles"]; + /** @description Podcast indexpoints use the same contract as odm index points but only Title and StartPoint will be set */ + indexPoints?: components["schemas"]["IndexPoint"][]; + availability: components["schemas"]["AvailabilityVm"]; + usageRights?: components["schemas"]["UsageRightsVm"]; + duration: components["schemas"]["Duration"]; + }; + LiveElement: { + title?: string; + description?: string; + programId?: string; + channelId?: string; + /** @description DateTime formatted string */ + startTime?: string; + /** @description ISO-8601 duration of the playlist element */ + duration?: string; + type?: string; + programTitle?: string; + /** @description ISO-8601 duration calculated from the episode start time */ + startPoint?: string; + }; + IndexPoint: { + title?: string; + mentioned?: string[]; + subjectList?: string[]; + contributors?: components["schemas"]["ContributorVm"][]; + description?: string; + /** @description ISO-8601 duration calculated from the episode start time */ + startPoint?: string; + }; + ProgramInformation: { + details?: components["schemas"]["DetailsVm"]; + originalTitle?: string; + }; + ProgramsHalLinks: { + self: components["schemas"]["HalLink"]; + playback: components["schemas"]["HalLink"]; + series?: components["schemas"]["HalLinkNameAndTitle"]; + season?: components["schemas"]["EpisodeSeasonHalLink"]; + favourite?: components["schemas"]["HalLinkTemplated"]; + share?: components["schemas"]["HalLinkShare"]; + progress?: components["schemas"]["HalLinkTemplated"]; + extraMaterial?: components["schemas"]["HalLink"]; + }; + DetailsVm: { + displayValue: string; + accessibilityValue: string; + }; + /** @description Applies to Standard, News and Umbrella series. The date and display value for the next upcoming episode. */ + NextEpisode: { + date?: string; + /** @description Display text for when the episode will be available */ + displayValue?: string; + }; + /** + * @description Follows the guidelines from https://nrkconfluence.atlassian.net/l/c/y03p59M0 + * @example { + * "title": "1. Stolt fortid, stor framtid", + * "subtitle": "Helena prøver å få kontakt med Camilla før sesongens første kamp mot Rosenborg. Espen jobbar desperat for å redde økonomien i klubben." + * } + */ + Titles: { + title: string; + subtitle?: string | null; + }; + UsageRightsVm: { + from: components["schemas"]["UsageRightsDateVm"]; + to: components["schemas"]["UsageRightsDateVm"]; + geoBlock: components["schemas"]["GeoBlockVm"]; + }; + UsageRightsDateVm: { + date: string; + displayValue: string; + }; + GeoBlockVm: { + isGeoBlocked: boolean; + displayValue: components["schemas"]["GeoBlockDisplayValue"]; + }; + /** @enum {string} */ + AvailabilityStatus: + | "coming" + | "available" + | "expires" + | "expired" + | "notAvailableOnline"; + /** + * @example { + * "href": "/some/url" + * } + */ + HalLink: { + href: string; + }; + /** + * @description Episodes link are included for `StandardSeries` and `UmbrellaSeries`. `SequentialSeries` has no notion of episodes, only seasons. + * @example { + * "href": "/some/url" + * } + */ + EpisodesHalLink: { + href: string; + }; + HalLinkTemplated: { + href: string; + templated: boolean; + }; + HalLinkShare: { + /** @description Query param `e` for episode, `p` for podcast, `s` for series. https://radio.nrk.no/lytt?s=saann-er-du{&autoplay} */ + href: string; + templated: boolean; + }; + /** + * @example { + * "href": "/tv/catalog/series/radioresepsjonen/seasons/201912", + * "name": "201912", + * "title": "Desember 2019" + * } + */ + HalLinkNameAndTitle: { + href: string; + name: string; + title: string; + }; + /** + * @example { + * "href": "/tv/catalog/series/radioresepsjonen/seasons/201912", + * "name": "201912", + * "title": "Desember 2019", + * "seriesType": "standard" + * } + */ + EpisodeSeasonHalLink: { + href: string; + name: string; + title: string; + /** @enum {string} */ + seriesType: "standard" | "news" | "sequential" | "umbrella"; + }; + /** + * @example { + * "id": "nyheter", + * "name": "Nyheter" + * } + */ + Category: { + id: string; + name: string; + }; + /** + * @example { + * "id": "nyheter", + * "displayValue": "Nyheter" + * } + */ + OdmCategory: { + id: string; + displayValue: string; + }; + /** + * @example { + * "url": "https://gfx.nrk.no/urxQMSXF-WnbfjBH5ke2igaWzubx5CSW-Z6FOsEAclhA", + * "width": 1920 + * } + */ + Image: { + /** Format: uri */ + url: string; + /** Format: int32 */ + width: number; + }; + /** @enum {string} */ + SeriesType: "sequential" | "news" | "standard" | "umbrella"; + /** @enum {string} */ + Type: "series" | "podcast"; + /** @enum {string} */ + SortDirection: "desc" | "asc"; + /** @enum {string} */ + SeasonDisplayType: "number" | "month" | "quarter" | "year" | "manual"; + /** @description Progresses link output applies to all episodes in the given list. */ + ProgressesHalLinks: components["schemas"]["ProgressesHalLink"][]; + /** + * @description ProgressHalLink consists of a list of comma-separated programIds that we gzip, base64 and url encode to avoid exceeding character limit in the url. + * @example { + * "href": "/tv/userdata/{userId}/progress/series/radioresepsjonen?contentIds=H4sIAAAAAAAAC_Pzc3M0tDQwNTIxtNTxg3GMkTlGhpYAsMOBOyYAAAA", + * "templated": true + * } + */ + ProgressesHalLink: { + href: string; + templated: boolean; + }; + /** @enum {string} */ + GeoBlockDisplayValue: "Verden" | "Norge"; + /** @description Represents the Availability of a Program based on the active usage rights. */ + AvailabilityVm: { + status: components["schemas"]["AvailabilityStatus"]; + /** @description Label should only be displayed if HasLabel is true. */ + hasLabel: boolean; + /** + * @description Label to be showed to the user based on the AvailabilityStatus. + * Example: "Tilgjengelig i morgen" + */ + label?: string; + }; + ContributorVm: { + role: string; + name: string[]; + }; + OdmContributorVm: { + role: string; + name: string[]; + }; + /** @description Duration of the episode. We use the [ISO-8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) format for duration. */ + Duration: { + /** Format: int32 */ + seconds: number; + displayValue: string; + iso8601: string; + }; + ExtraMaterialClipsHalResource: { + _links: components["schemas"]["ExtraMaterialClipsHalLinks"]; + /** + * Format: int32 + * @description Total number of clips for the given series/podcast. The number is only an indication, and does not reflect the number of non-playable clips. I.e. do not use this field for any kind of logic. + */ + totalClips: number; + _embedded: components["schemas"]["ExtraMaterialClipsHalEmbedded"]; + }; + /** @description Aggregated info for clips */ + ExtraMaterialClipsHalEmbedded: { + metadata?: { + 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 + */ + duration: string; + id: string; + /** @enum {undefined} */ + playability: "nonPlayable" | "playable"; + preplay: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["PreplayVm"]; + /** @description Link to full metadata */ + _links: { + self: components["schemas"]["HalLink"]; + }; + }[]; + }; + ExtraMaterialClipsHalLinks: { + self: components["schemas"]["HalLink"]; + share: components["schemas"]["HalLink"]; + }; + Badge: { + label?: string; + /** @enum {undefined} */ + type?: "new"; + }; + }; +} + +export interface operations { + /** Takes a episode id and returns the context. Supports ODM and podcast episodes. */ + GetEpisodeContext: { + parameters: { + path: { + /** The episode id */ + episodeId: string; + }; + }; + responses: { + /** Success */ + 200: { + content: { + "application/json": components["schemas"]["EpisodeContext"]; + }; + }; + /** Episode not found */ + 404: unknown; + }; + }; + /** Gets navigation aid for a programId. */ + GetProgramContext: { + parameters: { + path: { + /** The program id */ + programId: string; + }; + }; + responses: { + /** Success */ + 200: { + content: { + "application/json": components["schemas"]["EpisodeContext"]; + }; + }; + /** Program not found */ + 404: unknown; + }; + }; + /** Gets the program page for a program id */ + GetProgram: { + parameters: { + path: { + /** The program id */ + programId: string; + }; + }; + responses: { + /** Success */ + 200: { + content: { + "application/json": components["schemas"]["OdmProgramsHalResource"]; + }; + }; + /** Program not found */ + 404: unknown; + }; + }; + /** Gets the radio series type */ + GetSeriesType: { + parameters: { + path: { + /** The series id */ + seriesId: string; + }; + }; + responses: { + /** Success */ + 200: { + content: { + "application/json": components["schemas"]["SeriesTypeHalResource"]; + }; + }; + /** Series not found */ + 404: unknown; + }; + }; + /** Gets a radio series page */ + GetSeries: { + parameters: { + path: { + /** The series id */ + seriesId: string; + }; + query: { + /** This parameter only affects Standard and News Series. Default is 10. */ + pageSize?: number; + }; + }; + responses: { + /** Success */ + 200: { + content: { + "application/json": components["schemas"]["SeriesHalResource"]; + }; + }; + /** Series not found */ + 404: unknown; + }; + }; + /** Gets episodes for a radio series */ + GetSeriesepisodes: { + parameters: { + path: { + /** The series id */ + seriesId: string; + }; + query: { + /** Number of episodes returned. Default is 10 */ + pageSize?: number; + /** Which page to take from */ + page?: number; + /** Sorts episodes by date (Only applies for standard series episodes) */ + sort?: components["schemas"]["SortDirection"]; + }; + }; + responses: { + /** Success */ + 200: { + content: { + "application/json": components["schemas"]["EpisodesHalResource"]; + }; + }; + /** Series not found */ + 404: unknown; + }; + }; + /** Gets a season for a radio series */ + GetSeriesSeason: { + parameters: { + path: { + /** The series id */ + seriesId: string; + /** The season id */ + seasonId: string; + }; + }; + responses: { + /** Success */ + 200: { + content: { + "application/json": components["schemas"]["SeasonHalResource"]; + }; + }; + /** Series or season not found */ + 404: unknown; + }; + }; + /** Gets episodes for a radio series season */ + GetSeriesSeasonEpisodes: { + parameters: { + path: { + /** The series id */ + seriesId: string; + /** The season id */ + seasonId: string; + }; + query: { + /** Number of episodes returned. Default is 10 */ + pageSize?: number; + /** Which page to take from */ + page?: number; + /** Sorts episodes by date (only applies for standard series episodes) */ + sort?: components["schemas"]["SortDirection"]; + }; + }; + responses: { + /** Success */ + 200: { + content: { + "application/json": components["schemas"]["EpisodesHalResource"]; + }; + }; + /** Series or season not found */ + 404: unknown; + }; + }; + /** Gets clip extramaterials for a radio or podcast series */ + GetExtramaterialClips: { + parameters: { + path: { + /** The series or podcast id */ + id: string; + }; + query: { + /** Number of clips returned. Default is 10 */ + pageSize?: number; + /** Which page to take from. Default is 1 */ + page?: number; + /** Sorts videos by date. Default is 'desc' */ + sort?: components["schemas"]["SortDirection"]; + }; + }; + responses: { + /** Success */ + 200: { + content: { + "application/json": components["schemas"]["ExtraMaterialClipsHalResource"]; + }; + }; + /** No podcast or series with id found */ + 404: unknown; + }; + }; + /** Gets the preferred series type (ODM or podcast) */ + GetVersion: { + parameters: { + path: { + /** The series or podcast id */ + id: string; + }; + }; + responses: { + /** Success */ + 200: { + content: { + "application/json": components["schemas"]["SeriesHalResource"]; + }; + }; + /** Not found */ + 404: unknown; + }; + }; + /** Gets a podcast page */ + GetPodcast: { + parameters: { + path: { + /** The podcast id */ + podcastId: string; + }; + query: { + /** Default is 10. */ + pageSize?: number; + }; + }; + responses: { + /** Success */ + 200: { + content: { + "application/json": components["schemas"]["SeriesHalResource"]; + }; + }; + /** Podcast not found */ + 404: unknown; + }; + }; + /** Gets episodes for a podcast series */ + GetPodcastepisodes: { + parameters: { + path: { + /** The podcast id */ + podcastId: string; + }; + query: { + /** Number of episodes returned. Default is 10 */ + pageSize?: number; + /** Which page to take from */ + page?: number; + /** Sorts episodes by date (only applies for standard series episodes) */ + sort?: components["schemas"]["SortDirection"]; + }; + }; + responses: { + /** Success */ + 200: { + content: { + "application/json": components["schemas"]["EpisodesHalResource"]; + }; + }; + /** Podcast not found */ + 404: unknown; + }; + }; + /** Gets a podcast episode */ + GetPodcastEpisode: { + parameters: { + path: { + /** The podcast id */ + podcastId: string; + /** The podcast episode id */ + podcastEpisodeId: string; + }; + }; + responses: { + /** Success */ + 200: { + content: { + "application/json": components["schemas"]["PodcastEpisodeHalResource"]; + }; + }; + /** Podcast or episode not found */ + 404: unknown; + }; + }; + /** Gets a season for a podcast series */ + GetPodcastSeason: { + parameters: { + path: { + /** The podcast id */ + podcastId: string; + /** The season id */ + seasonId: string; + }; + }; + responses: { + /** Success */ + 200: { + content: { + "application/json": components["schemas"]["PodcastSeasonHalResource"]; + }; + }; + /** Podcast or season not found */ + 404: unknown; + }; + }; + /** Gets episodes for a podcast series season */ + GetPodcastSeasonEpisodes: { + parameters: { + path: { + /** The podcast id */ + podcastId: string; + /** The season id */ + seasonId: string; + }; + query: { + /** Number of episodes returned. Default is 10 */ + pageSize?: number; + /** Which page to take from */ + page?: number; + /** Sorts episodes by date (only applies for standard series episodes) */ + sort?: components["schemas"]["SortDirection"]; + }; + }; + responses: { + /** Success */ + 200: { + content: { + "application/json": components["schemas"]["EpisodesHalResource"]; + }; + }; + /** Podcast or season not found */ + 404: unknown; + }; + }; +} + +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"]; + }; + "/playback/dnslist": { + 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"]; + }; + "/playback/manifest/program/{programId}": { + 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"]; + }; + "/playback/manifest/clip/{clipId}": { + 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"]; + }; + "/playback/metadata/program/{programId}": { + 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"]; + }; + "/playback/metadata/clip/{clipId}": { + get: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["operations"]["getPlaybackClipMetadata"]; + }; + }; + components: { + schemas: { + DnsList: { + /** @description Gets list of DomainNames used by applications */ + domainNames: string[]; + _links: { + self: { + href: string; + }; + }; + }; + DebugData: { + _links: { + self: { + 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"]; + }; + PlaybackDataVm: { + maxBitrate: number; + minBitrate: number; + startBitrate: number; + springStreamContentType: string; + useNepOriginLive: boolean; + useNepOriginOdm: boolean; + cedexisDeviceId: string; + appVersion: string; + osVersion: string; + 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"]; + customManifestParameters: ( + | "AudioOnly" + | "DolbyOnly" + | "IFramePlaylist" + | "MuxAudio" + | "NoAudioOnly" + | "NoIFramePlaylist" + | "NoMuxAudi" + | "NoSignalDiscontinuities" + | "SignalDiscontinuities" + )[]; + subtitlesDistribution: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["SubtitlesDistribution"]; + }; + /** @enum {string} */ + DistributionFormat: + | "Dash" + | "Hds" + | "Hls" + | "ProgressiveDownload" + | "ProgressiveMp3"; + /** @enum {string} */ + DeviceGroupName: "Desktop" | "Mobile" | "SetTopUnit" | "Tablet" | "Tv"; + /** @enum {string} */ + PlayerPlatform: + | "Altibox" + | "AltiboxHls" + | "Android" + | "AndroidTv" + | "AppleTv" + | "AppleTv4" + | "CanalDigitalKabelTv" + | "CanalDigitalSatellitt" + | "Chromecast" + | "ChromecastLegacy" + | "GenericHls" + | "Get" + | "GoogleAssistant" + | "Ios" + | "Lg" + | "LgLegacy" + | "LgNetcast" + | "Ludo" + | "Panasonic" + | "Philips" + | "PlayStation3" + | "PlayStation4" + | "RiksTv" + | "SamsungOrsay" + | "SamsungTizen" + | "Sony" + | "TelenorKabelTv" + | "TelenorLegacyKabelTv" + | "WindowsPhone" + | "WindowsPhone8" + | "WorkStationHls"; + /** @enum {string} */ + SubtitlesDistribution: "Manifest" | "NoSubtitles" | "Sideload"; + LocationDataVm: { + clientIpAddress: string; + countryCode: string; + isIpNorwegian: boolean; + overriddenClientIpAddress: string; + overriddenCountryCode: string; + overriddenLookupSource: string; + 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"] + >; + PlayableManifest: { + /** @enum {string} */ + playability: "playable"; + 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"]; + }; + 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"]; + _links: { + self: { + href: string; + }; + metadata: { + href: string; + /** @enum {string} */ + name: "metadata"; + }; + }; + }; + NonPlayableManifest: { + /** @enum {string} */ + playability: "nonPlayable"; + playable: unknown | null; + statistics: { + scores: unknown | null; + ga: unknown | null; + luna: unknown | null; + }; + 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"]; + _links: { + self: { + href: string; + }; + metadata: { + href: string; + /** @enum {string} */ + name: "metadata"; + }; + }; + }; + PlayableElementVm: { + endSequenceStartTime: string | null; + duration: string | null; + /** @description List of mediaAssets */ + 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; + /** @description List of subtitles */ + 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"]; + /** @description True if program is marked as HDSecure, false otherwise */ + encrypted: boolean; + }; + /** + * @description The video format for the resource identified by the URL. + * @enum {string} + */ + PlayableFormat: "HDS" | "HLS" | "MP4"; + /** + * @description The mimetype that corresponds to the specified format. + * @enum {string} + */ + PlayableMimeType: + | "application/adobe-f4m" + | "application/mp4" + | "application/vnd.apple.mpegurl" + | "audio/mp4" + | "video/mp4"; + LiveBufferVm: { + /** @description Live buffer startTime */ + bufferStartTime: string | null; + /** @description Live buffer length */ + bufferDuration: string; + bufferType: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["LiveBufferType"]; + }; + /** + * @description Type of liveBuffer + * @enum {string} + */ + 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"]; + defaultOn: boolean; + webVtt: string; + }; + /** @enum {string} */ + SubtitleType: "gbr" | "mix" | "nor" | "ttv"; + /** @enum {string} */ + SubtitleLanguage: "en" | "nb"; + /** @enum {string} */ + SubtitleLabel: "English" | "Norsk tekst" | "Tekstet for hørselshemmede"; + ScoresStatisticsVm: { + springStreamSite: string; + springStreamStream: string; + springStreamContentType: string; + springStreamProgramId: string; + }; + /** + * @description * dimension1: ID. KILDESYSTEM:UNIK_IDENTIFIKATOR + * * dimension2: Tittel + * * dimension3: Publiseringsår. + * * dimension4: Publiseringsmåned. + * * dimension5: Publiseringsdag. [01, 31] + * * dimension10: Kilde-ID + * * dimension21: Serie. Kun relevant for episoder/instalments. + * * dimension22: Episodenummer. Kun relevant for episoder (instalments?). + * * dimension23: Kategori + * * dimension25: Mediatype [video | audio] + * * dimension26: Mediakategori [live | ondemand] + * * dimension29: Platform + * [Ekstern doc](https://github.com/nrkno/analytics-docs/blob/master/docs/schema.md) + */ + GoogleAnalyticsVm: { [key: string]: string }; + ConvivaStatisticsVm: { + /** + * @description A string identifying the current video application. + * Example: Desktop-Ludo + */ + playerName: string; + /** + * @description A unique, human-readable identifier for the content. + * Example: DMTV23400617-smaken-av-den-rette + */ + assetName: string; + /** + * @description Duration of the video content. In seconds. + * When duration is unknown, eg channels: -1 + */ + duration: number; + 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"]; + }; + /** @enum {string} */ + ConvivaStreamType: "LIVE" | "VOD"; + /** + * @description A string-to-string dictionary of custom metadata. + * When a property does not have a value, the string "None" should be used + */ + ConvivaCustomProperties: { + /** + * @description ContentId for current content + * Example: DMTV23400617 + */ + contentId: string; + /** + * @description Name of series + * Not set for channels and content without series + * Example: naar-kjemien-stemmer + */ + series: string; + /** + * @description Title of current content + * Example: smaken-av-den-rette + */ + title: string; + /** + * @description EpisodeNumber for current content + * Not set for channels and content without series + * Example: 5 (as string) + */ + episode: string; + /** + * @description Main category of the content + * Example: underholdning + */ + category: string; + mediaType: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["ConvivaMediaType"]; + /** + * @description Name of selected CDN + * Example: Telenor-Cdn + */ + cdnName: string; + /** @description Application version number, set by player */ + applicationVersion: string; + /** + * @description Application name, set by player + * Example: LudoHTML5PlayerAdapter (HTML5/Native HLS, hls.js, Flashls, ...) + */ + applicationName: string; + /** + * @description ServiceName, set by player + * Example: tv.nrk.no, nrk.no, nrksuper.no + */ + serviceName: string; + contentType: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["ConvivaContentType"]; + }; + /** + * @description MediaType for current content + * @enum {string} + */ + ConvivaMediaType: "Audio" | "Video"; + /** + * @description Type of content + * @enum {string} + */ + ConvivaContentType: + | "Channel" + | "Clip" + | "Event" + | "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"]; + }; + LunaConfig: { + beacon: string; + }; + LunaPayload: { + show: string | null; + title: string; + category: string | null; + contentLength: string | null; + device: string; + playerId: string; + deliveryType: string; + playerInfo: string; + cdnName: string; + }; + /** @enum {string} */ + PlayableStreamingMode: "live" | "onDemand"; + AvailabilityVm: { + /** @description Standardized text about the availability of the media element. */ + information: string; + /** + * @description Indicates that the media element may not be played outside the range of + * Norwegian IP addresses. + */ + isGeoBlocked: boolean; + /** + * @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; + /** + * @description Only applicable for live media elements. Will be null for on-demand media + * elements. + */ + live: + | (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"]; + }>) + | null; + externalEmbeddingAllowed: boolean; + }; + LiveTransmissionIntervalVm: { + from: string; + to: string; + }; + /** @enum {string} */ + DisplayAspectRatio: "16:9" | "1:1" | "2:3" | "4:3" | "9:16"; + OnDemandUsageRights: { + from: string; + to: string; + hasRightsNow: boolean; + }; + /** @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"]; + /** + * @description Standard message for end user describing why the media element is not + * playable. + */ + endUserMessage: string; + /** @description URL to the help page referred to in the end user message. */ + helpUrl: string; + /** + * @description URL to page with links to content available outside Norway (i.e. content that + * isn't geo-blocked). + */ + availableAbroadUrl: string | null; + }; + /** @enum {string} */ + NonPlayableReason: "blocked" | "notransmission" | "unavailable"; + /** @enum {string} */ + NonPlayableMessageType: + | "ChannelIsGeoblocked" + | "ChannelUnavailable" + | "NoMessage" + | "ProgramIsGeoblocked" + | "ProgramIsInLiveBuffer" + | "ProgramIsNotTranscoded" + | "ProgramRightsHasExpired" + | "ProgramRightsHasNotStarted" + | "ProgramUnavailable"; + /** @enum {string} */ + AvailabilityLabel: "Av" | "Synstolk" | "Tegnspråk"; + SeriesLink: { + href: string; + /** @enum {string} */ + name: "series"; + }; + NextLink: { + href: string; + /** @enum {string} */ + name: "next"; + }; + /** @enum {string} */ + ManifestLinkName: "default" | "synstolk" | "tegntolk"; + SkipDialogInfoVm: { + startIntroInSeconds: number; + endIntroInSeconds: number; + startCreditsInSeconds: number; + startIntro: string; + endIntro: string; + startCredits: string; + }; + IndexPointVm: { + /** @description IndexPoint title */ + title: string; + /** @description IndexPoint Duration */ + startPoint: string; + }; + PosterImageVm: { + url: string; + pixelWidth: number; + }; + /** @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"][]; + }; + /** @description Title to show before play */ + TitlesVm: { + title: string; + subtitle: string; + }; + PreplayVm: { + 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"]; + /** @description List of index points */ + indexPoints: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["IndexPointVm"][]; + }; + LegalAgeRating: { + code: string; + displayValue: string; + displayAge: string; + }; + LegalAgeBodyRated: { + /** @enum {string} */ + status: "rated"; + rating: external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["LegalAgeRating"]; + }; + LegalAgeBodyExempt: { + /** @enum {string} */ + status: "exempt"; + }; + 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"] + >; + }; + PlayableMetadataVm: { + /** @description Uri to resolve to get the media manifest */ + resolve: string; + }; + PlayableMetadata: { + /** @enum {string} */ + playability: "playable"; + 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"]; + 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"]; + _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"]; + progress?: { + href: string; + /** @enum {boolean} */ + templated: true; + }; + }; + _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"]; + }; + }; + }[] + > | null; + }; + }; + NonPlayableMetadata: { + /** @enum {string} */ + playability: "nonPlayable"; + playable: unknown | null; + 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"]; + 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"]; + _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"]; + progress?: { + href: string; + /** @enum {boolean} */ + templated: true; + }; + }; + _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"]; + }; + }; + }[] + > | 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"] + >; + }; + }; + operations: { + getPlaybackDebug: { + responses: { + /** OK */ + 200: { + content: { + "application/json": external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["DebugData"]; + }; + }; + }; + }; + getPlaybackDnsList: { + responses: { + /** OK */ + 200: { + content: { + "application/json": external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["DnsList"]; + }; + }; + }; + }; + /** This endpoint is just for clients that doesn't know its context */ + getPlaybackManifestRedirect: { + parameters: { + path: { + /** + * The unique ID for the playback element. + * Different kinds of media elements have different kinds of IDs. + * - On-demand (PRF): Four letters and eight digits. Example: MSUB19120216 + * - Klipp (Potion / Foss2): GUID. + * - Klipp (Foss1): The letter `f` followed by a sequence of digits. + * - Klipp (Guri): A sequence of digits. + * - Podcast: Prefix "nrkno-poddkast-". Example: nrkno-poddkast-10908-146680-20122018123000 + * - Kanal: The name of the channel. Example: nrk1 + */ + id: string; + }; + query: { + /** Is the client in in Super-universe or not. Used to get correct statistics and content */ + inSuperUniverse?: string; + /** PreferredBandwidth could be used to limit the number of qualities in the HLS manifest. Legal values: low, mid, high, auto */ + preferredBandwidth?: string; + /** PreferredCdn could be used to get the CDN used in the last request. Overrides Cedexis. The value that should be passed in exists as a header, named: CdnName */ + preferredCdn?: string; + /** Use to activate liveToVod on playback */ + live2Vod?: string; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["ManifestResponse"]; + }; + }; + }; + }; + getPlaybackProgramManifest: { + parameters: { + path: { + /** + * The unique ID for the playback element. + * Different kinds of media elements have different kinds of IDs. + * - On-demand (PRF): Four letters and eight digits. Example: MSUB19120216 + */ + programId: string; + }; + query: { + /** Is the client in in Super-universe or not. Used to get correct statistics and content */ + inSuperUniverse?: string; + /** PreferredBandwidth could be used to limit the number of qualities in the HLS manifest. Legal values: low, mid, high, auto */ + preferredBandwidth?: string; + /** PreferredCdn could be used to get the CDN used in the last request. Overrides Cedexis. The value that should be passed in exists as a header, named: CdnName */ + preferredCdn?: string; + /** Used to request HLS manifest for offline. This overrides/ignores preferredBandwith. Tablet/phone information is pulled from User-Agent */ + offline?: "standard" | "high"; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["ManifestResponse"]; + }; + }; + }; + }; + getPlaybackChannelManifest: { + parameters: { + path: { + /** + * The unique ID for the playback element. + * Different kinds of media elements have different kinds of IDs. + * - Kanal: The name of the channel. Example: nrk1 + */ + channelId: string; + }; + query: { + /** Is the client in in Super-universe or not. Used to get correct statistics and content */ + inSuperUniverse?: string; + /** PreferredBandwidth could be used to limit the number of qualities in the HLS manifest. Legal values: low, mid, high, auto */ + preferredBandwidth?: string; + /** PreferredCdn could be used to get the CDN used in the last request. Overrides Cedexis. The value that should be passed in exists as a header, named: CdnName */ + preferredCdn?: string; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["ManifestResponse"]; + }; + }; + }; + }; + getPlaybackClipManifest: { + parameters: { + path: { + /** + * The unique ID for the playback element. + * Different kinds of media elements have different kinds of IDs. + * - Klipp (Potion / Foss2): GUID. + * - Klipp (Foss1): The letter `f` followed by a sequence of digits. + * - Klipp (Guri): A sequence of digits. + */ + clipId: string; + }; + query: { + /** PreferredBandwidth could be used to limit the number of qualities in the HLS manifest. Legal values: low, mid, high, auto */ + preferredBandwidth?: string; + /** PreferredCdn could be used to get the CDN used in the last request. Overrides Cedexis. The value that should be passed in exists as a header, named: CdnName */ + preferredCdn?: string; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["ManifestResponse"]; + }; + }; + }; + }; + /** This endpoint is just for clients that doesn't know its context */ + getPlaybackMetadataRedirect: { + parameters: { + path: { + /** + * The unique ID for the playback element. + * Different kinds of media elements have different kinds of IDs. + * - On-demand (PRF): Four letters and eight digits. Example: MSUB19120216 + * - Klipp (Potion / Foss2): GUID. + * - Klipp (Foss1): The letter `f` followed by a sequence of digits. + * - Klipp (Guri): A sequence of digits. + * - Podcast: Prefix "nrkno-poddkast-". Example: nrkno-poddkast-10908-146680-20122018123000 + * - Kanal: The name of the channel. Example: nrk1 + */ + id: string; + }; + query: { + /** Use to activate liveToVod on playback */ + live2Vod?: string; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["MetadataResponse"]; + }; + }; + }; + }; + getPlaybackProgramMetadata: { + parameters: { + path: { + /** + * The unique ID for the playback element. + * Different kinds of media elements have different kinds of IDs. + * - On-demand (PRF): Four letters and eight digits. Example: MSUB19120216 + */ + programId: string; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["MetadataResponse"]; + }; + }; + }; + }; + getPlaybackChannelMetadata: { + parameters: { + path: { + /** + * The unique ID for the playback element. + * Different kinds of media elements have different kinds of IDs. + * - Kanal: The name of the channel. Example: nrk1 + */ + channelId: string; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["MetadataResponse"]; + }; + }; + }; + }; + getPlaybackClipMetadata: { + parameters: { + path: { + /** + * The unique ID for the playback element. + * Different kinds of media elements have different kinds of IDs. + * - Klipp (Potion / Foss2): GUID. + * - Klipp (Foss1): The letter `f` followed by a sequence of digits. + * - Klipp (Guri): A sequence of digits. + */ + clipId: string; + }; + }; + responses: { + /** OK */ + 200: { + content: { + "application/json": external["https://psapi.nrk.no/documentation/openapi/playback/openapi.json"]["components"]["schemas"]["MetadataResponse"]; + }; + }; + }; + }; + }; + }; +} diff --git a/lib/nrk-search.ts b/lib/nrk-search.ts new file mode 100644 index 0000000..07c53c8 --- /dev/null +++ b/lib/nrk-search.ts @@ -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 {} diff --git a/lib/nrk.ts b/lib/nrk.ts index a8546bd..bda8bb4 100644 --- a/lib/nrk.ts +++ b/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 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 & { 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) { - +async function withDownloadLink(episode: OriginalEpisode): Promise { // getting stream link - const [playbackStatus, playbackResponse] = await get(`https://psapi.nrk.no/playback/manifest/podcast/${withoutUrl.episodeId}`) - - if (playbackStatus === OK) { - - const url = playbackResponse.playable.assets[0].url; - return { - ...withoutUrl, url - } + 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 ${withoutUrl.episodeId}` + throw `Error getting downloadLink for ${episode.episodeId}` } } export const nrkRadio = { - search: async (query: string): Promise => { - const [status, response] = await get(`https://psapi.nrk.no/radio/search/search?q=${query}`); - if (status === OK) { - const series = response.results.series.results - return series as Serie[]; + 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}` } @@ -55,23 +51,17 @@ export const nrkRadio = { [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}`) + get(`https://psapi.nrk.no/radio/catalog/podcast/${seriesId}/episodes`), + get(`https://psapi.nrk.no/radio/catalog/podcast/${seriesId}`) ]); - if (episodeStatus === OK && seriesStatus === OK) { - - const episodes: Episode[] = await Promise.all(episodeResponse._embedded.episodes.map(withDownloadLink)); - + 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}` } diff --git a/routes/api/feeds/[seriesId].ts b/routes/api/feeds/[seriesId].ts index db51b16..688991c 100644 --- a/routes/api/feeds/[seriesId].ts +++ b/routes/api/feeds/[seriesId].ts @@ -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), ]), ], diff --git a/routes/api/search.ts b/routes/api/search.ts deleted file mode 100644 index c6e8929..0000000 --- a/routes/api/search.ts +++ /dev/null @@ -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 => { - - - const { query } = await req.json(); - const series = await nrkRadio.search(query); - - return new Response(JSON.stringify({ series }), { - headers: { "Content-Type": "application/json" } - }); -}; diff --git a/routes/index.tsx b/routes/index.tsx index 34b1a95..a0d5653 100644 --- a/routes/index.tsx +++ b/routes/index.tsx @@ -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 = { 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); }