feat!: Improved plugin settings api and medias source settings error messages for jackett and jellyfin

This commit is contained in:
Aleksi Lassila
2025-02-21 14:20:31 +02:00
parent 94696df7dc
commit b598245cb0
20 changed files with 284 additions and 132 deletions

View File

@@ -1,10 +1,19 @@
import { OmitType, PartialType } from '@nestjs/swagger';
import { ApiProperty, OmitType, PartialType } from '@nestjs/swagger';
import { PickAndPartial } from 'src/common/common.dto';
import { MediaSource } from './media-source.entity';
import { ValidationResponseDto } from 'src/source-providers/source-provider.dto';
export class MediaSourceDto extends PickAndPartial(
MediaSource,
['id', 'pluginId', 'name', 'userId', 'adminControlled', 'enabled'],
[
'id',
'pluginId',
'name',
'userId',
'adminControlled',
'enabled',
'priority',
],
['pluginSettings'],
) {}
@@ -23,3 +32,11 @@ export class CreateMediaSourceDto extends OmitType(MediaSourceDto, [
'id',
'userId',
]) {}
export class UpdateMediaSourceResponse {
@ApiProperty({ type: MediaSourceDto })
mediaSource: MediaSourceDto;
@ApiProperty({ type: ValidationResponseDto, required: false })
validationResponse: ValidationResponseDto | undefined;
}

View File

@@ -69,9 +69,8 @@ export class ServiceOwnershipValidator implements CanActivate {
if (!sourceId) return true;
const mediaSource = await this.mediaSourcesService.findMediaSource(
sourceId,
);
const mediaSource =
await this.mediaSourcesService.findMediaSource(sourceId);
if (!mediaSource) throw new NotFoundException('Source not found');
@@ -303,9 +302,8 @@ export class MediaSourcesController {
@GetAuthToken() token: string,
) {
const sourceId = params.sourceId;
const mediaSource = await this.mediaSourcesService.findMediaSource(
sourceId,
);
const mediaSource =
await this.mediaSourcesService.findMediaSource(sourceId);
if (!mediaSource) throw new NotFoundException('Source not found');
@@ -370,9 +368,8 @@ export class MediaSourcesController {
}
async getConnection(sourceId: string) {
const mediaSource = await this.mediaSourcesService.findMediaSource(
sourceId,
);
const mediaSource =
await this.mediaSourcesService.findMediaSource(sourceId);
if (!mediaSource.pluginId || !mediaSource.enabled) {
throw new BadRequestException('Source not configured');

View File

@@ -6,6 +6,7 @@ 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';
import { ValidationResponse } from '@aleksilassila/reiverr-plugin';
export enum MediaSourcesServiceError {
SourceNotFound = 'SourceNotFound',
@@ -58,7 +59,7 @@ export class MediaSourcesService {
user: User,
sourceDto: UpdateOrCreateMediaSourceDto,
callerUser: User = user,
): Promise<User> {
) {
if (!callerUser.isAdmin || callerUser.id !== user.id) {
throw MediaSourcesServiceError.Unauthorized;
}
@@ -88,18 +89,21 @@ export class MediaSourcesService {
source.adminControlled =
sourceDto.adminControlled ?? source.adminControlled;
let validationResponse: ValidationResponse | undefined;
if (sourceDto.pluginSettings !== undefined) {
let valid = false;
const provider = this.sourceProvidersService.getProvider(source.pluginId);
if (provider) {
const validationRes = await provider.settingsManager.validateSettings(
validationResponse = await provider.settingsManager.validateSettings(
sourceDto.pluginSettings,
);
valid = validationRes.isValid;
valid = validationResponse.isValid;
source.pluginSettings = validationResponse.settings;
} else {
source.pluginSettings = sourceDto.pluginSettings;
}
source.pluginSettings = sourceDto.pluginSettings;
source.enabled = !!valid;
}
source.name = sourceDto.name ?? source.name;
@@ -120,8 +124,10 @@ export class MediaSourcesService {
priority++;
}
await this.mediaSourceRepository.save(source);
return this.usersService.findOne(user.id);
return {
mediaSource: await this.mediaSourceRepository.save(source),
validationResponse,
};
}
getMediaSourceSettings(user: User, sourceId: string) {

View File

@@ -14,11 +14,15 @@ import { GetAuthUser, UserAccessControl } from 'src/auth/auth.guard';
import { UserDto } from 'src/users/user.dto';
import { User } from 'src/users/user.entity';
import { UsersService } from 'src/users/users.service';
import { UpdateOrCreateMediaSourceDto } from './media-source.dto';
import {
UpdateMediaSourceResponse,
UpdateOrCreateMediaSourceDto,
} from './media-source.dto';
import {
MediaSourcesService,
MediaSourcesServiceError,
} from './media-sources.service';
import { MediaSource } from './media-source.entity';
@ApiTags('users')
@Controller('users/:userId/sources')
@@ -30,33 +34,40 @@ export class MediaSourcesSettingsController {
) {}
@Put()
@ApiOkResponse({ description: 'Source updated', type: UserDto })
@ApiOkResponse({
description: 'Source updated',
type: UpdateMediaSourceResponse,
})
async updateSource(
@GetAuthUser() callerUser: User,
@Param('userId') userId: string,
@Body() sourceDto: UpdateOrCreateMediaSourceDto,
): Promise<UserDto> {
): Promise<UpdateMediaSourceResponse> {
const user = await this.usersService.findOne(userId);
if (!user) {
throw new NotFoundException('User not found');
}
const updatedUser = await this.mediaSourcesService
.updateOrCreateMediaSource(user, sourceDto, callerUser)
.catch((e) => {
if (e === MediaSourcesServiceError.Unauthorized) {
throw new UnauthorizedException();
} else {
throw new InternalServerErrorException('Failed to update source');
}
});
const { mediaSource: updatedSource, validationResponse } =
await this.mediaSourcesService
.updateOrCreateMediaSource(user, sourceDto, callerUser)
.catch((e) => {
if (e === MediaSourcesServiceError.Unauthorized) {
throw new UnauthorizedException();
} else {
throw new InternalServerErrorException('Failed to update source');
}
});
if (!updatedUser) {
if (!updatedSource) {
throw new InternalServerErrorException('Failed to update source');
}
return UserDto.fromEntity(updatedUser);
return {
mediaSource: updatedSource,
validationResponse,
};
}
@Delete(':sourceId')

View File

@@ -123,7 +123,7 @@ export class ValidationResponseDto implements ValidationResponse {
type: 'object',
additionalProperties: true,
})
replace: Record<string, any>;
settings: Record<string, any>;
}
export class AudioStreamDto implements AudioStream {