diff --git a/src/backend/common/infrastructure/config/source/applemusic.ts b/src/backend/common/infrastructure/config/source/applemusic.ts index b5acba9e..6a406db3 100644 --- a/src/backend/common/infrastructure/config/source/applemusic.ts +++ b/src/backend/common/infrastructure/config/source/applemusic.ts @@ -1,30 +1,42 @@ -import type {PollingOptions} from "../common.ts"; -import type {CommonSourceConfig, CommonSourceData, CommonSourceOptions} from "./index.ts"; - -export interface AppleMusicKey { - id: string - teamId: string - p8: string -} - -export interface AppleMusicData extends CommonSourceData, PollingOptions { - key?: AppleMusicKey - token?: string - mediaUserToken?: string +import * as z from "zod"; +import {pollingOptionsSchema} from "../common.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema} from "./index.ts"; + +export const appleMusicKeySchema = z.object({ + id: z.string(), + teamId: z.string(), + p8: z.string() +}); + +export type AppleMusicKey = z.infer; + +export const appleMusicDataSchema = z.object({ + ...commonSourceDataSchema.shape, + ...pollingOptionsSchema.shape, + key: appleMusicKeySchema.optional(), + token: z.string().optional(), + mediaUserToken: z.string().optional(), /** * Origin header to include in every Apple Music API request. * Required when using a browser token (not a MusicKit key). * - * @example "https://music.apple.com" + * @examples ["https://music.apple.com"] */ - origin?: string -} - -export interface AppleMusicSourceConfig extends CommonSourceConfig { - data?: AppleMusicData - options?: CommonSourceOptions & { - logAuth?: boolean - logDiff?: boolean + origin: z.string().optional().meta({ + description: "Origin header to include in every Apple Music API request.", + examples: ["https://music.apple.com"] + }), +}); + +export type AppleMusicData = z.infer; + +export const appleMusicSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, + data: appleMusicDataSchema.optional(), + options: z.object({ + ...commonSourceOptionsSchema.shape, + logAuth: z.boolean().optional(), + logDiff: z.boolean().optional(), /** * Fixes a quirk where Apple Music's history API hides duplicate plays. * If you listen to A → B → A, the API returns [A, B, X, Y ...] instead of [A, B, A, X, Y ...]. @@ -36,23 +48,36 @@ export interface AppleMusicSourceConfig extends CommonSourceConfig { * @default true * @examples [true, false] */ - recoverUnchangedTopHistory?: boolean + recoverUnchangedTopHistory: z.boolean().optional().meta({ + description: "Fixes a quirk where Apple Music's history API hides duplicate plays.", + default: true, + examples: [true, false] + }), /** * Removes extraneous suffixes from album data - * + * * Apple Music add ' - EP' and ' - Single' to album names for EP's and singles, respectively. * These suffixes are not part of the official names for the album and can cause issues in scrobble services * or when matching metadata (musicbrainz). - * + * * When this option is true (default), Multi-scrobbler automatically removes these suffixes. - * + * * @default true * @examples [true, false] */ - normalizeAlbum?: boolean - } -} + normalizeAlbum: z.boolean().optional().meta({ + description: "Removes extraneous suffixes from album data", + default: true, + examples: [true, false] + }), + }).optional(), +}); + +export type AppleMusicSourceConfig = z.infer; + +export const appleMusicSourceAIOConfigSchema = z.object({ + ...appleMusicSourceConfigSchema.shape, + type: z.literal('applemusic'), +}); -export interface AppleMusicSourceAIOConfig extends AppleMusicSourceConfig { - type: 'applemusic' -} +export type AppleMusicSourceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/chromecast.ts b/src/backend/common/infrastructure/config/source/chromecast.ts index 25b1177f..3c382ca4 100644 --- a/src/backend/common/infrastructure/config/source/chromecast.ts +++ b/src/backend/common/infrastructure/config/source/chromecast.ts @@ -1,6 +1,31 @@ -import type {CommonSourceConfig, CommonSourceData} from "./index.ts"; +import * as z from "zod"; +import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; -export interface ChromecastData extends CommonSourceData { +export const chromecastDeviceInfoSchema = z.object({ + /** + * A friendly name to identify this device + * + * @examples ["MySmartTV"] + * */ + name: z.string().meta({ + description: "A friendly name to identify this device", + examples: ["MySmartTV"] + }), + /** + * The IP address of the device + * + * @examples ["192.168.0.115"] + * */ + address: z.string().meta({ + description: "The IP address of the device", + examples: ["192.168.0.115"] + }), +}); + +export type ChromecastDeviceInfo = z.infer; + +export const chromecastDataSchema = z.object({ + ...commonSourceDataSchema.shape, /** * DO NOT scrobble from any cast devices that START WITH these values, case-insensitive * @@ -8,7 +33,10 @@ export interface ChromecastData extends CommonSourceData { * * @examples [["home-mini","family-tv"]] * */ - blacklistDevices?: string | string[] + blacklistDevices: z.union([z.string(), z.array(z.string())]).optional().meta({ + description: "DO NOT scrobble from any cast devices that START WITH these values, case-insensitive", + examples: [["home-mini", "family-tv"]] + }), /** * ONLY scrobble from any cast device that START WITH these values, case-insensitive @@ -19,14 +47,20 @@ export interface ChromecastData extends CommonSourceData { * * @examples [["home-mini","family-tv"]] * */ - whitelistDevices?: string | string[] + whitelistDevices: z.union([z.string(), z.array(z.string())]).optional().meta({ + description: "ONLY scrobble from any cast device that START WITH these values, case-insensitive", + examples: [["home-mini", "family-tv"]] + }), /** * DO NOT scrobble from any application that START WITH these values, case-insensitive * * @examples [["spotify","pandora"]] * */ - blacklistApps?: string | string[] + blacklistApps: z.union([z.string(), z.array(z.string())]).optional().meta({ + description: "DO NOT scrobble from any application that START WITH these values, case-insensitive", + examples: [["spotify", "pandora"]] + }), /** * ONLY scrobble from any application that START WITH these values, case-insensitive @@ -35,7 +69,10 @@ export interface ChromecastData extends CommonSourceData { * * @examples [["spotify","pandora"]] * */ - whitelistApps?: string | string[] + whitelistApps: z.union([z.string(), z.array(z.string())]).optional().meta({ + description: "ONLY scrobble from any application that START WITH these values, case-insensitive", + examples: [["spotify", "pandora"]] + }), /** * Try to use Avahi and avahi-browse to resolve mDNS devices instead of native mDNS querying @@ -44,21 +81,28 @@ export interface ChromecastData extends CommonSourceData { * * @default false * */ - useAvahi?: boolean + useAvahi: z.boolean().optional().meta({ + description: "Try to use Avahi and avahi-browse to resolve mDNS devices instead of native mDNS querying", + default: false + }), /** * Use mDNS to discovery Google Cast devices on your next automatically? * * If not explicitly set then it is TRUE if `devices` is not set * */ - useAutoDiscovery?: boolean | undefined + useAutoDiscovery: z.union([z.boolean(), z.undefined()]).optional().meta({ + description: "Use mDNS to discovery Google Cast devices on your next automatically?" + }), /** * A list of Google Cast devices to monitor * * If this is used then `useAutoDiscovery` is set to FALSE, if not explicitly set * */ - devices?: ChromecastDeviceInfo[] + devices: z.array(chromecastDeviceInfoSchema).optional().meta({ + description: "A list of Google Cast devices to monitor" + }), /** * Chromecast Apps report a "media type" in the status info returned for whatever is currently playing @@ -71,35 +115,33 @@ export interface ChromecastData extends CommonSourceData { * * @default false * */ - allowUnknownMedia?: boolean | string[] + allowUnknownMedia: z.union([z.boolean(), z.array(z.string())]).optional().meta({ + description: "Chromecast Apps report a \"media type\" in the status info returned for whatever is currently playing", + default: false + }), /** * Media provided by any App whose name is listed here will ALWAYS be tracked, regardless of the "media type" reported * * Apps will be recognized if they CONTAIN any of these values, case-insensitive * */ - forceMediaRecognitionOn?: string[] -} + forceMediaRecognitionOn: z.array(z.string()).optional().meta({ + description: "Media provided by any App whose name is listed here will ALWAYS be tracked, regardless of the \"media type\" reported" + }), +}); -export interface ChromecastDeviceInfo { - /** - * A friendly name to identify this device - * - * @examples ["MySmartTV"] - * */ - name: string - /** - * The IP address of the device - * - * @examples ["192.168.0.115"] - * */ - address: string -} +export type ChromecastData = z.infer; + +export const chromecastSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, + data: chromecastDataSchema, +}); + +export type ChromecastSourceConfig = z.infer; -export interface ChromecastSourceConfig extends CommonSourceConfig { - data: ChromecastData -} +export const chromecastSourceAIOConfigSchema = z.object({ + ...chromecastSourceConfigSchema.shape, + type: z.literal('chromecast'), +}); -export interface ChromecastSourceAIOConfig extends ChromecastSourceConfig { - type: 'chromecast' -} +export type ChromecastSourceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/deezer.ts b/src/backend/common/infrastructure/config/source/deezer.ts index 2c3c6ff4..250d293d 100644 --- a/src/backend/common/infrastructure/config/source/deezer.ts +++ b/src/backend/common/infrastructure/config/source/deezer.ts @@ -1,67 +1,113 @@ +import * as z from "zod"; import type {Second} from "../../../../../core/Atomic.ts"; -import type {PollingOptions} from "../common.ts"; -import type {CommonSourceConfig, CommonSourceData, CommonSourceOptions} from "./index.ts"; +import {pollingOptionsSchema} from "../common.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema} from "./index.ts"; -export interface DeezerData extends CommonSourceData, PollingOptions { +export const deezerDataSchema = z.object({ + ...commonSourceDataSchema.shape, + ...pollingOptionsSchema.shape, /** * deezer client id * * @examples ["a89cba1569901a0671d5a9875fed4be1"] * */ - clientId: string + clientId: z.string().meta({ + description: "deezer client id", + examples: ["a89cba1569901a0671d5a9875fed4be1"] + }), /** * deezer client secret * * @examples ["ec42e09d5ae0ee0f0816ca151008412a"] * */ - clientSecret: string + clientSecret: z.string().meta({ + description: "deezer client secret", + examples: ["ec42e09d5ae0ee0f0816ca151008412a"] + }), /** * deezer redirect URI -- required only if not the default shown here. URI must end in "callback" * * @default "http://localhost:9078/deezer/callback" * @examples ["http://localhost:9078/deezer/callback"] * */ - redirectUri?: string + redirectUri: z.string().optional().meta({ + description: "deezer redirect URI -- required only if not the default shown here.", + default: "http://localhost:9078/deezer/callback", + examples: ["http://localhost:9078/deezer/callback"] + }), - accessToken?: string -} -export interface DeezerSourceConfig extends CommonSourceConfig { - data: DeezerData -} + accessToken: z.string().optional(), +}); -export interface DeezerSourceAIOConfig extends DeezerSourceConfig { - type: 'deezer' -} +export type DeezerData = z.infer; + +export const deezerSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, + data: deezerDataSchema, +}); + +export type DeezerSourceConfig = z.infer; -export interface DeezerInternalData extends CommonSourceData, PollingOptions { +export const deezerSourceAIOConfigSchema = z.object({ + ...deezerSourceConfigSchema.shape, + type: z.literal('deezer'), +}); + +export type DeezerSourceAIOConfig = z.infer; + +export const deezerInternalDataSchema = z.object({ + ...commonSourceDataSchema.shape, + ...pollingOptionsSchema.shape, /** ARL retrieved from Deezer response header */ - arl: string + arl: z.string().meta({ + description: "ARL retrieved from Deezer response header" + }), /** User agent - * + * * @default "Mozilla/5.0 (X11; Linux i686; rv:135.0) Gecko/20100101 Firefox/135.0" */ - userAgent?: string + userAgent: z.string().optional().meta({ + description: "User agent", + default: "Mozilla/5.0 (X11; Linux i686; rv:135.0) Gecko/20100101 Firefox/135.0" + }), /** The ID (USER_ID) of the linked account to monitor. If not set, monitors the main ARL account */ - accountId?: string -} + accountId: z.string().optional().meta({ + description: "The ID (USER_ID) of the linked account to monitor." + }), +}); -export interface DeezerInternalSourceConfig extends CommonSourceConfig { - data: DeezerInternalData - options?: DeezerInternalSourceOptions -} +export type DeezerInternalData = z.infer; -export interface DeezerInternalSourceOptions extends CommonSourceOptions { - fuzzyDiscoveryIgnore?: boolean | 'aggressive' -} +export const deezerInternalSourceOptionsSchema = z.object({ + ...commonSourceOptionsSchema.shape, + fuzzyDiscoveryIgnore: z.union([z.boolean(), z.literal('aggressive')]).optional(), +}); -export interface DeezerInternalAIOConfig extends DeezerInternalSourceConfig { - type: 'deezer' -} +export type DeezerInternalSourceOptions = z.infer; + +export const deezerInternalSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, + data: deezerInternalDataSchema, + options: deezerInternalSourceOptionsSchema.optional(), +}); + +export type DeezerInternalSourceConfig = z.infer; + +export const deezerInternalAIOConfigSchema = z.object({ + ...deezerInternalSourceConfigSchema.shape, + type: z.literal('deezer'), +}); -export type DeezerCompatConfig = DeezerSourceConfig | DeezerInternalSourceConfig; +export type DeezerInternalAIOConfig = z.infer; -export type DeezerAIOCompatConfig = DeezerSourceAIOConfig | DeezerInternalAIOConfig; +export const deezerCompatConfigSchema = z.union([deezerSourceConfigSchema, deezerInternalSourceConfigSchema]); + +export type DeezerCompatConfig = z.infer; + +export const deezerAIOCompatConfigSchema = z.union([deezerSourceAIOConfigSchema, deezerInternalAIOConfigSchema]); + +export type DeezerAIOCompatConfig = z.infer; export interface DeezerInternalTrackData { /** Song Id */ @@ -84,4 +130,4 @@ export interface DeezerInternalTrackData { DURATION: Second __TYPE__: 'song' | string -} \ No newline at end of file +} diff --git a/src/backend/common/infrastructure/config/source/endpointlfm.ts b/src/backend/common/infrastructure/config/source/endpointlfm.ts index 019ae6c0..102d5c4f 100644 --- a/src/backend/common/infrastructure/config/source/endpointlfm.ts +++ b/src/backend/common/infrastructure/config/source/endpointlfm.ts @@ -1,6 +1,8 @@ -import type {CommonSourceConfig, CommonSourceData} from "./index.ts"; +import * as z from "zod"; +import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; -export interface LastFMEndpointData extends CommonSourceData { +export const lastFmEndpointDataSchema = z.object({ + ...commonSourceDataSchema.shape, /** * The URL ending that should be used to identify scrobbles for this source * @@ -13,13 +15,23 @@ export interface LastFMEndpointData extends CommonSourceData { * * If no slug is found from an extension's incoming webhook event the first Last.fm source without a slug will be used * */ - slug?: string | null -} + slug: z.union([z.string(), z.null()]).optional().meta({ + description: "The URL ending that should be used to identify scrobbles for this source" + }), +}); -export interface LastFMEndpointSourceConfig extends CommonSourceConfig { - data?: LastFMEndpointData -} +export type LastFMEndpointData = z.infer; -export interface LastFMEndpointSourceAIOConfig extends LastFMEndpointSourceConfig { - type: 'endpointlfm' -} +export const lastFmEndpointSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, + data: lastFmEndpointDataSchema.optional(), +}); + +export type LastFMEndpointSourceConfig = z.infer; + +export const lastFmEndpointSourceAIOConfigSchema = z.object({ + ...lastFmEndpointSourceConfigSchema.shape, + type: z.literal('endpointlfm'), +}); + +export type LastFMEndpointSourceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/endpointlz.ts b/src/backend/common/infrastructure/config/source/endpointlz.ts index 61427633..443e4bc3 100644 --- a/src/backend/common/infrastructure/config/source/endpointlz.ts +++ b/src/backend/common/infrastructure/config/source/endpointlz.ts @@ -1,6 +1,8 @@ -import type {CommonSourceConfig, CommonSourceData} from "./index.ts"; +import * as z from "zod"; +import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; -export interface ListenbrainzEndpointData extends CommonSourceData { +export const listenbrainzEndpointDataSchema = z.object({ + ...commonSourceDataSchema.shape, /** * The URL ending that should be used to identify scrobbles for this source * @@ -13,29 +15,43 @@ export interface ListenbrainzEndpointData extends CommonSourceData { * * If no slug is found from an extension's incoming webhook event the first Listenbrainz source without a slug will be used * */ - slug?: string + slug: z.string().optional().meta({ + description: "The URL ending that should be used to identify scrobbles for this source" + }), /** * If an LZ submission request contains this token in the Authorization Header it will be used to match the submission with this Source * * See: https://listenbrainz.readthedocs.io/en/latest/users/api/index.html#add-the-user-token-to-your-requests * */ - token?: string + token: z.string().optional().meta({ + description: "If an LZ submission request contains this token in the Authorization Header it will be used to match the submission with this Source" + }), - /** + /** * The listenbrainz "username" to associate with this Source - * + * * Will be returned in validate-token responses and used to determine a response for playing-now, if your client supports it - * + * * If no value is configured then the Source's name will be used */ - username?: string -} + username: z.string().optional().meta({ + description: "The listenbrainz \"username\" to associate with this Source" + }), +}); + +export type ListenbrainzEndpointData = z.infer; + +export const listenbrainzEndpointSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, + data: listenbrainzEndpointDataSchema.optional(), +}); + +export type ListenbrainzEndpointSourceConfig = z.infer; -export interface ListenbrainzEndpointSourceConfig extends CommonSourceConfig { - data?: ListenbrainzEndpointData -} +export const listenbrainzEndpointSourceAIOConfigSchema = z.object({ + ...listenbrainzEndpointSourceConfigSchema.shape, + type: z.literal('endpointlz'), +}); -export interface ListenbrainzEndpointSourceAIOConfig extends ListenbrainzEndpointSourceConfig { - type: 'endpointlz' -} +export type ListenbrainzEndpointSourceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/icecast.ts b/src/backend/common/infrastructure/config/source/icecast.ts index 61198f7a..c752b6e5 100644 --- a/src/backend/common/infrastructure/config/source/icecast.ts +++ b/src/backend/common/infrastructure/config/source/icecast.ts @@ -1,5 +1,5 @@ -import type {CommonSourceConfig, CommonSourceData, CommonSourceOptions} from "./index.ts"; - +import * as z from "zod"; +import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema, manualListeningOptionsSchema} from "./index.ts"; export interface IcecastMetadata { icy?: { @@ -16,32 +16,52 @@ export interface IcecastMetadata { } } -export type IcecastSource = 'icy' | 'ogg' | 'icestats' | 'stats' | 'sevenhtml' | 'nextsongs'; +export const icecastSourceSchema = z.union([z.literal('icy'), z.literal('ogg'), z.literal('icestats'), z.literal('stats'), z.literal('sevenhtml'), z.literal('nextsongs')]); -export interface IcecastOptions { - sources?: IcecastSource[] - icestatsEndpoint?: string - statsEndpoint?: string - nextsongsEndpoint?: string - sevenhtmlEndpoint?: string - icyMetaInt?: number -} +export type IcecastSource = z.infer; + +export const icecastOptionsSchema = z.object({ + sources: z.array(icecastSourceSchema).optional(), + icestatsEndpoint: z.string().optional(), + statsEndpoint: z.string().optional(), + nextsongsEndpoint: z.string().optional(), + sevenhtmlEndpoint: z.string().optional(), + icyMetaInt: z.number().optional(), +}); -export interface IcecastData extends CommonSourceData, IcecastOptions { +export type IcecastOptions = z.infer; + +export const icecastDataSchema = z.object({ + ...commonSourceDataSchema.shape, + ...icecastOptionsSchema.shape, /** * The Icecast stream URL * */ - url: string -} + url: z.string().meta({ + description: "The Icecast stream URL" + }), +}); -export interface IcecastSourceOptions extends CommonSourceOptions { -} +export type IcecastData = z.infer; -export interface IcecastSourceConfig extends CommonSourceConfig { - data: IcecastData - options?: IcecastSourceOptions -} +export const icecastSourceOptionsSchema = z.object({ + ...commonSourceOptionsSchema.shape, + ...manualListeningOptionsSchema.shape, +}); -export interface IcecastSourceAIOConfig extends IcecastSourceConfig { - type: 'icecast' -} +export type IcecastSourceOptions = z.infer; + +export const icecastSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, + data: icecastDataSchema, + options: icecastSourceOptionsSchema.optional(), +}); + +export type IcecastSourceConfig = z.infer; + +export const icecastSourceAIOConfigSchema = z.object({ + ...icecastSourceConfigSchema.shape, + type: z.literal('icecast'), +}); + +export type IcecastSourceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/jellyfin.ts b/src/backend/common/infrastructure/config/source/jellyfin.ts index 0328eec6..621e881f 100644 --- a/src/backend/common/infrastructure/config/source/jellyfin.ts +++ b/src/backend/common/infrastructure/config/source/jellyfin.ts @@ -1,110 +1,147 @@ -import type {CommonSourceConfig, CommonSourceData, CommonSourceOptions} from "./index.ts"; -import type { - // @ts-expect-error weird typings? - CollectionType, - // @ts-expect-error weird typings? - MediaType, -} from "@jellyfin/sdk/lib/generated-client/index.js"; - -export interface JellyApiData extends CommonSourceData { +import * as z from "zod"; +import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema} from "./index.ts"; + +// `CollectionType`/`MediaType` (from `@jellyfin/sdk`) don't resolve cleanly - the package's typings don't +// line up with its actual exports (see the `@ts-expect-error` this replaced). Represented as `string`/`string[]` +// per instruction rather than chasing the SDK's broken typings. + +export const jellyApiDataSchema = z.object({ + ...commonSourceDataSchema.shape, /** * HOST:PORT of the Jellyfin server to connect to * */ - url: string + url: z.string().meta({ + description: "HOST:PORT of the Jellyfin server to connect to" + }), /** * The username of the user to authenticate for or track scrobbles for * */ - user: string + user: z.string().meta({ + description: "The username of the user to authenticate for or track scrobbles for" + }), /** * Password of the username to authenticate for * * Required if `apiKey` is not provided. * */ - password?: string + password: z.string().optional().meta({ + description: "Password of the username to authenticate for" + }), /** * API Key to authenticate with. * * Required if `password` is not provided. * */ - apiKey?: string + apiKey: z.string().optional().meta({ + description: "API Key to authenticate with." + }), /** * Only scrobble for specific users (case-insensitive) * * If `true` MS will scrobble activity from all users * */ - usersAllow?: string | true | string[] + usersAllow: z.union([z.string(), z.literal(true), z.array(z.string())]).optional().meta({ + description: "Only scrobble for specific users (case-insensitive)" + }), /** * Do not scrobble for these users (case-insensitive) * */ - usersBlock?: string | string[] + usersBlock: z.union([z.string(), z.array(z.string())]).optional().meta({ + description: "Do not scrobble for these users (case-insensitive)" + }), /** * Only scrobble if device or application name contains strings from this list (case-insensitive) * */ - devicesAllow?: string | string[] + devicesAllow: z.union([z.string(), z.array(z.string())]).optional().meta({ + description: "Only scrobble if device or application name contains strings from this list (case-insensitive)" + }), /** * Do not scrobble if device or application name contains strings from this list (case-insensitive) * */ - devicesBlock?: string | string[] + devicesBlock: z.union([z.string(), z.array(z.string())]).optional().meta({ + description: "Do not scrobble if device or application name contains strings from this list (case-insensitive)" + }), /** * Only scrobble if library name contains string from this list (case-insensitive) * */ - librariesAllow?: string | string[] + librariesAllow: z.union([z.string(), z.array(z.string())]).optional().meta({ + description: "Only scrobble if library name contains string from this list (case-insensitive)" + }), /** * Do not scrobble if library name contains strings from this list (case-insensitive) * */ - librariesBlock?: string | string[] + librariesBlock: z.union([z.string(), z.array(z.string())]).optional().meta({ + description: "Do not scrobble if library name contains strings from this list (case-insensitive)" + }), /** * Allow MS to scrobble audio media in libraries classified other than 'music' - * + * * `librariesAllow` will achieve the same result as this but this is more convenient if you do not want to explicitly list every library name or are only using `librariesBlock` */ - additionalAllowedLibraryTypes?: CollectionType[] + additionalAllowedLibraryTypes: z.array(z.string()).optional().meta({ + description: "Allow MS to scrobble audio media in libraries classified other than 'music'" + }), /** * Force media with a type of "Unknown" to be counted as Audio - * + * * @default false * @deprecated use allowMediaTypes instead */ - allowUnknown?: boolean + allowUnknown: z.boolean().optional().meta({ + description: "Force media with a type of \"Unknown\" to be counted as Audio", + default: false + }), /** * Allow these media types to be scrobbled. - * + * * If not defined or empty, uses 'Audio' as default. If non-empty then *only* uses these types (make sure you include Audio!) - * + * * Values are case-insensitive. - * + * * See https://github.com/jellyfin/jellyfin-sdk-typescript/blob/master/src/generated-client/models/media-type.ts#L22 for possible types - * + * */ - allowMediaTypes?: MediaType[] | string + allowMediaTypes: z.union([z.array(z.string()), z.string()]).optional().meta({ + description: "Allow these media types to be scrobbled." + }), /** * HOST:PORT of the Jellyfin server that your browser will be able to access from the frontend (and thus load images and links from) * If unspecified it will use the normal server HOST and PORT from the `url` * Necessary if you are using a reverse proxy or other network configuration that prevents the frontend from accessing the server directly - * + * * ENV: JELLYFIN_FRONTEND_URL_OVERRIDE * */ - frontendUrlOverride?: string -} + frontendUrlOverride: z.string().optional().meta({ + description: "HOST:PORT of the Jellyfin server that your browser will be able to access from the frontend (and thus load images and links from)" + }), +}); + +export type JellyApiData = z.infer; + +export const jellyApiOptionsSchema = z.object({ + ...commonSourceOptionsSchema.shape, +}); -export interface JellyApiOptions extends CommonSourceOptions { -} +export type JellyApiOptions = z.infer; -export interface JellyApiSourceConfig extends CommonSourceConfig { - data: JellyApiData - options?: JellyApiOptions -} +export const jellyApiSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, + data: jellyApiDataSchema, + options: jellyApiOptionsSchema.optional(), +}); -export interface JellyApiSourceAIOConfig extends JellyApiSourceConfig { - type: 'jellyfin' -} +export type JellyApiSourceConfig = z.infer; +export const jellyApiSourceAIOConfigSchema = z.object({ + ...jellyApiSourceConfigSchema.shape, + type: z.literal('jellyfin'), +}); -export type JellyfinCompatConfig = JellyApiSourceConfig; \ No newline at end of file +export type JellyApiSourceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/jriver.ts b/src/backend/common/infrastructure/config/source/jriver.ts index 1d4f5e33..efbbcf45 100644 --- a/src/backend/common/infrastructure/config/source/jriver.ts +++ b/src/backend/common/infrastructure/config/source/jriver.ts @@ -1,7 +1,11 @@ -import type {PollingOptions, RequestRetryOptions} from "../common.ts"; -import type {CommonSourceConfig, CommonSourceData} from "./index.ts"; +import * as z from "zod"; +import {pollingOptionsSchema, requestRetryOptionsSchema} from "../common.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; -export interface JRiverData extends CommonSourceData, PollingOptions, RequestRetryOptions { +export const jRiverDataSchema = z.object({ + ...commonSourceDataSchema.shape, + ...pollingOptionsSchema.shape, + ...requestRetryOptionsSchema.shape, /** * URL of the JRiver HTTP server to connect to * @@ -20,22 +24,39 @@ export interface JRiverData extends CommonSourceData, PollingOptions, RequestRet * @examples ["http://localhost:52199/MCWS/v1/"] * @default "http://localhost:52199/MCWS/v1/" * */ - url: string + url: z.string().meta({ + description: "URL of the JRiver HTTP server to connect to", + default: "http://localhost:52199/MCWS/v1/", + examples: ["http://localhost:52199/MCWS/v1/"] + }), /** * If you have enabled authentication, the username you set * */ - username?: string + username: z.string().optional().meta({ + description: "If you have enabled authentication, the username you set" + }), /** * If you have enabled authentication, the password you set * */ - password?: string -} -export interface JRiverSourceConfig extends CommonSourceConfig { - data: JRiverData -} - -export interface JRiverSourceAIOConfig extends JRiverSourceConfig { - type: 'jriver' -} + password: z.string().optional().meta({ + description: "If you have enabled authentication, the password you set" + }), +}); + +export type JRiverData = z.infer; + +export const jRiverSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, + data: jRiverDataSchema, +}); + +export type JRiverSourceConfig = z.infer; + +export const jRiverSourceAIOConfigSchema = z.object({ + ...jRiverSourceConfigSchema.shape, + type: z.literal('jriver'), +}); + +export type JRiverSourceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/kodi.ts b/src/backend/common/infrastructure/config/source/kodi.ts index 6c71d4c2..3efa1b5f 100644 --- a/src/backend/common/infrastructure/config/source/kodi.ts +++ b/src/backend/common/infrastructure/config/source/kodi.ts @@ -1,8 +1,10 @@ -import type {PollingOptions} from "../common.ts"; -import type {CommonSourceConfig, CommonSourceData} from "./index.ts"; +import * as z from "zod"; +import {pollingOptionsSchema} from "../common.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; - -export interface KodiData extends CommonSourceData, PollingOptions { +export const kodiDataSchema = z.object({ + ...commonSourceDataSchema.shape, + ...pollingOptionsSchema.shape, /** * URL of the Kodi HTTP server to connect to * @@ -21,22 +23,39 @@ export interface KodiData extends CommonSourceData, PollingOptions { * @examples ["http://localhost:8080/jsonrpc"] * @default "http://localhost:8080/jsonrpc" * */ - url: string + url: z.string().meta({ + description: "URL of the Kodi HTTP server to connect to", + default: "http://localhost:8080/jsonrpc", + examples: ["http://localhost:8080/jsonrpc"] + }), /** * The username set for Remote Control via Web Sever * */ - username: string + username: z.string().meta({ + description: "The username set for Remote Control via Web Sever" + }), /** * The password set for Remote Control via Web Sever * */ - password: string -} -export interface KodiSourceConfig extends CommonSourceConfig { - data: KodiData -} - -export interface KodiSourceAIOConfig extends KodiSourceConfig { - type: 'kodi' -} + password: z.string().meta({ + description: "The password set for Remote Control via Web Sever" + }), +}); + +export type KodiData = z.infer; + +export const kodiSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, + data: kodiDataSchema, +}); + +export type KodiSourceConfig = z.infer; + +export const kodiSourceAIOConfigSchema = z.object({ + ...kodiSourceConfigSchema.shape, + type: z.literal('kodi'), +}); + +export type KodiSourceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/koito.ts b/src/backend/common/infrastructure/config/source/koito.ts index 784bb018..c8bb1a73 100644 --- a/src/backend/common/infrastructure/config/source/koito.ts +++ b/src/backend/common/infrastructure/config/source/koito.ts @@ -1,21 +1,37 @@ -import type {KoitoData} from "../client/koito.ts"; -import type {PollingOptions} from "../common.ts"; -import type {CommonSourceConfig, CommonSourceData} from "./index.ts"; +import * as z from "zod"; +import {koitoDataSchema} from "../client/koito.ts"; +import {pollingOptionsSchema} from "../common.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; -export interface KoitoSourceData extends KoitoData, CommonSourceData, PollingOptions { -} +export const koitoSourceDataSchema = z.object({ + ...koitoDataSchema.shape, + ...commonSourceDataSchema.shape, + ...pollingOptionsSchema.shape, +}); -export interface KoitoSourceConfig extends CommonSourceConfig { +export type KoitoSourceData = z.infer; + +export const koitoSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, /** * When used in `koito.config` this tells multi-scrobbler whether to use this data to configure a source or client. * * @default source * @examples ["source"] * */ - configureAs: 'source' - data: KoitoSourceData -} + configureAs: z.literal('source').meta({ + description: "When used in `koito.config` this tells multi-scrobbler whether to use this data to configure a source or client.", + default: "source", + examples: ["source"] + }), + data: koitoSourceDataSchema, +}); + +export type KoitoSourceConfig = z.infer; + +export const koitoSourceAIOConfigSchema = z.object({ + ...koitoSourceConfigSchema.shape, + type: z.literal('koito'), +}); -export interface KoitoSourceAIOConfig extends KoitoSourceConfig { - type: 'koito' -} +export type KoitoSourceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/lastfm.ts b/src/backend/common/infrastructure/config/source/lastfm.ts index 4fb02ca3..5649d493 100644 --- a/src/backend/common/infrastructure/config/source/lastfm.ts +++ b/src/backend/common/infrastructure/config/source/lastfm.ts @@ -1,20 +1,37 @@ -import type {LastfmData} from "../client/lastfm.ts"; -import type {PollingOptions} from "../common.ts"; -import type {CommonSourceConfig, CommonSourceData} from "./index.ts"; +import * as z from "zod"; +import {lastfmDataSchema} from "../client/lastfm.ts"; +import {pollingOptionsSchema} from "../common.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; -export interface LastFmSourceData extends CommonSourceData, PollingOptions, LastfmData{} +export const lastFmSourceDataSchema = z.object({ + ...commonSourceDataSchema.shape, + ...pollingOptionsSchema.shape, + ...lastfmDataSchema.shape, +}); -export interface LastfmSourceConfig extends CommonSourceConfig { +export type LastFmSourceData = z.infer; + +export const lastfmSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, /** * When used in `lastfm.config` this tells multi-scrobbler whether to use this data to configure a source or client. * * @default source * @examples ["source"] * */ - configureAs: 'source' - data: LastFmSourceData -} + configureAs: z.literal('source').meta({ + description: "When used in `lastfm.config` this tells multi-scrobbler whether to use this data to configure a source or client.", + default: "source", + examples: ["source"] + }), + data: lastFmSourceDataSchema, +}); + +export type LastfmSourceConfig = z.infer; + +export const lastFmSouceAIOConfigSchema = z.object({ + ...lastfmSourceConfigSchema.shape, + type: z.literal('lastfm'), +}); -export interface LastFmSouceAIOConfig extends LastfmSourceConfig { - type: 'lastfm' -} +export type LastFmSouceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/librefm.ts b/src/backend/common/infrastructure/config/source/librefm.ts index 2637e6a8..141fe519 100644 --- a/src/backend/common/infrastructure/config/source/librefm.ts +++ b/src/backend/common/infrastructure/config/source/librefm.ts @@ -1,20 +1,37 @@ -import type {LibrefmData} from "../client/librefm.ts"; -import type {PollingOptions} from "../common.ts"; -import type {CommonSourceConfig, CommonSourceData} from "./index.ts"; +import * as z from "zod"; +import {librefmDataSchema} from "../client/librefm.ts"; +import {pollingOptionsSchema} from "../common.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; -export interface librefmSourceData extends CommonSourceData, PollingOptions, LibrefmData{} +export const librefmSourceDataSchema = z.object({ + ...commonSourceDataSchema.shape, + ...pollingOptionsSchema.shape, + ...librefmDataSchema.shape, +}); -export interface LibrefmSourceConfig extends CommonSourceConfig { +export type librefmSourceData = z.infer; + +export const librefmSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, /** * When used in `librefm.config` this tells multi-scrobbler whether to use this data to configure a source or client. * * @default source * @examples ["source"] * */ - configureAs?: 'source' - data: LibrefmData -} + configureAs: z.literal('source').optional().meta({ + description: "When used in `librefm.config` this tells multi-scrobbler whether to use this data to configure a source or client.", + default: "source", + examples: ["source"] + }), + data: librefmDataSchema, +}); + +export type LibrefmSourceConfig = z.infer; + +export const librefmSouceAIOConfigSchema = z.object({ + ...librefmSourceConfigSchema.shape, + type: z.literal('librefm'), +}); -export interface LibrefmSouceAIOConfig extends LibrefmSourceConfig { - type: 'librefm' -} +export type LibrefmSouceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/listenbrainz.ts b/src/backend/common/infrastructure/config/source/listenbrainz.ts index 029fde0b..cf84e35f 100644 --- a/src/backend/common/infrastructure/config/source/listenbrainz.ts +++ b/src/backend/common/infrastructure/config/source/listenbrainz.ts @@ -1,21 +1,37 @@ -import type {ListenBrainzData} from "../client/listenbrainz.ts"; -import type {PollingOptions} from "../common.ts"; -import type {CommonSourceConfig, CommonSourceData} from "./index.ts"; +import * as z from "zod"; +import {listenBrainzDataSchema} from "../client/listenbrainz.ts"; +import {pollingOptionsSchema} from "../common.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; -export interface ListenBrainzSourceData extends ListenBrainzData, CommonSourceData, PollingOptions { -} +export const listenBrainzSourceDataSchema = z.object({ + ...listenBrainzDataSchema.shape, + ...commonSourceDataSchema.shape, + ...pollingOptionsSchema.shape, +}); -export interface ListenBrainzSourceConfig extends CommonSourceConfig { +export type ListenBrainzSourceData = z.infer; + +export const listenBrainzSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, /** * When used in `listenbrainz.config` this tells multi-scrobbler whether to use this data to configure a source or client. * * @default source * @examples ["source"] * */ - configureAs: 'source' - data: ListenBrainzSourceData -} + configureAs: z.literal('source').meta({ + description: "When used in `listenbrainz.config` this tells multi-scrobbler whether to use this data to configure a source or client.", + default: "source", + examples: ["source"] + }), + data: listenBrainzSourceDataSchema, +}); + +export type ListenBrainzSourceConfig = z.infer; + +export const listenBrainzSourceAIOConfigSchema = z.object({ + ...listenBrainzSourceConfigSchema.shape, + type: z.literal('listenbrainz'), +}); -export interface ListenBrainzSourceAIOConfig extends ListenBrainzSourceConfig { - type: 'listenbrainz' -} +export type ListenBrainzSourceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/maloja.ts b/src/backend/common/infrastructure/config/source/maloja.ts index 67fed754..4ff6ffb2 100644 --- a/src/backend/common/infrastructure/config/source/maloja.ts +++ b/src/backend/common/infrastructure/config/source/maloja.ts @@ -1,21 +1,37 @@ -import type {MalojaData} from "../client/maloja.ts"; -import type {PollingOptions} from "../common.ts"; -import type {CommonSourceConfig, CommonSourceData} from "./index.ts"; +import * as z from "zod"; +import {malojaDataSchema} from "../client/maloja.ts"; +import {pollingOptionsSchema} from "../common.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; -export interface MalojaSourceData extends MalojaData, CommonSourceData, PollingOptions { -} +export const malojaSourceDataSchema = z.object({ + ...malojaDataSchema.shape, + ...commonSourceDataSchema.shape, + ...pollingOptionsSchema.shape, +}); -export interface MalojaSourceConfig extends CommonSourceConfig { +export type MalojaSourceData = z.infer; + +export const malojaSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, /** * When used in `maloja.config` this tells multi-scrobbler whether to use this data to configure a source or client. * * @default source * @examples ["source"] * */ - configureAs: 'source' - data: MalojaSourceData -} + configureAs: z.literal('source').meta({ + description: "When used in `maloja.config` this tells multi-scrobbler whether to use this data to configure a source or client.", + default: "source", + examples: ["source"] + }), + data: malojaSourceDataSchema, +}); + +export type MalojaSourceConfig = z.infer; + +export const malojaSourceAIOConfigSchema = z.object({ + ...malojaSourceConfigSchema.shape, + type: z.literal('maloja'), +}); -export interface MalojaSourceAIOConfig extends MalojaSourceConfig { - type: 'maloja' -} +export type MalojaSourceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/mopidy.ts b/src/backend/common/infrastructure/config/source/mopidy.ts index 5acd3b93..d5aed420 100644 --- a/src/backend/common/infrastructure/config/source/mopidy.ts +++ b/src/backend/common/infrastructure/config/source/mopidy.ts @@ -1,7 +1,10 @@ -import type {PollingOptions} from "../common.ts"; -import type {CommonSourceConfig, CommonSourceData} from "./index.ts"; +import * as z from "zod"; +import {pollingOptionsSchema} from "../common.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; -export interface MopidyData extends CommonSourceData, PollingOptions { +export const mopidyDataSchema = z.object({ + ...commonSourceDataSchema.shape, + ...pollingOptionsSchema.shape, /** * URL of the Mopidy HTTP server to connect to * @@ -22,7 +25,11 @@ export interface MopidyData extends CommonSourceData, PollingOptions { * @examples ["ws://localhost:6680/mopidy/ws/"] * @default "ws://localhost:6680/mopidy/ws/" * */ - url?: string + url: z.string().optional().meta({ + description: "URL of the Mopidy HTTP server to connect to", + default: "ws://localhost:6680/mopidy/ws/", + examples: ["ws://localhost:6680/mopidy/ws/"] + }), /** * Do not scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive @@ -31,7 +38,9 @@ export interface MopidyData extends CommonSourceData, PollingOptions { * * List is ignored if uriWhitelist is used. * */ - uriBlacklist?: string[] + uriBlacklist: z.array(z.string()).optional().meta({ + description: "Do not scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive" + }), /** * Only scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive @@ -39,7 +48,9 @@ export interface MopidyData extends CommonSourceData, PollingOptions { * EX: Only scrobble tracks from soundcloud by adding 'soundcloud' to this list. * * */ - uriWhitelist?: string[] + uriWhitelist: z.array(z.string()).optional().meta({ + description: "Only scrobble tracks whose URI STARTS WITH any of these strings, case-insensitive" + }), /** * Remove album data that matches any case-insensitive string from this list when scrobbling, @@ -49,7 +60,11 @@ export interface MopidyData extends CommonSourceData, PollingOptions { * @examples [["Soundcloud", "Mixcloud"]] * @default ["Soundcloud"] * */ - albumBlacklist?: string[] + albumBlacklist: z.array(z.string()).optional().meta({ + description: "Remove album data that matches any case-insensitive string from this list when scrobbling,", + default: ["Soundcloud"], + examples: [["Soundcloud", "Mixcloud"]] + }), /** * How long to wait before polling the source API for new tracks (in seconds) @@ -57,7 +72,11 @@ export interface MopidyData extends CommonSourceData, PollingOptions { * @default 10 * @examples [10] * */ - interval?: number + interval: z.number().optional().meta({ + description: "How long to wait before polling the source API for new tracks (in seconds)", + default: 10, + examples: [10] + }), /** * When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds) @@ -65,12 +84,25 @@ export interface MopidyData extends CommonSourceData, PollingOptions { * @default 30 * @examples [30] * */ - maxInterval?: number -} -export interface MopidySourceConfig extends CommonSourceConfig { - data: MopidyData -} + maxInterval: z.number().optional().meta({ + description: "When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)", + default: 30, + examples: [30] + }), +}); -export interface MopidySourceAIOConfig extends MopidySourceConfig { - type: 'mopidy' -} +export type MopidyData = z.infer; + +export const mopidySourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, + data: mopidyDataSchema, +}); + +export type MopidySourceConfig = z.infer; + +export const mopidySourceAIOConfigSchema = z.object({ + ...mopidySourceConfigSchema.shape, + type: z.literal('mopidy'), +}); + +export type MopidySourceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/mpd.ts b/src/backend/common/infrastructure/config/source/mpd.ts index d6f31913..a979188c 100644 --- a/src/backend/common/infrastructure/config/source/mpd.ts +++ b/src/backend/common/infrastructure/config/source/mpd.ts @@ -1,7 +1,10 @@ -import type {PollingOptions} from "../common.ts"; -import type {CommonSourceConfig, CommonSourceData, CommonSourceOptions} from "./index.ts"; +import * as z from "zod"; +import {pollingOptionsSchema} from "../common.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema} from "./index.ts"; -export interface MPDData extends CommonSourceData, PollingOptions { +export const mpdDataSchema = z.object({ + ...commonSourceDataSchema.shape, + ...pollingOptionsSchema.shape, /** * URL:PORT of the MPD server to connect to * @@ -10,33 +13,51 @@ export interface MPDData extends CommonSourceData, PollingOptions { * @examples ["localhost:6600"] * @default "localhost:6600" * */ - url?: string + url: z.string().optional().meta({ + description: "URL:PORT of the MPD server to connect to", + default: "localhost:6600", + examples: ["localhost:6600"] + }), /** * If using socket specify the path instead of url. * * trailing `~` is replaced by your home directory * */ - path?: string + path: z.string().optional().meta({ + description: "If using socket specify the path instead of url." + }), /** * Password for the server, if set https://mpd.readthedocs.io/en/stable/user.html#permissions-and-passwords * */ - password?: string + password: z.string().optional().meta({ + description: "Password for the server, if set https://mpd.readthedocs.io/en/stable/user.html#permissions-and-passwords" + }), -} +}); -export interface MPDSourceOptions extends CommonSourceOptions { - //disableDiscovery?: boolean -} +export type MPDData = z.infer; -export interface MPDSourceConfig extends CommonSourceConfig { - data: MPDData - options: MPDSourceOptions -} +export const mpdSourceOptionsSchema = z.object({ + ...commonSourceOptionsSchema.shape, +}); -export interface MPDSourceAIOConfig extends MPDSourceConfig { - type: 'mpd' -} +export type MPDSourceOptions = z.infer; -export type PlayerState = 'play' | 'stop' | 'pause'; \ No newline at end of file +export const mpdSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, + data: mpdDataSchema, + options: mpdSourceOptionsSchema, +}); + +export type MPDSourceConfig = z.infer; + +export const mpdSourceAIOConfigSchema = z.object({ + ...mpdSourceConfigSchema.shape, + type: z.literal('mpd'), +}); + +export type MPDSourceAIOConfig = z.infer; + +export type PlayerState = 'play' | 'stop' | 'pause'; diff --git a/src/backend/common/infrastructure/config/source/mpris.ts b/src/backend/common/infrastructure/config/source/mpris.ts index d5ad3395..1f59c7b7 100644 --- a/src/backend/common/infrastructure/config/source/mpris.ts +++ b/src/backend/common/infrastructure/config/source/mpris.ts @@ -1,4 +1,5 @@ -import type {CommonSourceConfig, CommonSourceData} from "./index.ts"; +import * as z from "zod"; +import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; export const PLAYBACK_STATUS_PLAYING = 'Playing'; export const PLAYBACK_STATUS_PAUSED = 'Paused'; @@ -28,13 +29,17 @@ export interface PlayerInfo { metadata: MPRISMetadata } -export interface MPRISData extends CommonSourceData { +export const mprisDataSchema = z.object({ + ...commonSourceDataSchema.shape, /** * DO NOT scrobble from any players that START WITH these values, case-insensitive * * @examples [["spotify","vlc"]] * */ - blacklist?: string | string[] + blacklist: z.union([z.string(), z.array(z.string())]).optional().meta({ + description: "DO NOT scrobble from any players that START WITH these values, case-insensitive", + examples: [["spotify", "vlc"]] + }), /** * ONLY from any players that START WITH these values, case-insensitive @@ -43,13 +48,24 @@ export interface MPRISData extends CommonSourceData { * * @examples [["spotify","vlc"]] * */ - whitelist?: string | string[] -} + whitelist: z.union([z.string(), z.array(z.string())]).optional().meta({ + description: "ONLY from any players that START WITH these values, case-insensitive", + examples: [["spotify", "vlc"]] + }), +}); -export interface MPRISSourceConfig extends CommonSourceConfig { - data: MPRISData -} +export type MPRISData = z.infer; -export interface MPRISSourceAIOConfig extends MPRISSourceConfig { - type: 'mpris' -} +export const mprisSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, + data: mprisDataSchema, +}); + +export type MPRISSourceConfig = z.infer; + +export const mprisSourceAIOConfigSchema = z.object({ + ...mprisSourceConfigSchema.shape, + type: z.literal('mpris'), +}); + +export type MPRISSourceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/musiccast.ts b/src/backend/common/infrastructure/config/source/musiccast.ts index 5c433056..4ffc6ff8 100644 --- a/src/backend/common/infrastructure/config/source/musiccast.ts +++ b/src/backend/common/infrastructure/config/source/musiccast.ts @@ -1,6 +1,7 @@ +import * as z from "zod"; import { REPORTED_PLAYER_STATUSES } from '../../../../../core/Atomic.ts'; import type {ReportedPlayerStatus} from '../../../../../core/Atomic.ts'; -import type {CommonSourceConfig, CommonSourceData} from "./index.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; export type PlaybackStatus = 'play' | 'stop' | 'pause' | 'fast_reverse' | 'fast_forward' @@ -70,19 +71,31 @@ export const playbackToReportedStatus = (pb: PlaybackStatus): ReportedPlayerStat } } -export interface MusicCastData extends CommonSourceData { +export const musicCastDataSchema = z.object({ + ...commonSourceDataSchema.shape, /** * The host or URL of the YamahaExtendedControl endpoint to use * * @examples [["192.168.0.101","http://192.168.0.101/YamahaExtendedControl"]] * */ - url: string -} + url: z.string().meta({ + description: "The host or URL of the YamahaExtendedControl endpoint to use", + examples: [["192.168.0.101", "http://192.168.0.101/YamahaExtendedControl"]] + }), +}); -export interface MusicCastSourceConfig extends CommonSourceConfig { - data: MusicCastData -} +export type MusicCastData = z.infer; -export interface MusicCastSourceAIOConfig extends MusicCastSourceConfig { - type: 'musiccast' -} +export const musicCastSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, + data: musicCastDataSchema, +}); + +export type MusicCastSourceConfig = z.infer; + +export const musicCastSourceAIOConfigSchema = z.object({ + ...musicCastSourceConfigSchema.shape, + type: z.literal('musiccast'), +}); + +export type MusicCastSourceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/musikcube.ts b/src/backend/common/infrastructure/config/source/musikcube.ts index bcbc6dc2..0e1a9afd 100644 --- a/src/backend/common/infrastructure/config/source/musikcube.ts +++ b/src/backend/common/infrastructure/config/source/musikcube.ts @@ -1,4 +1,5 @@ -import type {CommonSourceConfig, CommonSourceData} from "./index.ts"; +import * as z from "zod"; +import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; export const PLAYBACK_STATUS_PLAYING_MC = 'playing'; export const PLAYBACK_STATUS_PAUSED_MC = 'paused'; @@ -75,7 +76,8 @@ export interface MCPlaybackOverviewRequest extends MCRequestCommon { device_id: string } -export interface MusikcubeData extends CommonSourceData { +export const musikcubeDataSchema = z.object({ + ...commonSourceDataSchema.shape, /** * URL of the Musikcube Websocket (Metadata) server to connect to * @@ -94,23 +96,37 @@ export interface MusikcubeData extends CommonSourceData { * @examples ["ws://localhost:7905"] * @default "ws://localhost:7905" * */ - url?: string + url: z.string().optional().meta({ + description: "URL of the Musikcube Websocket (Metadata) server to connect to", + default: "ws://localhost:7905", + examples: ["ws://localhost:7905"] + }), /** * Password set in Musikcube https://github.com/clangen/musikcube/wiki/remote-api-documentation * * * musikcube -> settings -> server setup -> password * */ - password: string + password: z.string().meta({ + description: "Password set in Musikcube https://github.com/clangen/musikcube/wiki/remote-api-documentation" + }), - device_id?: string + device_id: z.string().optional(), -} +}); -export interface MusikcubeSourceConfig extends CommonSourceConfig { - data: MusikcubeData -} +export type MusikcubeData = z.infer; -export interface MusikcubeSourceAIOConfig extends MusikcubeSourceConfig { - type: 'musikcube' -} +export const musikcubeSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, + data: musikcubeDataSchema, +}); + +export type MusikcubeSourceConfig = z.infer; + +export const musikcubeSourceAIOConfigSchema = z.object({ + ...musikcubeSourceConfigSchema.shape, + type: z.literal('musikcube'), +}); + +export type MusikcubeSourceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/plex.ts b/src/backend/common/infrastructure/config/source/plex.ts index 54882b75..d27f1d4e 100644 --- a/src/backend/common/infrastructure/config/source/plex.ts +++ b/src/backend/common/infrastructure/config/source/plex.ts @@ -1,63 +1,92 @@ -import type {PollingOptions} from "../common.ts"; -import type {CommonSourceConfig, CommonSourceData, CommonSourceOptions} from "./index.ts"; +import * as z from "zod"; +import {pollingOptionsSchema} from "../common.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema} from "./index.ts"; -export interface PlexApiData extends CommonSourceData, PollingOptions { - token?: string +export const plexApiDataSchema = z.object({ + ...commonSourceDataSchema.shape, + ...pollingOptionsSchema.shape, + token: z.string().optional(), /** * http(s)://HOST:PORT of the Plex server to connect to * */ - url: string + url: z.string().meta({ + description: "http(s)://HOST:PORT of the Plex server to connect to" + }), - /** + /** * Only scrobble for specific users (case-insensitive) * * If `true` MS will scrobble activity from all users * */ - usersAllow?: string | true | string[] - /** - * Do not scrobble for these users (case-insensitive) - * */ - usersBlock?: string | string[] - - /** - * Only scrobble if device or application name contains strings from this list (case-insensitive) - * */ - devicesAllow?: string | string[] - /** - * Do not scrobble if device or application name contains strings from this list (case-insensitive) - * */ - devicesBlock?: string | string[] - - /** - * Only scrobble if library name contains string from this list (case-insensitive) - * */ - librariesAllow?: string | string[] - /** - * Do not scrobble if library name contains strings from this list (case-insensitive) - * */ - librariesBlock?: string | string[] -} - -export interface PlexApiOptions extends CommonSourceOptions { - /** + usersAllow: z.union([z.string(), z.literal(true), z.array(z.string())]).optional().meta({ + description: "Only scrobble for specific users (case-insensitive)" + }), + /** + * Do not scrobble for these users (case-insensitive) + * */ + usersBlock: z.union([z.string(), z.array(z.string())]).optional().meta({ + description: "Do not scrobble for these users (case-insensitive)" + }), + + /** + * Only scrobble if device or application name contains strings from this list (case-insensitive) + * */ + devicesAllow: z.union([z.string(), z.array(z.string())]).optional().meta({ + description: "Only scrobble if device or application name contains strings from this list (case-insensitive)" + }), + /** + * Do not scrobble if device or application name contains strings from this list (case-insensitive) + * */ + devicesBlock: z.union([z.string(), z.array(z.string())]).optional().meta({ + description: "Do not scrobble if device or application name contains strings from this list (case-insensitive)" + }), + + /** + * Only scrobble if library name contains string from this list (case-insensitive) + * */ + librariesAllow: z.union([z.string(), z.array(z.string())]).optional().meta({ + description: "Only scrobble if library name contains string from this list (case-insensitive)" + }), + /** + * Do not scrobble if library name contains strings from this list (case-insensitive) + * */ + librariesBlock: z.union([z.string(), z.array(z.string())]).optional().meta({ + description: "Do not scrobble if library name contains strings from this list (case-insensitive)" + }), +}); + +export type PlexApiData = z.infer; + +export const plexApiOptionsSchema = z.object({ + ...commonSourceOptionsSchema.shape, + /** * Ignore invalid cert errors when connecting to Plex - * + * * Useful for Plex servers using "Required" Secure Connections with self-signed certificates - * + * * Do not enable unless you know you need this. - * + * * @default false */ - ignoreInvalidCert?: boolean -} + ignoreInvalidCert: z.boolean().optional().meta({ + description: "Ignore invalid cert errors when connecting to Plex", + default: false + }), +}); + +export type PlexApiOptions = z.infer; + +export const plexApiSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, + data: plexApiDataSchema, + options: plexApiOptionsSchema.optional(), +}); -export interface PlexApiSourceConfig extends CommonSourceConfig { - data: PlexApiData - options?: PlexApiOptions -} +export type PlexApiSourceConfig = z.infer; -export interface PlexApiSourceAIOConfig extends PlexApiSourceConfig { - type: 'plex' -} +export const plexApiSourceAIOConfigSchema = z.object({ + ...plexApiSourceConfigSchema.shape, + type: z.literal('plex'), +}); -export type PlexCompatConfig = PlexApiSourceConfig; \ No newline at end of file +export type PlexApiSourceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/rocksky.ts b/src/backend/common/infrastructure/config/source/rocksky.ts index df1d03c1..4b84a64f 100644 --- a/src/backend/common/infrastructure/config/source/rocksky.ts +++ b/src/backend/common/infrastructure/config/source/rocksky.ts @@ -1,26 +1,45 @@ -import type {RockSkyData, RockSkyOptions} from "../client/rocksky.ts"; -import type {PollingOptions} from "../common.ts"; -import type {CommonSourceConfig, CommonSourceData, CommonSourceOptions} from "./index.ts"; +import * as z from "zod"; +import {rockSkyDataSchema, rockSkyOptionsSchema} from "../client/rocksky.ts"; +import {pollingOptionsSchema} from "../common.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema} from "./index.ts"; -export interface RockskySourceData extends RockSkyData, CommonSourceData, PollingOptions { -} +export const rockskySourceDataSchema = z.object({ + ...rockSkyDataSchema.shape, + ...commonSourceDataSchema.shape, + ...pollingOptionsSchema.shape, +}); -export interface RockskySourceOptions extends RockSkyOptions, CommonSourceOptions { +export type RockskySourceData = z.infer; -} +export const rockskySourceOptionsSchema = z.object({ + ...rockSkyOptionsSchema.shape, + ...commonSourceOptionsSchema.shape, +}); -export interface RockskySourceConfig extends CommonSourceConfig { +export type RockskySourceOptions = z.infer; + +export const rockskySourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, /** * When used in `rocksky.config` this tells multi-scrobbler whether to use this data to configure a source or client. * * @default source * @examples ["source"] * */ - configureAs: 'source' - data: RockskySourceData - options?: RockskySourceOptions -} - -export interface RockskySourceAIOConfig extends RockskySourceConfig { - type: 'rocksky' -} + configureAs: z.literal('source').meta({ + description: "When used in `rocksky.config` this tells multi-scrobbler whether to use this data to configure a source or client.", + default: "source", + examples: ["source"] + }), + data: rockskySourceDataSchema, + options: rockskySourceOptionsSchema.optional(), +}); + +export type RockskySourceConfig = z.infer; + +export const rockskySourceAIOConfigSchema = z.object({ + ...rockskySourceConfigSchema.shape, + type: z.literal('rocksky'), +}); + +export type RockskySourceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/sonos.ts b/src/backend/common/infrastructure/config/source/sonos.ts index df55522f..8b5ecba2 100644 --- a/src/backend/common/infrastructure/config/source/sonos.ts +++ b/src/backend/common/infrastructure/config/source/sonos.ts @@ -1,41 +1,67 @@ -import type {PollingOptions} from "../common.ts"; -import type {CommonSourceConfig, CommonSourceData, CommonSourceOptions} from "./index.ts"; +import * as z from "zod"; +import {pollingOptionsSchema} from "../common.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema} from "./index.ts"; -export interface SonosData extends CommonSourceData, PollingOptions { +export const sonosDataSchema = z.object({ + ...commonSourceDataSchema.shape, + ...pollingOptionsSchema.shape, /** * IP address of any connected Sonos speaker or device * * @examples ["192.168.0.170"] * */ - host: string + host: z.string().meta({ + description: "IP address of any connected Sonos speaker or device", + examples: ["192.168.0.170"] + }), /** * Only scrobble if device name contains strings from this list (case-insensitive) * */ - devicesAllow?: string | string[] + devicesAllow: z.union([z.string(), z.array(z.string())]).optional().meta({ + description: "Only scrobble if device name contains strings from this list (case-insensitive)" + }), /** * Do not scrobble if device name contains strings from this list (case-insensitive) * */ - devicesBlock?: string | string[] + devicesBlock: z.union([z.string(), z.array(z.string())]).optional().meta({ + description: "Do not scrobble if device name contains strings from this list (case-insensitive)" + }), /** * Only scrobble if the name of a group the playing device belongs to contains strings from this list (case-insensitive) * */ - groupsAllow?: string | string[] + groupsAllow: z.union([z.string(), z.array(z.string())]).optional().meta({ + description: "Only scrobble if the name of a group the playing device belongs to contains strings from this list (case-insensitive)" + }), /** * Do not scrobble if the name of a group the playing device belongs to contains strings from this list (case-insensitive) * */ - groupsBlock?: string | string[] -} - -export interface SonosSourceOptions extends CommonSourceOptions { - logEmptyPlayer?: boolean -} -export interface SonosSourceConfig extends CommonSourceConfig { - data: SonosData - options?: SonosSourceOptions -} - -export interface SonosSourceAIOConfig extends SonosSourceConfig { - type: 'sonos' -} + groupsBlock: z.union([z.string(), z.array(z.string())]).optional().meta({ + description: "Do not scrobble if the name of a group the playing device belongs to contains strings from this list (case-insensitive)" + }), +}); + +export type SonosData = z.infer; + +export const sonosSourceOptionsSchema = z.object({ + ...commonSourceOptionsSchema.shape, + logEmptyPlayer: z.boolean().optional(), +}); + +export type SonosSourceOptions = z.infer; + +export const sonosSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, + data: sonosDataSchema, + options: sonosSourceOptionsSchema.optional(), +}); + +export type SonosSourceConfig = z.infer; + +export const sonosSourceAIOConfigSchema = z.object({ + ...sonosSourceConfigSchema.shape, + type: z.literal('sonos'), +}); + +export type SonosSourceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/spotify.ts b/src/backend/common/infrastructure/config/source/spotify.ts index ab94ca20..cb7381af 100644 --- a/src/backend/common/infrastructure/config/source/spotify.ts +++ b/src/backend/common/infrastructure/config/source/spotify.ts @@ -1,26 +1,39 @@ -import type {PollingOptions} from "../common.ts"; -import type {CommonSourceConfig, CommonSourceData} from "./index.ts"; +import * as z from "zod"; +import {pollingOptionsSchema} from "../common.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; -export interface SpotifySourceData extends CommonSourceData, PollingOptions { +export const spotifySourceDataSchema = z.object({ + ...commonSourceDataSchema.shape, + ...pollingOptionsSchema.shape, /** * spotify client id * * @examples ["787c921a2a2ab42320831aba0c8f2fc2"] * */ - clientId: string + clientId: z.string().meta({ + description: "spotify client id", + examples: ["787c921a2a2ab42320831aba0c8f2fc2"] + }), /** * spotify client secret * * @examples ["ec42e09d5ae0ee0f0816ca151008412a"] * */ - clientSecret: string + clientSecret: z.string().meta({ + description: "spotify client secret", + examples: ["ec42e09d5ae0ee0f0816ca151008412a"] + }), /** * spotify redirect URI -- required only if not the default shown here. URI must end in "callback" * * @default "http://localhost:9078/callback" * @examples ["http://localhost:9078/callback"] * */ - redirectUri?: string + redirectUri: z.string().optional().meta({ + description: "spotify redirect URI -- required only if not the default shown here.", + default: "http://localhost:9078/callback", + examples: ["http://localhost:9078/callback"] + }), /** * How long to wait before polling the source API for new tracks (in seconds) * @@ -36,13 +49,25 @@ export interface SpotifySourceData extends CommonSourceData, PollingOptions { * @default 10 * @examples [10] * */ - interval?: number -} + interval: z.number().optional().meta({ + description: "How long to wait before polling the source API for new tracks (in seconds)", + default: 10, + examples: [10] + }), +}); -export interface SpotifySourceConfig extends CommonSourceConfig { - data: SpotifySourceData -} +export type SpotifySourceData = z.infer; -export interface SpotifySourceAIOConfig extends SpotifySourceConfig { - type: 'spotify' -} +export const spotifySourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, + data: spotifySourceDataSchema, +}); + +export type SpotifySourceConfig = z.infer; + +export const spotifySourceAIOConfigSchema = z.object({ + ...spotifySourceConfigSchema.shape, + type: z.literal('spotify'), +}); + +export type SpotifySourceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/subsonic.ts b/src/backend/common/infrastructure/config/source/subsonic.ts index aa917e03..f2c730a5 100644 --- a/src/backend/common/infrastructure/config/source/subsonic.ts +++ b/src/backend/common/infrastructure/config/source/subsonic.ts @@ -1,26 +1,37 @@ -import type {PollingOptions} from "../common.ts"; -import type {CommonSourceConfig, CommonSourceData} from "./index.ts"; +import * as z from "zod"; +import {pollingOptionsSchema} from "../common.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; -export interface SubsonicData extends CommonSourceData, PollingOptions { +export const subsonicDataSchema = z.object({ + ...commonSourceDataSchema.shape, + ...pollingOptionsSchema.shape, /** * URL of the subsonic media server to query * * @examples ["http://airsonic.local"] * */ - url: string + url: z.string().meta({ + description: "URL of the subsonic media server to query", + examples: ["http://airsonic.local"] + }), /** * Username to login to the server with * * @example ["MyUser"] * */ - user: string + user: z.string().meta({ + description: "Username to login to the server with" + }), /** * Password for the user to login to the server with * * @examples ["MyPassword"] * */ - password: string + password: z.string().meta({ + description: "Password for the user to login to the server with", + examples: ["MyPassword"] + }), /** * How long to wait before polling the source API for new tracks (in seconds) @@ -28,7 +39,11 @@ export interface SubsonicData extends CommonSourceData, PollingOptions { * @default 10 * @examples [10] * */ - interval?: number + interval: z.number().optional().meta({ + description: "How long to wait before polling the source API for new tracks (in seconds)", + default: 10, + examples: [10] + }), /** * When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds) @@ -36,7 +51,11 @@ export interface SubsonicData extends CommonSourceData, PollingOptions { * @default 30 * @examples [30] * */ - maxInterval?: number + maxInterval: z.number().optional().meta({ + description: "When there has been no new activity from the Source API multi-scrobbler will gradually increase the wait time between polling up to this value (in seconds)", + default: 30, + examples: [30] + }), /** * If your subsonic server is using self-signed certs you may need to disable TLS errors in order to get a connection @@ -45,7 +64,10 @@ export interface SubsonicData extends CommonSourceData, PollingOptions { * * @default false * */ - ignoreTlsErrors?: boolean + ignoreTlsErrors: z.boolean().optional().meta({ + description: "If your subsonic server is using self-signed certs you may need to disable TLS errors in order to get a connection", + default: false + }), /** * Older Subsonic versions, and some badly implemented servers (Nextcloud), use legacy authentication which sends your password in CLEAR TEXT. This is less secure than the newer, recommended hashing authentication method but in some cases it is needed. See "Authentication" section here => https://www.subsonic.org/pages/api.jsp @@ -54,19 +76,33 @@ export interface SubsonicData extends CommonSourceData, PollingOptions { * * @default false * */ - legacyAuthentication?: boolean + legacyAuthentication: z.boolean().optional().meta({ + description: "Older Subsonic versions, and some badly implemented servers (Nextcloud), use legacy authentication which sends your password in CLEAR TEXT.", + default: false + }), /** * Only scrobble for specific users (case-insensitive) * * If undefined or an empty string/list MS will scrobble activity from all users * */ - usersAllow?: string | string[] -} -export interface SubSonicSourceConfig extends CommonSourceConfig { - data: SubsonicData -} + usersAllow: z.union([z.string(), z.array(z.string())]).optional().meta({ + description: "Only scrobble for specific users (case-insensitive)" + }), +}); -export interface SubsonicSourceAIOConfig extends SubSonicSourceConfig { - type: 'subsonic' -} +export type SubsonicData = z.infer; + +export const subSonicSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, + data: subsonicDataSchema, +}); + +export type SubSonicSourceConfig = z.infer; + +export const subsonicSourceAIOConfigSchema = z.object({ + ...subSonicSourceConfigSchema.shape, + type: z.literal('subsonic'), +}); + +export type SubsonicSourceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/tealfm.ts b/src/backend/common/infrastructure/config/source/tealfm.ts index 802db69d..75470aa4 100644 --- a/src/backend/common/infrastructure/config/source/tealfm.ts +++ b/src/backend/common/infrastructure/config/source/tealfm.ts @@ -1,28 +1,47 @@ -import type {TealData, TealOptions} from "../client/tealfm.ts"; -import type {PollingOptions} from "../common.ts"; -import type {CommonSourceConfig, CommonSourceData, CommonSourceOptions} from "./index.ts"; +import * as z from "zod"; +import {tealClientOptionsSchema, tealDataSchema} from "../client/tealfm.ts"; +import {pollingOptionsSchema} from "../common.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema} from "./index.ts"; +export const tealSourceDataSchema = z.object({ + ...tealDataSchema.shape, + ...commonSourceDataSchema.shape, + ...pollingOptionsSchema.shape, + serviceAllow: z.array(z.string()).optional(), + serviceDeny: z.array(z.string()).optional(), +}); -export interface TealSourceData extends TealData, CommonSourceData, PollingOptions { - serviceAllow?: string[] - serviceDeny?: string[] -} +export type TealSourceData = z.infer; -export interface TealSourceConfig extends CommonSourceConfig { +export const tealSourceOptionsSchema = z.object({ + ...commonSourceOptionsSchema.shape, + ...tealClientOptionsSchema.shape, +}); + +export type TealSourceOptions = z.infer; + +export const tealSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, /** * Should always be `souce` when using Tealfm as a Source * * @default source * @examples ["source"] * */ - configureAs: 'source' - data: TealSourceData - options?: TealSourceOptions -} + configureAs: z.literal('source').meta({ + description: "Should always be `souce` when using Tealfm as a Source", + default: "source", + examples: ["source"] + }), + data: tealSourceDataSchema, + options: tealSourceOptionsSchema.optional(), +}); + +export type TealSourceConfig = z.infer; -export interface TealSourceOptions extends CommonSourceOptions, TealOptions { -} +export const tealSourceAIOConfigSchema = z.object({ + ...tealSourceConfigSchema.shape, + type: z.literal('tealfm'), +}); -export interface TealSourceAIOConfig extends TealSourceConfig { - type: 'tealfm' -} \ No newline at end of file +export type TealSourceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/vlc.ts b/src/backend/common/infrastructure/config/source/vlc.ts index f75c829f..10a4d992 100644 --- a/src/backend/common/infrastructure/config/source/vlc.ts +++ b/src/backend/common/infrastructure/config/source/vlc.ts @@ -1,8 +1,11 @@ +import * as z from "zod"; import type {VlcMeta} from "vlc-client/dist/Types.js"; -import type {PollingOptions} from "../common.ts"; -import type {CommonSourceConfig, CommonSourceData, CommonSourceOptions} from "./index.ts"; +import {pollingOptionsSchema} from "../common.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema} from "./index.ts"; -export interface VLCData extends CommonSourceData, PollingOptions { +export const vlcDataSchema = z.object({ + ...commonSourceDataSchema.shape, + ...pollingOptionsSchema.shape, /** * URL:PORT of the VLC server to connect to * @@ -11,27 +14,41 @@ export interface VLCData extends CommonSourceData, PollingOptions { * @examples ["localhost:8080"] * @default "localhost:8080" * */ - url?: string + url: z.string().optional().meta({ + description: "URL:PORT of the VLC server to connect to", + default: "localhost:8080", + examples: ["localhost:8080"] + }), /** * Password for the server * */ - password: string + password: z.string().meta({ + description: "Password for the server" + }), -} +}); + +export type VLCData = z.infer; -export interface VLCSourceOptions extends CommonSourceOptions { +export const vlcSourceOptionsSchema = z.object({ + ...commonSourceOptionsSchema.shape, /** A list of regular expressions to use to extract metadata (title, album, artist) from a filename * * Used when VLC reports only the filename for the current audio track * */ - filenamePatterns?: string | string[] + filenamePatterns: z.union([z.string(), z.array(z.string())]).optional().meta({ + description: "A list of regular expressions to use to extract metadata (title, album, artist) from a filename" + }), /** * Log to DEBUG when a filename-only track is matched or not matched by filenamePatterns * * @default false * */ - logFilenamePatterns?: boolean + logFilenamePatterns: z.boolean().optional().meta({ + description: "Log to DEBUG when a filename-only track is matched or not matched by filenamePatterns", + default: false + }), /** * Dump all the metadata VLC reports for an audio track to DEBUG. * @@ -39,17 +56,28 @@ export interface VLCSourceOptions extends CommonSourceOptions { * * @default false * */ - dumpVlcMetadata?: boolean -} + dumpVlcMetadata: z.boolean().optional().meta({ + description: "Dump all the metadata VLC reports for an audio track to DEBUG.", + default: false + }), +}); -export interface VLCSourceConfig extends CommonSourceConfig { - data: VLCData - options?: VLCSourceOptions -} +export type VLCSourceOptions = z.infer; -export interface VLCSourceAIOConfig extends VLCSourceConfig { - type: 'vlc' -} +export const vlcSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, + data: vlcDataSchema, + options: vlcSourceOptionsSchema.optional(), +}); + +export type VLCSourceConfig = z.infer; + +export const vlcSourceAIOConfigSchema = z.object({ + ...vlcSourceConfigSchema.shape, + type: z.literal('vlc'), +}); + +export type VLCSourceAIOConfig = z.infer; export type PlayerState = 'playing' | 'stopped' | 'paused'; diff --git a/src/backend/common/infrastructure/config/source/webscrobbler.ts b/src/backend/common/infrastructure/config/source/webscrobbler.ts index f90d1c67..1b534c14 100644 --- a/src/backend/common/infrastructure/config/source/webscrobbler.ts +++ b/src/backend/common/infrastructure/config/source/webscrobbler.ts @@ -1,6 +1,8 @@ -import type {CommonSourceConfig, CommonSourceData} from "./index.ts"; +import * as z from "zod"; +import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; -export interface WebScrobblerData extends CommonSourceData { +export const webScrobblerDataSchema = z.object({ + ...commonSourceDataSchema.shape, /** * The URL ending that should be used to identify scrobbles for this source * @@ -17,27 +19,43 @@ export interface WebScrobblerData extends CommonSourceData { * * If no slug is found from an extension's incoming webhook event the first WebScrobbler source without a slug will be used * */ - slug?: string | null + slug: z.union([z.string(), z.null()]).optional().meta({ + description: "The URL ending that should be used to identify scrobbles for this source" + }), /** * Block scrobbling from specific WebScrobbler Connectors * - * @examples [["youtube"]] + * @examples ["youtube"] * */ - blacklist?: string | string[] + blacklist: z.union([z.string(), z.array(z.string())]).optional().meta({ + description: "Block scrobbling from specific WebScrobbler Connectors", + examples: ["youtube"] + }), /** * Only allow scrobbling from specific WebScrobbler Connectors * - * @examples [["mixcloud","soundcloud","bandcamp"]] + * @examples ["mixcloud","soundcloud","bandcamp"] * */ - whitelist?: string | string[] -} + whitelist: z.union([z.string(), z.array(z.string())]).optional().meta({ + description: "Only allow scrobbling from specific WebScrobbler Connectors", + examples: ["mixcloud", "soundcloud", "bandcamp"] + }), +}); -export interface WebScrobblerSourceConfig extends CommonSourceConfig { - data?: WebScrobblerData -} +export type WebScrobblerData = z.infer; -export interface WebScrobblerSourceAIOConfig extends WebScrobblerSourceConfig { - type: 'webscrobbler' -} +export const webScrobblerSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, + data: webScrobblerDataSchema.optional(), +}); + +export type WebScrobblerSourceConfig = z.infer; + +export const webScrobblerSourceAIOConfigSchema = z.object({ + ...webScrobblerSourceConfigSchema.shape, + type: z.literal('webscrobbler'), +}); + +export type WebScrobblerSourceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/ymbridge.ts b/src/backend/common/infrastructure/config/source/ymbridge.ts index b9dde3a8..d4adc7e6 100644 --- a/src/backend/common/infrastructure/config/source/ymbridge.ts +++ b/src/backend/common/infrastructure/config/source/ymbridge.ts @@ -1,17 +1,32 @@ -import type {PollingOptions} from "../common.ts"; -import type {CommonSourceConfig, CommonSourceData} from "./index.ts"; +import * as z from "zod"; +import {pollingOptionsSchema} from "../common.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema} from "./index.ts"; -export interface YandexMusicBridgeData extends CommonSourceData, PollingOptions { +export const yandexMusicBridgeDataSchema = z.object({ + ...commonSourceDataSchema.shape, + ...pollingOptionsSchema.shape, /** URL of the local Python bridge, for example http://yandex-music-bridge:9980 */ - url: string + url: z.string().meta({ + description: "URL of the local Python bridge, for example http://yandex-music-bridge:9980" + }), /** Optional API key sent as X-API-Key to the bridge */ - apiKey?: string -} + apiKey: z.string().optional().meta({ + description: "Optional API key sent as X-API-Key to the bridge" + }), +}); -export interface YandexMusicBridgeSourceConfig extends CommonSourceConfig { - data?: YandexMusicBridgeData -} +export type YandexMusicBridgeData = z.infer; -export interface YandexMusicBridgeSourceAIOConfig extends YandexMusicBridgeSourceConfig { - type: 'ymbridge' -} +export const yandexMusicBridgeSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, + data: yandexMusicBridgeDataSchema.optional(), +}); + +export type YandexMusicBridgeSourceConfig = z.infer; + +export const yandexMusicBridgeSourceAIOConfigSchema = z.object({ + ...yandexMusicBridgeSourceConfigSchema.shape, + type: z.literal('ymbridge'), +}); + +export type YandexMusicBridgeSourceAIOConfig = z.infer; diff --git a/src/backend/common/infrastructure/config/source/ytmusic.ts b/src/backend/common/infrastructure/config/source/ytmusic.ts index 1d08c42a..d5339e38 100644 --- a/src/backend/common/infrastructure/config/source/ytmusic.ts +++ b/src/backend/common/infrastructure/config/source/ytmusic.ts @@ -1,41 +1,53 @@ -import type {PollingOptions} from "../common.ts"; -import type {CommonSourceConfig, CommonSourceData, CommonSourceOptions} from "./index.ts"; +import * as z from "zod"; +import {pollingOptionsSchema} from "../common.ts"; +import {commonSourceConfigSchema, commonSourceDataSchema, commonSourceOptionsSchema} from "./index.ts"; -export interface InnertubeOptions { +export const innertubeOptionsSchema = z.object({ /** * Proof of Origin token - * + * * May be required if YTM starts returning 403 - * + * * @see https://github.com/yt-dlp/yt-dlp/wiki/Extractors#po-token-guide */ - po_token?: string + po_token: z.string().optional().meta({ + description: "Proof of Origin token" + }), /** * Visitor ID value found in VISITOR_INFO1_LIVE or visitorData cookie - * + * * May be required if YTM starts returning 403 - * + * * @see https://github.com/yt-dlp/yt-dlp/wiki/Extractors#po-token-guide */ - visitor_data?: string + visitor_data: z.string().optional().meta({ + description: "Visitor ID value found in VISITOR_INFO1_LIVE or visitorData cookie" + }), /** * If account login results in being able to choose multiple account, use a zero-based index to choose which one to monitor - * + * * @examples [0,1] */ - account_index?: number + account_index: z.number().optional().meta({ + description: "If account login results in being able to choose multiple account, use a zero-based index to choose which one to monitor", + examples: [0, 1] + }), + + location: z.string().optional(), + lang: z.string().optional(), + generate_session_locally: z.boolean().optional(), + device_category: z.string().optional(), + client_type: z.string().optional(), + timezone: z.string().optional(), +}); - location?: string - lang?: string - generate_session_locally?: boolean - device_category?: string - client_type?: string - timezone?: string -} +export type InnertubeOptions = z.infer; -export interface YTMusicData extends CommonSourceData, PollingOptions { +export const ytMusicDataSchema = z.object({ + ...commonSourceDataSchema.shape, + ...pollingOptionsSchema.shape, /** * The cookie retrieved from the Request Headers of music.youtube.com after logging in. * @@ -43,55 +55,80 @@ export interface YTMusicData extends CommonSourceData, PollingOptions { * * @examples ["VISITOR_INFO1_LIVE=jMp2xA1Xz2_PbVc; __Secure-3PAPISID=3AxsXpy0M/AkISpjek; ..."] * */ - cookie?: string + cookie: z.string().optional().meta({ + description: "The cookie retrieved from the Request Headers of music.youtube.com after logging in.", + examples: ["VISITOR_INFO1_LIVE=jMp2xA1Xz2_PbVc; __Secure-3PAPISID=3AxsXpy0M/AkISpjek; ..."] + }), /** * Google Cloud Console project OAuth Client ID - * + * * Generated from a custom OAuth Client, see docs */ - clientId?: string - + clientId: z.string().optional().meta({ + description: "Google Cloud Console project OAuth Client ID" + }), + /** * Google Cloud Console project OAuth Client Secret - * + * * Generated from a custom OAuth Client, see docs */ - clientSecret?: string + clientSecret: z.string().optional().meta({ + description: "Google Cloud Console project OAuth Client Secret" + }), /** * Google Cloud Console project OAuth Client Authorized redirect URI - * + * * Generated from a custom OAuth Client, see docs. multi-scrobbler will generate a default based on BASE_URL. * Only specify this if the default does not work for you. */ - redirectUri?: string + redirectUri: z.string().optional().meta({ + description: "Google Cloud Console project OAuth Client Authorized redirect URI" + }), /** * Additional options for authorization and tailoring YTM client */ - innertubeOptions?: InnertubeOptions -} + innertubeOptions: innertubeOptionsSchema.optional().meta({ + description: "Additional options for authorization and tailoring YTM client" + }), +}); -export interface YTMusicSourceConfig extends CommonSourceConfig { - data?: YTMusicData - options?: CommonSourceOptions & { +export type YTMusicData = z.infer; + +export const ytMusicSourceConfigSchema = z.object({ + ...commonSourceConfigSchema.shape, + data: ytMusicDataSchema.optional(), + options: z.object({ + ...commonSourceOptionsSchema.shape, /** * When true MS will log to DEBUG all of the credentials data it receives from YTM * */ - logAuth?: boolean + logAuth: z.boolean().optional().meta({ + description: "When true MS will log to DEBUG all of the credentials data it receives from YTM" + }), /** * Always log history diff - * + * * By default MS will log to `WARN` if history diff is inconsistent but does not log if diff is expected (on new tracks found) * Set this to `true` to ALWAYS log diff on new tracks. Expected diffs will log to `DEBUG` and inconsistent diffs will continue to log to `WARN` - * + * * @default false */ - logDiff?: boolean - } -} + logDiff: z.boolean().optional().meta({ + description: "Always log history diff", + default: false + }), + }).optional(), +}); + +export type YTMusicSourceConfig = z.infer; + +export const ytMusicSourceAIOConfigSchema = z.object({ + ...ytMusicSourceConfigSchema.shape, + type: z.literal('ytmusic'), +}); -export interface YTMusicSourceAIOConfig extends YTMusicSourceConfig { - type: 'ytmusic' -} +export type YTMusicSourceAIOConfig = z.infer;