diff --git a/backend/src/media-sources/media-source.dto.ts b/backend/src/media-sources/media-source.dto.ts index 6c7df59..934a96c 100644 --- a/backend/src/media-sources/media-source.dto.ts +++ b/backend/src/media-sources/media-source.dto.ts @@ -11,7 +11,7 @@ export class MediaSourceDto extends PickAndPartial( export class UpdateOrCreateMediaSourceDto extends PickAndPartial( MediaSource, ['pluginSettings', 'pluginId'], - ['enabled', 'id', 'adminControlled', 'name', 'priority'], + ['id', 'adminControlled', 'name', 'priority'], ) {} export class UpdateMediaSourceDto extends OmitType( diff --git a/backend/src/media-sources/media-source.entity.ts b/backend/src/media-sources/media-source.entity.ts index 0987f98..fd1ee8f 100644 --- a/backend/src/media-sources/media-source.entity.ts +++ b/backend/src/media-sources/media-source.entity.ts @@ -41,7 +41,7 @@ export class MediaSource { @Column({ default: false }) adminControlled: boolean; - @ApiProperty({ required: false, type: 'object' }) + @ApiProperty({ required: false, type: 'object', additionalProperties: true }) @Column('json', { default: '{}' }) pluginSettings: SourceProviderSettings = {}; diff --git a/backend/src/media-sources/media-sources.service.ts b/backend/src/media-sources/media-sources.service.ts index 2b9f0a1..b232806 100644 --- a/backend/src/media-sources/media-sources.service.ts +++ b/backend/src/media-sources/media-sources.service.ts @@ -5,6 +5,7 @@ import { Repository } from 'typeorm'; import { UpdateOrCreateMediaSourceDto } from './media-source.dto'; import { MediaSource } from './media-source.entity'; import { MEIDA_SOURCE_REPOSITORY } from './media-source.providers'; +import { SourceProvidersService } from 'src/source-providers/source-providers.service'; export enum MediaSourcesServiceError { SourceNotFound = 'SourceNotFound', @@ -16,6 +17,7 @@ export class MediaSourcesService { constructor( @Inject(MEIDA_SOURCE_REPOSITORY) private readonly mediaSourceRepository: Repository, + private sourceProvidersService: SourceProvidersService, private readonly usersService: UsersService, ) {} @@ -86,8 +88,20 @@ export class MediaSourcesService { source.adminControlled = sourceDto.adminControlled ?? source.adminControlled; - source.enabled = sourceDto.enabled ?? source.enabled; - source.pluginSettings = sourceDto.pluginSettings ?? source.pluginSettings; + if (sourceDto.pluginSettings !== undefined) { + let valid = false; + const provider = this.sourceProvidersService.getProvider(source.pluginId); + + if (provider) { + const validationRes = await provider.settingsManager.validateSettings( + sourceDto.pluginSettings, + ); + valid = validationRes.isValid; + } + + source.pluginSettings = sourceDto.pluginSettings; + source.enabled = !!valid; + } source.name = sourceDto.name ?? source.name; let priority = 0; diff --git a/src/lib/apis/reiverr/reiverr.openapi.ts b/src/lib/apis/reiverr/reiverr.openapi.ts index 4f2d5f3..346a13a 100644 --- a/src/lib/apis/reiverr/reiverr.openapi.ts +++ b/src/lib/apis/reiverr/reiverr.openapi.ts @@ -55,7 +55,7 @@ export interface MediaSource { enabled?: boolean; /** @default false */ adminControlled?: boolean; - pluginSettings?: object; + pluginSettings?: Record; priority: number; } @@ -118,38 +118,40 @@ export interface UpdateUserDto { oldPassword?: string; } -export interface PlayStateDto { - id: string; - tmdbId: number; - mediaType: 'Movie' | 'Series' | 'Episode'; - userId: string; - season?: number; - episode?: number; - /** - * Whether the user has watched this media - * @default false - */ - watched: boolean; - /** - * A number between 0 and 1 - * @default false - * @example 0.5 - */ - progress: number; - /** Last time the user played this media */ - lastPlayedAt: string; +export interface SignInDto { + name: string; + password: string; } -export interface MovieUserDataDto { - tmdbId: string; - inLibrary: boolean; - playState?: PlayStateDto; +export interface SignInResponse { + accessToken: string; + user: UserDto; } -export interface SeriesUserDataDto { - tmdbId: string; - inLibrary: boolean; - playStates: PlayStateDto[]; +export interface PluginSettingsTemplateDto { + /** @example {"setting1":"string","setting2":{"type":"link","url":"https://example.com"}} */ + settings: Record; +} + +export interface PluginSettingsDto { + /** @example {"setting1":"some value","setting2":12345,"setting3":true,"setting4":{"nestedKey":"nestedValue"}} */ + settings: Record; +} + +export interface ValidationResponseDto { + /** @example true */ + isValid: boolean; + /** @example {"setting1":"error message","setting2":"another error message"} */ + errors: Record; + /** @example {"setting1":"new value","setting2":"another new value"} */ + replace: Record; +} + +export interface SourceProviderCapabilitiesDto { + moviePlayback: boolean; + episodePlayback: boolean; + movieIndexing: boolean; + episodeIndexing: boolean; } export interface PaginatedResponseDto { @@ -429,16 +431,69 @@ export interface StreamDto { export interface UpdateOrCreateMediaSourceDto { pluginId: string; - pluginSettings?: object; + pluginSettings?: Record; id?: string; name?: string; /** @default false */ - enabled?: boolean; - /** @default false */ adminControlled?: boolean; priority?: number; } +export interface PlayStateDto { + id: string; + tmdbId: number; + mediaType: 'Movie' | 'Series' | 'Episode'; + userId: string; + season?: number; + episode?: number; + /** + * Whether the user has watched this media + * @default false + */ + watched: boolean; + /** + * A number between 0 and 1 + * @default false + * @example 0.5 + */ + progress: number; + /** Last time the user played this media */ + lastPlayedAt: string; +} + +export interface MovieUserDataDto { + tmdbId: string; + inLibrary: boolean; + playState?: PlayStateDto; +} + +export interface SeriesUserDataDto { + tmdbId: string; + inLibrary: boolean; + playStates: PlayStateDto[]; +} + +export interface UpdatePlayStateDto { + id?: string; + tmdbId?: number; + mediaType?: 'Movie' | 'Series' | 'Episode'; + season?: number; + episode?: number; + /** + * Whether the user has watched this media + * @default false + */ + watched?: boolean; + /** + * A number between 0 and 1 + * @default false + * @example 0.5 + */ + progress?: number; + /** Last time the user played this media */ + lastPlayedAt?: string; +} + export interface MovieDto { id?: string; tmdbId: string; @@ -463,63 +518,6 @@ export interface SuccessResponseDto { success: boolean; } -export interface UpdatePlayStateDto { - id?: string; - tmdbId?: number; - mediaType?: 'Movie' | 'Series' | 'Episode'; - season?: number; - episode?: number; - /** - * Whether the user has watched this media - * @default false - */ - watched?: boolean; - /** - * A number between 0 and 1 - * @default false - * @example 0.5 - */ - progress?: number; - /** Last time the user played this media */ - lastPlayedAt?: string; -} - -export interface SignInDto { - name: string; - password: string; -} - -export interface SignInResponse { - accessToken: string; - user: UserDto; -} - -export interface PluginSettingsTemplateDto { - /** @example {"setting1":"string","setting2":{"type":"link","url":"https://example.com"}} */ - settings: Record; -} - -export interface PluginSettingsDto { - /** @example {"setting1":"some value","setting2":12345,"setting3":true,"setting4":{"nestedKey":"nestedValue"}} */ - settings: Record; -} - -export interface ValidationResponseDto { - /** @example true */ - isValid: boolean; - /** @example {"setting1":"error message","setting2":"another error message"} */ - errors: Record; - /** @example {"setting1":"new value","setting2":"another new value"} */ - replace: Record; -} - -export interface SourceProviderCapabilitiesDto { - moviePlayback: boolean; - episodePlayback: boolean; - movieIndexing: boolean; - episodeIndexing: boolean; -} - import type { AxiosInstance, AxiosRequestConfig, @@ -805,6 +803,42 @@ export class Api extends HttpClient + this.request({ + path: `/api/users/${userId}/sources`, + method: 'PUT', + body: data, + type: ContentType.Json, + format: 'json', + ...params + }), + + /** + * No description + * + * @tags users + * @name DeleteSource + * @request DELETE:/api/users/{userId}/sources/{sourceId} + */ + deleteSource: (sourceId: string, userId: string, params: RequestParams = {}) => + this.request({ + path: `/api/users/${userId}/sources/${sourceId}`, + method: 'DELETE', + format: 'json', + ...params + }), + /** * No description * @@ -856,100 +890,6 @@ export class Api extends HttpClient - this.request({ - path: `/api/users/${userId}/sources`, - method: 'PUT', - body: data, - type: ContentType.Json, - format: 'json', - ...params - }), - - /** - * No description - * - * @tags users - * @name DeleteSource - * @request DELETE:/api/users/{userId}/sources/{sourceId} - */ - deleteSource: (sourceId: string, userId: string, params: RequestParams = {}) => - this.request({ - path: `/api/users/${userId}/sources/${sourceId}`, - method: 'DELETE', - format: 'json', - ...params - }), - - /** - * No description - * - * @tags users - * @name GetLibraryItems - * @request GET:/api/users/{userId}/library - */ - getLibraryItems: (userId: string, params: RequestParams = {}) => - this.request< - PaginatedResponseDto & { - items: LibraryItemDto[]; - }, - any - >({ - path: `/api/users/${userId}/library`, - method: 'GET', - format: 'json', - ...params - }), - - /** - * No description - * - * @tags users - * @name AddLibraryItem - * @request PUT:/api/users/{userId}/library/tmdb/{tmdbId} - */ - addLibraryItem: ( - userId: string, - tmdbId: string, - query: { - mediaType: 'Movie' | 'Series' | 'Episode'; - }, - params: RequestParams = {} - ) => - this.request({ - path: `/api/users/${userId}/library/tmdb/${tmdbId}`, - method: 'PUT', - query: query, - format: 'json', - ...params - }), - - /** - * No description - * - * @tags users - * @name RemoveLibraryItem - * @request DELETE:/api/users/{userId}/library/tmdb/{tmdbId} - */ - removeLibraryItem: (userId: string, tmdbId: string, params: RequestParams = {}) => - this.request({ - path: `/api/users/${userId}/library/tmdb/${tmdbId}`, - method: 'DELETE', - format: 'json', - ...params - }), - /** * No description * @@ -1026,6 +966,275 @@ export class Api extends HttpClient + this.request< + PaginatedResponseDto & { + items: LibraryItemDto[]; + }, + any + >({ + path: `/api/users/${userId}/library`, + method: 'GET', + format: 'json', + ...params + }), + + /** + * No description + * + * @tags users + * @name AddLibraryItem + * @request PUT:/api/users/{userId}/library/tmdb/{tmdbId} + */ + addLibraryItem: ( + userId: string, + tmdbId: string, + query: { + mediaType: 'Movie' | 'Series' | 'Episode'; + }, + params: RequestParams = {} + ) => + this.request({ + path: `/api/users/${userId}/library/tmdb/${tmdbId}`, + method: 'PUT', + query: query, + format: 'json', + ...params + }), + + /** + * No description + * + * @tags users + * @name RemoveLibraryItem + * @request DELETE:/api/users/{userId}/library/tmdb/{tmdbId} + */ + removeLibraryItem: (userId: string, tmdbId: string, params: RequestParams = {}) => + this.request({ + path: `/api/users/${userId}/library/tmdb/${tmdbId}`, + method: 'DELETE', + format: 'json', + ...params + }) + }; + api = { + /** + * No description + * + * @name SignIn + * @request POST:/api/auth + */ + signIn: (data: SignInDto, params: RequestParams = {}) => + this.request< + SignInResponse, + { + /** @example 401 */ + statusCode: number; + /** @example "Unauthorized" */ + message: string; + /** @example "Unauthorized" */ + error?: string; + } + >({ + path: `/api/auth`, + method: 'POST', + body: data, + type: ContentType.Json, + format: 'json', + ...params + }), + + /** + * No description + * + * @name GetHello + * @request GET:/api + */ + getHello: (params: RequestParams = {}) => + this.request({ + path: `/api`, + method: 'GET', + ...params + }), + + /** + * No description + * + * @name TmdbProxyGet + * @request GET:/api/tmdb/v3/proxy/* + */ + tmdbProxyGet: (params: RequestParams = {}) => + this.request({ + path: `/api/tmdb/v3/proxy/*`, + method: 'GET', + ...params + }), + + /** + * No description + * + * @name TmdbProxyPost + * @request POST:/api/tmdb/v3/proxy/* + */ + tmdbProxyPost: (params: RequestParams = {}) => + this.request({ + path: `/api/tmdb/v3/proxy/*`, + method: 'POST', + ...params + }), + + /** + * No description + * + * @name TmdbProxyPut + * @request PUT:/api/tmdb/v3/proxy/* + */ + tmdbProxyPut: (params: RequestParams = {}) => + this.request({ + path: `/api/tmdb/v3/proxy/*`, + method: 'PUT', + ...params + }), + + /** + * No description + * + * @name TmdbProxyDelete + * @request DELETE:/api/tmdb/v3/proxy/* + */ + tmdbProxyDelete: (params: RequestParams = {}) => + this.request({ + path: `/api/tmdb/v3/proxy/*`, + method: 'DELETE', + ...params + }), + + /** + * No description + * + * @name TmdbProxyPatch + * @request PATCH:/api/tmdb/v3/proxy/* + */ + tmdbProxyPatch: (params: RequestParams = {}) => + this.request({ + path: `/api/tmdb/v3/proxy/*`, + method: 'PATCH', + ...params + }), + + /** + * No description + * + * @name TmdbProxyOptions + * @request OPTIONS:/api/tmdb/v3/proxy/* + */ + tmdbProxyOptions: (params: RequestParams = {}) => + this.request({ + path: `/api/tmdb/v3/proxy/*`, + method: 'OPTIONS', + ...params + }), + + /** + * No description + * + * @name TmdbProxyHead + * @request HEAD:/api/tmdb/v3/proxy/* + */ + tmdbProxyHead: (params: RequestParams = {}) => + this.request({ + path: `/api/tmdb/v3/proxy/*`, + method: 'HEAD', + ...params + }), + + /** + * No description + * + * @name TmdbProxySearch + * @request SEARCH:/api/tmdb/v3/proxy/* + */ + tmdbProxySearch: (params: RequestParams = {}) => + this.request({ + path: `/api/tmdb/v3/proxy/*`, + method: 'SEARCH', + ...params + }) + }; + providers = { + /** + * No description + * + * @tags providers + * @name GetSourceProviders + * @request GET:/api/providers + */ + getSourceProviders: (params: RequestParams = {}) => + this.request({ + path: `/api/providers`, + method: 'GET', + format: 'json', + ...params + }), + + /** + * No description + * + * @tags providers + * @name GetSourceSettingsTemplate + * @request GET:/api/providers/{providerId}/settings/template + */ + getSourceSettingsTemplate: (providerId: string, params: RequestParams = {}) => + this.request({ + path: `/api/providers/${providerId}/settings/template`, + method: 'GET', + format: 'json', + ...params + }), + + /** + * No description + * + * @tags providers + * @name ValidateSourceSettings + * @request POST:/api/providers/{providerId}/settings/validate + */ + validateSourceSettings: ( + providerId: string, + data: PluginSettingsDto, + params: RequestParams = {} + ) => + this.request({ + path: `/api/providers/${providerId}/settings/validate`, + method: 'POST', + body: data, + type: ContentType.Json, + format: 'json', + ...params + }), + + /** + * No description + * + * @tags providers + * @name GetSourceCapabilities + * @request GET:/api/providers/{providerId}/capabilities + */ + getSourceCapabilities: (providerId: string, params: RequestParams = {}) => + this.request({ + path: `/api/providers/${providerId}/capabilities`, + method: 'GET', + format: 'json', + ...params }) }; sources = { @@ -1393,215 +1602,4 @@ export class Api extends HttpClient - this.request< - SignInResponse, - { - /** @example 401 */ - statusCode: number; - /** @example "Unauthorized" */ - message: string; - /** @example "Unauthorized" */ - error?: string; - } - >({ - path: `/api/auth`, - method: 'POST', - body: data, - type: ContentType.Json, - format: 'json', - ...params - }), - - /** - * No description - * - * @name GetHello - * @request GET:/api - */ - getHello: (params: RequestParams = {}) => - this.request({ - path: `/api`, - method: 'GET', - ...params - }), - - /** - * No description - * - * @name TmdbProxyGet - * @request GET:/api/tmdb/v3/proxy/* - */ - tmdbProxyGet: (params: RequestParams = {}) => - this.request({ - path: `/api/tmdb/v3/proxy/*`, - method: 'GET', - ...params - }), - - /** - * No description - * - * @name TmdbProxyPost - * @request POST:/api/tmdb/v3/proxy/* - */ - tmdbProxyPost: (params: RequestParams = {}) => - this.request({ - path: `/api/tmdb/v3/proxy/*`, - method: 'POST', - ...params - }), - - /** - * No description - * - * @name TmdbProxyPut - * @request PUT:/api/tmdb/v3/proxy/* - */ - tmdbProxyPut: (params: RequestParams = {}) => - this.request({ - path: `/api/tmdb/v3/proxy/*`, - method: 'PUT', - ...params - }), - - /** - * No description - * - * @name TmdbProxyDelete - * @request DELETE:/api/tmdb/v3/proxy/* - */ - tmdbProxyDelete: (params: RequestParams = {}) => - this.request({ - path: `/api/tmdb/v3/proxy/*`, - method: 'DELETE', - ...params - }), - - /** - * No description - * - * @name TmdbProxyPatch - * @request PATCH:/api/tmdb/v3/proxy/* - */ - tmdbProxyPatch: (params: RequestParams = {}) => - this.request({ - path: `/api/tmdb/v3/proxy/*`, - method: 'PATCH', - ...params - }), - - /** - * No description - * - * @name TmdbProxyOptions - * @request OPTIONS:/api/tmdb/v3/proxy/* - */ - tmdbProxyOptions: (params: RequestParams = {}) => - this.request({ - path: `/api/tmdb/v3/proxy/*`, - method: 'OPTIONS', - ...params - }), - - /** - * No description - * - * @name TmdbProxyHead - * @request HEAD:/api/tmdb/v3/proxy/* - */ - tmdbProxyHead: (params: RequestParams = {}) => - this.request({ - path: `/api/tmdb/v3/proxy/*`, - method: 'HEAD', - ...params - }), - - /** - * No description - * - * @name TmdbProxySearch - * @request SEARCH:/api/tmdb/v3/proxy/* - */ - tmdbProxySearch: (params: RequestParams = {}) => - this.request({ - path: `/api/tmdb/v3/proxy/*`, - method: 'SEARCH', - ...params - }) - }; - providers = { - /** - * No description - * - * @tags providers - * @name GetSourceProviders - * @request GET:/api/providers - */ - getSourceProviders: (params: RequestParams = {}) => - this.request({ - path: `/api/providers`, - method: 'GET', - format: 'json', - ...params - }), - - /** - * No description - * - * @tags providers - * @name GetSourceSettingsTemplate - * @request GET:/api/providers/{providerId}/settings/template - */ - getSourceSettingsTemplate: (providerId: string, params: RequestParams = {}) => - this.request({ - path: `/api/providers/${providerId}/settings/template`, - method: 'GET', - format: 'json', - ...params - }), - - /** - * No description - * - * @tags providers - * @name ValidateSourceSettings - * @request POST:/api/providers/{providerId}/settings/validate - */ - validateSourceSettings: ( - providerId: string, - data: PluginSettingsDto, - params: RequestParams = {} - ) => - this.request({ - path: `/api/providers/${providerId}/settings/validate`, - method: 'POST', - body: data, - type: ContentType.Json, - format: 'json', - ...params - }), - - /** - * No description - * - * @tags providers - * @name GetSourceCapabilities - * @request GET:/api/providers/{providerId}/capabilities - */ - getSourceCapabilities: (providerId: string, params: RequestParams = {}) => - this.request({ - path: `/api/providers/${providerId}/capabilities`, - method: 'GET', - format: 'json', - ...params - }) - }; } diff --git a/src/lib/components/Button.svelte b/src/lib/components/Button.svelte index 0da7a2c..5741ac7 100644 --- a/src/lib/components/Button.svelte +++ b/src/lib/components/Button.svelte @@ -6,19 +6,33 @@ import { type ComponentType, createEventDispatcher } from 'svelte'; import type { Selectable } from '../selectable'; import { DotsVertical } from 'radix-icons-svelte'; + import type { ContainerProps } from './Container.type'; + + type $$Props = { + disabled?: boolean; + focusOnMount?: boolean; + focusedChild?: boolean; + type?: 'primary' | 'secondary' | 'primary-dark'; + confirmDanger?: boolean; + action?: (() => Promise) | null; + secondaryAction?: (() => Promise | any) | null; + icon?: ComponentType; + iconAfter?: ComponentType; + iconAbsolute?: ComponentType; + } & ContainerProps; const dispatch = createEventDispatcher<{ clickOrSelect: null }>(); - export let disabled: boolean = false; - export let focusOnMount: boolean = false; - export let focusedChild = false; - export let type: 'primary' | 'secondary' | 'primary-dark' = 'primary'; - export let confirmDanger = false; - export let action: (() => Promise) | null = null; - export let secondaryAction: (() => Promise | any) | null = null; - export let icon: ComponentType | undefined = undefined; - export let iconAfter: ComponentType | undefined = undefined; - export let iconAbsolute: ComponentType | undefined = undefined; + export let disabled: Required<$$Props>['disabled'] = false; + export let focusOnMount: Required<$$Props>['focusOnMount'] = false; + export let focusedChild: Required<$$Props>['focusedChild'] = false; + export let type: Required<$$Props>['type'] = 'primary'; + export let confirmDanger: Required<$$Props>['confirmDanger'] = false; + export let action: Required<$$Props>['action'] = null; + export let secondaryAction: Required<$$Props>['secondaryAction'] = null; + export let icon: $$Props['icon'] = undefined; + export let iconAfter: $$Props['iconAfter'] = undefined; + export let iconAbsolute: $$Props['iconAbsolute'] = undefined; let actionIsFetching = false; $: _disabled = disabled || actionIsFetching; diff --git a/src/lib/components/SelectField.svelte b/src/lib/components/SelectField.svelte index 898b852..4fbd2d4 100644 --- a/src/lib/components/SelectField.svelte +++ b/src/lib/components/SelectField.svelte @@ -7,7 +7,7 @@ const dispatch = createEventDispatcher<{ clickOrSelect: null }>(); export let color: 'secondary' | 'primary' = 'secondary'; - export let value: string; + export let value: string = ''; export let disabled: boolean = false; export let action: (() => Promise) | undefined = undefined; diff --git a/src/lib/components/TextField.svelte b/src/lib/components/TextField.svelte index 6cdd89a..8099a16 100644 --- a/src/lib/components/TextField.svelte +++ b/src/lib/components/TextField.svelte @@ -61,6 +61,7 @@ selected: hasFocus, unselected: !hasFocus })} + on:blur {type} {value} on:input={handleChange} diff --git a/src/lib/pages/ManagePage/EditMediaSourceDialog.ManagePage.svelte b/src/lib/pages/ManagePage/EditMediaSourceDialog.ManagePage.svelte index c443fed..ae05fc4 100644 --- a/src/lib/pages/ManagePage/EditMediaSourceDialog.ManagePage.svelte +++ b/src/lib/pages/ManagePage/EditMediaSourceDialog.ManagePage.svelte @@ -9,6 +9,8 @@ import TextField from '../../components/TextField.svelte'; import Toggle from '../../components/Toggle.svelte'; import { reiverrApiNew, user } from '../../stores/user.store'; + import { capitalize } from '$lib/utils'; + import { mediaSourcesDataStore } from '$lib/stores/data.store'; export let modalId: symbol; @@ -37,34 +39,42 @@ let validationResponse: ValidationResponseDto | undefined; - async function handleSave() { - // validationResponse = undefined; + async function validateForm() { const source = await mediaSource; + validationResponse = await reiverrApiNew.providers .validateSourceSettings(source.pluginId, { settings: $settings }) .then((r) => r.data); - if (validationResponse?.isValid) { - const replacedSettings = { - ...get(settings) - }; - Object.keys(validationResponse?.replace).forEach((key) => { - replacedSettings[key] = validationResponse?.replace[key]; + return validationResponse; + } + + async function handleSave() { + const source = await mediaSource; + const res = await validateForm(); + + const replacedSettings = { + ...get(settings) + }; + if (res?.replace) { + Object.keys(res?.replace).forEach((key) => { + replacedSettings[key] = res?.replace[key]; }); - await reiverrApiNew.users.updateSource(get(user)?.id || '', { - id: sourceId, - name: $name, - pluginId: source.pluginId, - pluginSettings: replacedSettings, - enabled: true - }); - modalStack.close(modalId); } + await reiverrApiNew.users.updateSource(get(user)?.id || '', { + id: sourceId, + name: $name, + pluginId: source.pluginId, + pluginSettings: replacedSettings + }); + await mediaSourcesDataStore.refresh(); + modalStack.close(modalId); } async function handleRemovePlugin() { await mediaSource.then((s) => reiverrApiNew.users.deleteSource(s.id, get(user)?.id || '')); await user.refreshUser(); + await mediaSourcesDataStore.refresh(); modalStack.close(modalId); } @@ -74,7 +84,7 @@ Loading... {:then [mediaSource, pluginSettingsTemplate]}

- {mediaSource.name} + {capitalize(mediaSource.pluginId)}

Edit media source settings

@@ -83,6 +93,7 @@ {#if pluginSettingsTemplate?.settings} {#each Object.keys(pluginSettingsTemplate.settings) as key} + {@const stale = mediaSource.pluginSettings?.[key] !== $settings[key]} {@const template = pluginSettingsTemplate.settings[key]} {#if template.type === 'string' || template.type === 'number' || template.type === 'password'} settings.update((s) => ({ ...s, [key]: detail }))} value={$settings[key] || ''} placeholder={template.placeholder} + on:blur={() => stale && validateForm()} > {template.label} @@ -106,10 +118,12 @@ - + {/await} diff --git a/src/lib/pages/ManagePage/MediaSourceButton.ManagePage.svelte b/src/lib/pages/ManagePage/MediaSourceButton.ManagePage.svelte index 470ec22..0c6a8d6 100644 --- a/src/lib/pages/ManagePage/MediaSourceButton.ManagePage.svelte +++ b/src/lib/pages/ManagePage/MediaSourceButton.ManagePage.svelte @@ -1,6 +1,7 @@ - handleEditPluginSettings()} -/> + handleEditPluginSettings()}> +
+
+ + {capitalize(mediaSource.name)} + +
+ diff --git a/src/lib/pages/ManagePage/MediaSources.ManagePage.svelte b/src/lib/pages/ManagePage/MediaSources.ManagePage.svelte index 27ef027..8d13380 100644 --- a/src/lib/pages/ManagePage/MediaSources.ManagePage.svelte +++ b/src/lib/pages/ManagePage/MediaSources.ManagePage.svelte @@ -9,15 +9,12 @@ import { get } from 'svelte/store'; import { createErrorNotification } from '../../components/Notifications/notification.store'; import MediaSourceButton from './MediaSourceButton.ManagePage.svelte'; + import { scrollIntoView } from '$lib/selectable'; + import { mediaSourcesDataStore } from '$lib/stores/data.store'; const allPlugins = reiverrApiNew.providers.getSourceProviders().then((r) => r.data); - let userSources = getUserMediaSources(); - function getUserMediaSources() { - return reiverrApiNew.users - .findUserById($user?.id || '') - .then((r) => r.data.mediaSources?.sort((a, b) => a.priority - b.priority) ?? []); - } + const { promise: userSources } = mediaSourcesDataStore.getRequest(); async function addSource(pluginId: string) { const userId = get(user)?.id; @@ -27,12 +24,12 @@ return; } - await reiverrApiNew.users.updateSource(userId, { pluginId, enabled: false }); - userSources = getUserMediaSources(); + await reiverrApiNew.users.updateSource(userId, { pluginId }); + await mediaSourcesDataStore.refresh(); } -
+

Media Soruces

@@ -41,7 +38,7 @@

- {#await userSources then userSources} + {#await $userSources then userSources} {#each userSources as source} {/each} @@ -62,4 +59,4 @@ Add Source {/await} -
+ diff --git a/src/lib/pages/TitlePages/EpisodePage.svelte b/src/lib/pages/TitlePages/EpisodePage.svelte index edc5be2..7346476 100644 --- a/src/lib/pages/TitlePages/EpisodePage.svelte +++ b/src/lib/pages/TitlePages/EpisodePage.svelte @@ -46,14 +46,14 @@ // let isWatched = false; const tmdbEpisode = tmdbApi.getEpisode(Number(id), Number(season), Number(episode)); - let sonarrItem = sonarrApi.getSeriesByTmdbId(Number(id)); - $: sonarrEpisode = getSonarrEpisode(sonarrItem); - let sonarrFiles = getFiles(sonarrItem, sonarrEpisode); + // let sonarrItem = sonarrApi.getSeriesByTmdbId(Number(id)); + // $: sonarrEpisode = getSonarrEpisode(sonarrItem); + // let sonarrFiles = getFiles(sonarrItem, sonarrEpisode); - const jellyfinSeries = jellyfinApi.getLibraryItemFromTmdbId(id); - let jellyfinEpisode = jellyfinSeries.then((series) => - jellyfinApi.getEpisode(series?.Id || '', Number(season), Number(episode)) - ); + // const jellyfinSeries = jellyfinApi.getLibraryItemFromTmdbId(id); + // let jellyfinEpisode = jellyfinSeries.then((series) => + // jellyfinApi.getEpisode(series?.Id || '', Number(season), Number(episode)) + // ); let titleProperties: { href?: string; label: string }[] = []; $: { @@ -111,69 +111,69 @@ // return out; // } - async function getSonarrEpisode(sonarrItem: Promise) { - return sonarrItem.then((sonarrItem) => { - if (!sonarrItem?.id) return; + // async function getSonarrEpisode(sonarrItem: Promise) { + // return sonarrItem.then((sonarrItem) => { + // if (!sonarrItem?.id) return; - return sonarrApi - .getEpisodes(sonarrItem.id, Number(season)) - .then((episodes) => episodes.find((e) => e.episodeNumber === Number(episode))); - }); - } + // return sonarrApi + // .getEpisodes(sonarrItem.id, Number(season)) + // .then((episodes) => episodes.find((e) => e.episodeNumber === Number(episode))); + // }); + // } - async function handleAddedToSonarr() { - sonarrItem = sonarrApi.getSeriesByTmdbId(Number(id)); - return retry(() => getSonarrEpisode(sonarrItem)).then((sonarrEpisode) => { - sonarrEpisode && - createModal(SonarrMediaManagerModal, { - sonarrItem: sonarrEpisode, - onGrabRelease: () => {} - }); - }); - } + // async function handleAddedToSonarr() { + // sonarrItem = sonarrApi.getSeriesByTmdbId(Number(id)); + // return retry(() => getSonarrEpisode(sonarrItem)).then((sonarrEpisode) => { + // sonarrEpisode && + // createModal(SonarrMediaManagerModal, { + // sonarrItem: sonarrEpisode, + // onGrabRelease: () => {} + // }); + // }); + // } - async function handleRequestEpisode() { - return Promise.all([sonarrEpisode, tmdbEpisode]).then(([sonarrEpisode, tmdbEpisode]) => { - if (sonarrEpisode) { - createModal(SonarrMediaManagerModal, { - sonarrItem: sonarrEpisode, - onGrabRelease: () => {} // TODO - }); - } else if (tmdbEpisode) { - createModal(MMAddToSonarrDialog, { - tmdbId: Number(id), - backdropUri: tmdbEpisode.still_path || '', - title: tmdbEpisode.name || '', - onComplete: handleAddedToSonarr - }); - } else { - console.error('No series found'); - } - }); - } + // async function handleRequestEpisode() { + // return Promise.all([sonarrEpisode, tmdbEpisode]).then(([sonarrEpisode, tmdbEpisode]) => { + // if (sonarrEpisode) { + // createModal(SonarrMediaManagerModal, { + // sonarrItem: sonarrEpisode, + // onGrabRelease: () => {} // TODO + // }); + // } else if (tmdbEpisode) { + // createModal(MMAddToSonarrDialog, { + // tmdbId: Number(id), + // backdropUri: tmdbEpisode.still_path || '', + // title: tmdbEpisode.name || '', + // onComplete: handleAddedToSonarr + // }); + // } else { + // console.error('No series found'); + // } + // }); + // } - function createConfirmDeleteFiles(files: EpisodeFileResource[]) { - createModal(ConfirmDialog, { - header: 'Delete Season Files?', - body: `Are you sure you want to delete all ${files.length} file(s)?`, - confirm: () => - sonarrApi - .deleteSonarrEpisodes(files.map((f) => f.id || -1)) - .then(() => (sonarrFiles = getFiles(sonarrItem, sonarrEpisode))) - }); - } + // function createConfirmDeleteFiles(files: EpisodeFileResource[]) { + // createModal(ConfirmDialog, { + // header: 'Delete Season Files?', + // body: `Are you sure you want to delete all ${files.length} file(s)?`, + // confirm: () => + // sonarrApi + // .deleteSonarrEpisodes(files.map((f) => f.id || -1)) + // .then(() => (sonarrFiles = getFiles(sonarrItem, sonarrEpisode))) + // }); + // } - function getFiles( - sonarrItem: Promise, - sonarrEpisode: Promise - ) { - return Promise.all([sonarrItem, sonarrEpisode]).then(([sonarrItem, sonarrEpisode]) => { - if (!sonarrItem?.id) return []; - return sonarrApi - .getFilesBySeriesId(sonarrItem.id) - .then((files) => files.filter((f) => sonarrEpisode?.episodeFileId === f.id)); - }); - } + // function getFiles( + // sonarrItem: Promise, + // sonarrEpisode: Promise + // ) { + // return Promise.all([sonarrItem, sonarrEpisode]).then(([sonarrItem, sonarrEpisode]) => { + // if (!sonarrItem?.id) return []; + // return sonarrApi + // .getFilesBySeriesId(sonarrItem.id) + // .then((files) => files.filter((f) => sonarrEpisode?.episodeFileId === f.id)); + // }); + // } // async function handlePlay() { // const awaitedStreams = await Promise.all( diff --git a/src/lib/pages/TitlePages/MoviePage/MoviePage.svelte b/src/lib/pages/TitlePages/MoviePage/MoviePage.svelte index 349e6f2..64b038c 100644 --- a/src/lib/pages/TitlePages/MoviePage/MoviePage.svelte +++ b/src/lib/pages/TitlePages/MoviePage/MoviePage.svelte @@ -51,10 +51,10 @@ const { promise: tmdbMovie } = tmdbMovieDataStore.getRequest(tmdbId); $: recommendations = tmdbApi.getMovieRecommendations(tmdbId); - const { promise: jellyfinItemP } = useRequest( - (id: string) => jellyfinApi.getLibraryItemFromTmdbId(id), - id - ); + // const { promise: jellyfinItemP } = useRequest( + // (id: string) => jellyfinApi.getLibraryItemFromTmdbId(id), + // id + // ); // function getStreams() { // const out: { source: MediaSource; streams: Promise }[] = []; @@ -76,8 +76,8 @@ // tmdbId // ); - let radarrItem = radarrApi.getMovieByTmdbId(tmdbId); - $: radarrDownloads = getDownloads(radarrItem); + // let radarrItem = radarrApi.getMovieByTmdbId(tmdbId); + // $: radarrDownloads = getDownloads(radarrItem); // $: radarrFiles = getFiles(radarrItem); // const { requests, isFetching, data } = useActionRequests({ @@ -115,34 +115,34 @@ }); } - async function getDownloads(item: typeof radarrItem) { - return item.then((item) => (item ? radarrApi.getDownloadsById(item?.id || -1) : [])); - } + // async function getDownloads(item: typeof radarrItem) { + // return item.then((item) => (item ? radarrApi.getDownloadsById(item?.id || -1) : [])); + // } - function handleAddedToRadarr() { - radarrItem = radarrApi.getMovieByTmdbId(tmdbId); - radarrItem.then( - (radarrItem) => - radarrItem && createModal(MovieMediaManagerModal, { radarrItem, onGrabRelease }) - ); - } + // function handleAddedToRadarr() { + // radarrItem = radarrApi.getMovieByTmdbId(tmdbId); + // radarrItem.then( + // (radarrItem) => + // radarrItem && createModal(MovieMediaManagerModal, { radarrItem, onGrabRelease }) + // ); + // } - const onGrabRelease = () => setTimeout(() => (radarrDownloads = getDownloads(radarrItem)), 8000); + // const onGrabRelease = () => setTimeout(() => (radarrDownloads = getDownloads(radarrItem)), 8000); - async function handleRequest() { - return radarrItem.then((radarrItem) => { - if (radarrItem) createModal(MovieMediaManagerModal, { radarrItem, onGrabRelease }); - else - return $tmdbMovie.then((tmdbMovie) => { - createModal(MMAddToRadarrDialog, { - title: tmdbMovie?.title || '', - tmdbId, - backdropUri: tmdbMovie?.backdrop_path || '', - onComplete: handleAddedToRadarr - }); - }); - }); - } + // async function handleRequest() { + // return radarrItem.then((radarrItem) => { + // if (radarrItem) createModal(MovieMediaManagerModal, { radarrItem, onGrabRelease }); + // else + // return $tmdbMovie.then((tmdbMovie) => { + // createModal(MMAddToRadarrDialog, { + // title: tmdbMovie?.title || '', + // tmdbId, + // backdropUri: tmdbMovie?.backdrop_path || '', + // onComplete: handleAddedToRadarr + // }); + // }); + // }); + // } // function createConfirmDeleteSeasonDialog(files: MovieFileResource[]) { // createModal(ConfirmDialog, { diff --git a/src/lib/pages/TitlePages/SeriesPage/EpisodeGrid.svelte b/src/lib/pages/TitlePages/SeriesPage/EpisodeGrid.svelte index 709f878..2bab3bc 100644 --- a/src/lib/pages/TitlePages/SeriesPage/EpisodeGrid.svelte +++ b/src/lib/pages/TitlePages/SeriesPage/EpisodeGrid.svelte @@ -1,7 +1,8 @@