mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-27 03:05:10 +02:00
refactor: pretty much the whole backend module hierarchy
This commit is contained in:
485
backend/src/source-providers/device-profile.dto.ts
Normal file
485
backend/src/source-providers/device-profile.dto.ts
Normal file
@@ -0,0 +1,485 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
CodecProfile,
|
||||
ContainerProfile,
|
||||
DeviceProfile,
|
||||
DirectPlayProfile,
|
||||
ProfileCondition,
|
||||
SubtitleProfile,
|
||||
TranscodingProfile,
|
||||
} from '@aleksilassila/reiverr-plugin';
|
||||
|
||||
export class DirectPlayProfileDto implements DirectPlayProfile {
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
description: 'Gets or sets the container.',
|
||||
nullable: true,
|
||||
})
|
||||
Container?: string;
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
description: 'Gets or sets the audio codec.',
|
||||
nullable: true,
|
||||
})
|
||||
AudioCodec?: string | null;
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
description: 'Gets or sets the video codec.',
|
||||
nullable: true,
|
||||
})
|
||||
VideoCodec?: string | null;
|
||||
|
||||
@ApiProperty({
|
||||
required: false,
|
||||
description: 'Gets or sets the Dlna profile type.',
|
||||
enum: ['Audio', 'Video', 'Photo', 'Subtitle', 'Lyric'],
|
||||
})
|
||||
Type?: 'Audio' | 'Video' | 'Photo' | 'Subtitle' | 'Lyric';
|
||||
}
|
||||
|
||||
export class ProfileConditionDto implements ProfileCondition {
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the condition.',
|
||||
enum: [
|
||||
'Equals',
|
||||
'NotEquals',
|
||||
'LessThanEqual',
|
||||
'GreaterThanEqual',
|
||||
'EqualsAny',
|
||||
],
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
Condition?:
|
||||
| 'Equals'
|
||||
| 'NotEquals'
|
||||
| 'LessThanEqual'
|
||||
| 'GreaterThanEqual'
|
||||
| 'EqualsAny';
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the property.',
|
||||
enum: [
|
||||
'AudioChannels',
|
||||
'AudioBitrate',
|
||||
'AudioProfile',
|
||||
'Width',
|
||||
'Height',
|
||||
'Has64BitOffsets',
|
||||
'PacketLength',
|
||||
'VideoBitDepth',
|
||||
'VideoBitrate',
|
||||
'VideoFramerate',
|
||||
'VideoLevel',
|
||||
'VideoProfile',
|
||||
'VideoTimestamp',
|
||||
'IsAnamorphic',
|
||||
'RefFrames',
|
||||
'NumAudioStreams',
|
||||
'NumVideoStreams',
|
||||
'IsSecondaryAudio',
|
||||
'VideoCodecTag',
|
||||
'IsAvc',
|
||||
'IsInterlaced',
|
||||
'AudioSampleRate',
|
||||
'AudioBitDepth',
|
||||
'VideoRangeType',
|
||||
],
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
Property?:
|
||||
| 'AudioChannels'
|
||||
| 'AudioBitrate'
|
||||
| 'AudioProfile'
|
||||
| 'Width'
|
||||
| 'Height'
|
||||
| 'Has64BitOffsets'
|
||||
| 'PacketLength'
|
||||
| 'VideoBitDepth'
|
||||
| 'VideoBitrate'
|
||||
| 'VideoFramerate'
|
||||
| 'VideoLevel'
|
||||
| 'VideoProfile'
|
||||
| 'VideoTimestamp'
|
||||
| 'IsAnamorphic'
|
||||
| 'RefFrames'
|
||||
| 'NumAudioStreams'
|
||||
| 'NumVideoStreams'
|
||||
| 'IsSecondaryAudio'
|
||||
| 'VideoCodecTag'
|
||||
| 'IsAvc'
|
||||
| 'IsInterlaced'
|
||||
| 'AudioSampleRate'
|
||||
| 'AudioBitDepth'
|
||||
| 'VideoRangeType'
|
||||
| null;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the value.',
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
Value?: string | null;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Indicates if the condition is required.',
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
IsRequired?: boolean;
|
||||
}
|
||||
|
||||
export class TranscodingProfileDto implements TranscodingProfile {
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the container.',
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
Container?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the DLNA profile type.',
|
||||
enum: ['Audio', 'Video', 'Photo', 'Subtitle', 'Lyric'],
|
||||
required: false,
|
||||
})
|
||||
Type?: 'Audio' | 'Video' | 'Photo' | 'Subtitle' | 'Lyric';
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the video codec.',
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
VideoCodec?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the audio codec.',
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
AudioCodec?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Media streaming protocol.',
|
||||
enum: ['http', 'hls'],
|
||||
required: false,
|
||||
})
|
||||
Protocol?: 'http' | 'hls';
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Indicates if the content length should be estimated.',
|
||||
default: false,
|
||||
required: false,
|
||||
})
|
||||
EstimateContentLength?: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Indicates if M2TS mode is enabled.',
|
||||
default: false,
|
||||
required: false,
|
||||
})
|
||||
EnableMpegtsM2TsMode?: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the transcoding seek info mode.',
|
||||
default: 'Auto',
|
||||
enum: ['Auto', 'Bytes'],
|
||||
required: false,
|
||||
})
|
||||
TranscodeSeekInfo?: 'Auto' | 'Bytes';
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Indicates if timestamps should be copied.',
|
||||
default: false,
|
||||
required: false,
|
||||
})
|
||||
CopyTimestamps?: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the encoding context.',
|
||||
default: 'Streaming',
|
||||
enum: ['Streaming', 'Static'],
|
||||
required: false,
|
||||
})
|
||||
Context?: 'Streaming' | 'Static';
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Indicates if subtitles are allowed in the manifest.',
|
||||
default: false,
|
||||
required: false,
|
||||
})
|
||||
EnableSubtitlesInManifest?: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the maximum audio channels.',
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
MaxAudioChannels?: string | null;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the minimum amount of segments.',
|
||||
format: 'int32',
|
||||
default: 0,
|
||||
required: false,
|
||||
})
|
||||
MinSegments?: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the segment length.',
|
||||
format: 'int32',
|
||||
default: 0,
|
||||
required: false,
|
||||
})
|
||||
SegmentLength?: number;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Indicates if breaking the video stream on non-keyframes is supported.',
|
||||
default: false,
|
||||
required: false,
|
||||
})
|
||||
BreakOnNonKeyFrames?: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the profile conditions.',
|
||||
type: [ProfileConditionDto],
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
Conditions?: ProfileConditionDto[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Indicates if variable bitrate encoding is supported.',
|
||||
default: true,
|
||||
required: false,
|
||||
})
|
||||
EnableAudioVbrEncoding?: boolean;
|
||||
}
|
||||
|
||||
export class ContainerProfileDto implements ContainerProfile {
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Gets or sets the MediaBrowser.Model.Dlna.DlnaProfileType which this container must meet.',
|
||||
enum: ['Audio', 'Video', 'Photo', 'Subtitle', 'Lyric'],
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
Type?: 'Audio' | 'Video' | 'Photo' | 'Subtitle' | 'Lyric';
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the profile conditions.',
|
||||
type: [ProfileConditionDto],
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
Conditions?: ProfileConditionDto[];
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Gets or sets the container(s) which this container must meet.',
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
Container?: string | null;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Gets or sets the sub container(s) which this container must meet.',
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
SubContainer?: string | null;
|
||||
}
|
||||
|
||||
export class CodecProfileDto implements CodecProfile {
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Gets or sets the MediaBrowser.Model.Dlna.CodecType which this container must meet.',
|
||||
enum: ['Video', 'VideoAudio', 'Audio'],
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
Type?: 'Video' | 'VideoAudio' | 'Audio';
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the profile conditions.',
|
||||
type: [ProfileConditionDto],
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
Conditions?: ProfileConditionDto[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the apply conditions if this profile is met.',
|
||||
type: [ProfileConditionDto],
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
ApplyConditions?: ProfileConditionDto[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the codec(s) that this profile applies to.',
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
Codec?: string | null;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Gets or sets the container(s) which this profile will be applied to.',
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
Container?: string | null;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Gets or sets the sub-container(s) which this profile will be applied to.',
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
SubContainer?: string | null;
|
||||
}
|
||||
|
||||
export class SubtitleProfileDto implements SubtitleProfile {
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the format.',
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
Format?: string | null;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the delivery method.',
|
||||
enum: ['Encode', 'Embed', 'External', 'Hls', 'Drop'],
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
Method?: 'Encode' | 'Embed' | 'External' | 'Hls' | 'Drop';
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the DIDL mode.',
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
DidlMode?: string | null;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the language.',
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
Language?: string | null;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the container.',
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
Container?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A MediaBrowser.Model.Dlna.DeviceProfile represents a set of metadata which determines which content a certain device is able to play.
|
||||
* <br />
|
||||
* Specifically, it defines the supported <see cref="P:MediaBrowser.Model.Dlna.DeviceProfile.ContainerProfiles">containers</see> and
|
||||
* <see cref="P:MediaBrowser.Model.Dlna.DeviceProfile.CodecProfiles">codecs</see> (video and/or audio, including codec profiles and levels)
|
||||
* the device is able to direct play (without transcoding or remuxing),
|
||||
* as well as which <see cref="P:MediaBrowser.Model.Dlna.DeviceProfile.TranscodingProfiles">containers/codecs to transcode to</see> in case it isn't.
|
||||
*/
|
||||
export class DeviceProfileDto implements DeviceProfile {
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Gets or sets the name of this device profile. User profiles must have a unique name.',
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
Name?: string | null;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the unique internal identifier.',
|
||||
format: 'uuid',
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
Id?: string | null;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Gets or sets the maximum allowed bitrate for all streamed content.',
|
||||
format: 'int32',
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
MaxStreamingBitrate?: number | null;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Gets or sets the maximum allowed bitrate for statically streamed content (= direct played files).',
|
||||
format: 'int32',
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
MaxStaticBitrate?: number | null;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Gets or sets the maximum allowed bitrate for transcoded music streams.',
|
||||
format: 'int32',
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
MusicStreamingTranscodingBitrate?: number | null;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
'Gets or sets the maximum allowed bitrate for statically streamed (= direct played) music files.',
|
||||
format: 'int32',
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
MaxStaticMusicBitrate?: number | null;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the direct play profiles.',
|
||||
type: [DirectPlayProfileDto],
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
DirectPlayProfiles?: DirectPlayProfileDto[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the transcoding profiles.',
|
||||
type: [TranscodingProfileDto],
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
TranscodingProfiles?: TranscodingProfileDto[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the container profiles.',
|
||||
type: [ContainerProfileDto],
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
ContainerProfiles?: ContainerProfileDto[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the codec profiles.',
|
||||
type: [CodecProfileDto],
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
CodecProfiles?: CodecProfileDto[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Gets or sets the subtitle profiles.',
|
||||
type: [SubtitleProfileDto],
|
||||
nullable: true,
|
||||
required: false,
|
||||
})
|
||||
SubtitleProfiles?: SubtitleProfileDto[];
|
||||
}
|
||||
253
backend/src/source-providers/source-provider.dto.ts
Normal file
253
backend/src/source-providers/source-provider.dto.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import {
|
||||
ApiProperty,
|
||||
ApiPropertyOptional,
|
||||
getSchemaPath,
|
||||
} from '@nestjs/swagger';
|
||||
import { DeviceProfileDto } from './device-profile.dto';
|
||||
import {
|
||||
AudioStream,
|
||||
IndexItem,
|
||||
PlaybackConfig,
|
||||
SourceProviderSettings,
|
||||
SourceProviderSettingsInput,
|
||||
SourceProviderSettingsLink,
|
||||
SourceProviderSettingsTemplate,
|
||||
Quality,
|
||||
Subtitles,
|
||||
ValidationResponse,
|
||||
StreamCandidate,
|
||||
StreamProperty,
|
||||
Stream,
|
||||
} from '@aleksilassila/reiverr-plugin';
|
||||
|
||||
export class IndexItemDto implements IndexItem {
|
||||
@ApiProperty()
|
||||
id: string;
|
||||
}
|
||||
|
||||
class PluginSettingsLinkDto implements SourceProviderSettingsLink {
|
||||
@ApiProperty({ example: 'link', enum: ['link'] })
|
||||
type: 'link';
|
||||
|
||||
@ApiProperty({ example: 'https://example.com' })
|
||||
url: string;
|
||||
|
||||
@ApiProperty({ example: 'Example' })
|
||||
label: string;
|
||||
}
|
||||
|
||||
class PluginSettingsInputDto implements SourceProviderSettingsInput {
|
||||
@ApiProperty({
|
||||
example: 'string',
|
||||
enum: ['string', 'number', 'boolean', 'password'],
|
||||
})
|
||||
type: 'string' | 'number' | 'boolean' | 'password';
|
||||
|
||||
@ApiProperty({ example: 'Example' })
|
||||
label: string;
|
||||
|
||||
@ApiProperty({ example: 'Placeholder' })
|
||||
placeholder: string;
|
||||
}
|
||||
|
||||
export class PluginSettingsTemplateDto {
|
||||
@ApiProperty({
|
||||
example: {
|
||||
setting1: 'string',
|
||||
setting2: { type: 'link', url: 'https://example.com' },
|
||||
},
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
oneOf: [
|
||||
{ $ref: getSchemaPath(PluginSettingsInputDto) },
|
||||
{ $ref: getSchemaPath(PluginSettingsLinkDto) },
|
||||
],
|
||||
},
|
||||
})
|
||||
settings: SourceProviderSettingsTemplate;
|
||||
}
|
||||
|
||||
export class SourceProviderCapabilitiesDto {
|
||||
@ApiProperty()
|
||||
moviePlayback: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
episodePlayback: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
movieIndexing: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
episodeIndexing: boolean;
|
||||
|
||||
// @ApiProperty()
|
||||
// requesting: boolean;
|
||||
|
||||
// @ApiProperty()
|
||||
// deletion: boolean;
|
||||
}
|
||||
|
||||
export class PluginSettingsDto {
|
||||
@ApiProperty({
|
||||
type: 'object',
|
||||
additionalProperties: true, // Indicates that any properties are allowed
|
||||
example: {
|
||||
setting1: 'some value',
|
||||
setting2: 12345,
|
||||
setting3: true,
|
||||
setting4: { nestedKey: 'nestedValue' },
|
||||
},
|
||||
})
|
||||
settings: SourceProviderSettings;
|
||||
}
|
||||
|
||||
export class ValidationResponseDto implements ValidationResponse {
|
||||
@ApiProperty({ example: true })
|
||||
isValid: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
example: {
|
||||
setting1: 'error message',
|
||||
setting2: 'another error message',
|
||||
},
|
||||
type: 'object',
|
||||
additionalProperties: { type: 'string' },
|
||||
})
|
||||
errors: Record<string, string>;
|
||||
|
||||
@ApiProperty({
|
||||
example: {
|
||||
setting1: 'new value',
|
||||
setting2: 'another new value',
|
||||
},
|
||||
type: 'object',
|
||||
additionalProperties: true,
|
||||
})
|
||||
replace: Record<string, any>;
|
||||
}
|
||||
|
||||
export class AudioStreamDto implements AudioStream {
|
||||
@ApiProperty()
|
||||
index: number;
|
||||
|
||||
@ApiProperty()
|
||||
label: string;
|
||||
|
||||
@ApiProperty({ example: 'aac', required: false })
|
||||
codec: string | undefined;
|
||||
|
||||
@ApiProperty({ example: 96_000, type: 'number', required: false })
|
||||
bitrate: number | undefined;
|
||||
}
|
||||
|
||||
export class QualityDto implements Quality {
|
||||
@ApiProperty()
|
||||
index: number;
|
||||
|
||||
@ApiProperty()
|
||||
bitrate: number;
|
||||
|
||||
@ApiProperty()
|
||||
label: string;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
codec: string | undefined;
|
||||
|
||||
@ApiProperty()
|
||||
original: boolean;
|
||||
}
|
||||
|
||||
export class SubtitlesDto implements Subtitles {
|
||||
@ApiProperty()
|
||||
src: string;
|
||||
@ApiProperty()
|
||||
lang: string;
|
||||
@ApiProperty({
|
||||
type: 'string',
|
||||
enum: ['subtitles', 'captions', 'descriptions'],
|
||||
})
|
||||
kind: 'subtitles' | 'captions' | 'descriptions';
|
||||
@ApiProperty()
|
||||
label: string;
|
||||
}
|
||||
|
||||
export class VideoStreamPropertyDto implements StreamProperty {
|
||||
@ApiProperty()
|
||||
label: string;
|
||||
|
||||
@ApiProperty({
|
||||
oneOf: [{ type: 'string' }, { type: 'number' }],
|
||||
})
|
||||
value: string | number;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
formatted: string | undefined;
|
||||
}
|
||||
|
||||
export class StreamCandidateDto implements StreamCandidate {
|
||||
@ApiProperty()
|
||||
key: string;
|
||||
|
||||
@ApiProperty()
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ type: [VideoStreamPropertyDto] })
|
||||
properties: VideoStreamPropertyDto[];
|
||||
}
|
||||
|
||||
export class StreamDto extends StreamCandidateDto implements Stream {
|
||||
@ApiProperty()
|
||||
src: string;
|
||||
|
||||
@ApiProperty()
|
||||
directPlay: boolean;
|
||||
|
||||
@ApiProperty({ description: 'Duration in seconds' })
|
||||
duration: number;
|
||||
|
||||
@ApiProperty({ description: 'Play progress as a number between 0 and 1' })
|
||||
progress: number;
|
||||
|
||||
@ApiProperty({ type: [AudioStreamDto] })
|
||||
audioStreams: AudioStreamDto[];
|
||||
|
||||
@ApiProperty()
|
||||
audioStreamIndex: number;
|
||||
|
||||
@ApiProperty({ type: [QualityDto] })
|
||||
qualities: QualityDto[];
|
||||
|
||||
@ApiProperty()
|
||||
qualityIndex: number;
|
||||
|
||||
@ApiProperty({ type: [SubtitlesDto] })
|
||||
subtitles: SubtitlesDto[];
|
||||
}
|
||||
|
||||
export class PlaybackConfigDto implements PlaybackConfig {
|
||||
@ApiPropertyOptional({ example: 0, required: false })
|
||||
bitrate: number | undefined;
|
||||
|
||||
@ApiPropertyOptional({ example: 0, required: false })
|
||||
audioStreamIndex: number | undefined;
|
||||
|
||||
@ApiPropertyOptional({ example: 0, required: false })
|
||||
progress: number | undefined;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: 'en',
|
||||
required: false,
|
||||
type: DeviceProfileDto,
|
||||
})
|
||||
deviceProfile: DeviceProfileDto | undefined;
|
||||
|
||||
@ApiPropertyOptional({ example: 'en', required: false })
|
||||
defaultLanguage: string | undefined;
|
||||
}
|
||||
|
||||
export class StreamCandidatesDto {
|
||||
@ApiProperty({
|
||||
type: [StreamCandidateDto],
|
||||
})
|
||||
candidates: StreamCandidateDto[];
|
||||
}
|
||||
129
backend/src/source-providers/source-providers.controller.ts
Normal file
129
backend/src/source-providers/source-providers.controller.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { SourceProvider } from '@aleksilassila/reiverr-plugin';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
Param,
|
||||
PipeTransform,
|
||||
Post,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
GetAuthToken,
|
||||
GetAuthUser,
|
||||
UserAccessControl,
|
||||
} from 'src/auth/auth.guard';
|
||||
import { User } from 'src/users/user.entity';
|
||||
import {
|
||||
PluginSettingsDto,
|
||||
PluginSettingsTemplateDto,
|
||||
SourceProviderCapabilitiesDto,
|
||||
ValidationResponseDto,
|
||||
} from './source-provider.dto';
|
||||
import { SourceProvidersService } from './source-providers.service';
|
||||
|
||||
export const JELLYFIN_DEVICE_ID = 'Reiverr Client';
|
||||
|
||||
@Injectable()
|
||||
export class GetSourceProviderPipe implements PipeTransform {
|
||||
constructor(private readonly sourcesService: SourceProvidersService) {}
|
||||
|
||||
async transform(providerId: string) {
|
||||
const provider = this.sourcesService.getProvider(providerId);
|
||||
|
||||
if (!provider) {
|
||||
throw new NotFoundException('Plugin not found');
|
||||
}
|
||||
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
|
||||
@ApiTags('providers')
|
||||
@Controller('providers')
|
||||
@UseGuards(UserAccessControl)
|
||||
export class SourceProvidersController {
|
||||
constructor(private sourceProvidersService: SourceProvidersService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOkResponse({
|
||||
description: 'All source plugins found',
|
||||
type: String,
|
||||
isArray: true,
|
||||
})
|
||||
async getSourceProviders() {
|
||||
return this.sourceProvidersService
|
||||
.getProviders()
|
||||
.then((plugins) => Object.keys(plugins));
|
||||
}
|
||||
|
||||
@Get(':providerId/settings/template')
|
||||
@ApiOkResponse({
|
||||
description: 'Source settings template',
|
||||
type: PluginSettingsTemplateDto,
|
||||
})
|
||||
async getSourceSettingsTemplate(
|
||||
@Param('providerId') providerId: string,
|
||||
@GetAuthUser() callerUser: User,
|
||||
): Promise<PluginSettingsTemplateDto> {
|
||||
const provider = this.sourceProvidersService.getProvider(providerId);
|
||||
|
||||
if (!provider) {
|
||||
throw new NotFoundException('Plugin not found');
|
||||
}
|
||||
|
||||
// return plugin.getSettingsTemplate(callerUser.pluginSettings?.[sourceId]);
|
||||
return {
|
||||
settings: provider.settingsManager.getSettingsTemplate(),
|
||||
};
|
||||
}
|
||||
|
||||
@Post(':providerId/settings/validate')
|
||||
@ApiOkResponse({
|
||||
description: 'Source settings validation',
|
||||
type: ValidationResponseDto,
|
||||
})
|
||||
async validateSourceSettings(
|
||||
@GetAuthUser() callerUser: User,
|
||||
@Param('providerId') providerId: string,
|
||||
@Body() settings: PluginSettingsDto,
|
||||
): Promise<ValidationResponseDto> {
|
||||
const provider = this.sourceProvidersService.getProvider(providerId);
|
||||
|
||||
if (!provider) {
|
||||
throw new NotFoundException('Plugin not found');
|
||||
}
|
||||
|
||||
return provider.settingsManager.validateSettings(settings.settings);
|
||||
}
|
||||
|
||||
@Get(':providerId/capabilities')
|
||||
@ApiOkResponse({
|
||||
type: SourceProviderCapabilitiesDto,
|
||||
})
|
||||
async getSourceCapabilities(
|
||||
@GetAuthUser() user: User,
|
||||
@Param('providerId', GetSourceProviderPipe) provider: SourceProvider,
|
||||
@GetAuthToken() token: string,
|
||||
): Promise<SourceProviderCapabilitiesDto> {
|
||||
// const settings = this.mediaSourcesService.getMediaSourceSettings(
|
||||
// user,
|
||||
// provider.name,
|
||||
// );
|
||||
|
||||
// if (!settings) {
|
||||
// throw new BadRequestException('Source configuration not found');
|
||||
// }
|
||||
|
||||
return {
|
||||
movieIndexing: !!provider.getMovieCatalogue,
|
||||
episodeIndexing: !!provider.getEpisodeCatalogue,
|
||||
moviePlayback: !!provider.getMovieStreams && !!provider.getMovieStream,
|
||||
episodePlayback:
|
||||
!!provider.getEpisodeStreams && !!provider.getEpisodeStream,
|
||||
};
|
||||
}
|
||||
}
|
||||
12
backend/src/source-providers/source-providers.module.ts
Normal file
12
backend/src/source-providers/source-providers.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { UsersModule } from 'src/users/users.module';
|
||||
import { SourceProvidersController } from './source-providers.controller';
|
||||
import { SourceProvidersService } from './source-providers.service';
|
||||
|
||||
@Module({
|
||||
providers: [SourceProvidersService],
|
||||
controllers: [SourceProvidersController],
|
||||
exports: [SourceProvidersService],
|
||||
imports: [forwardRef(() => UsersModule)],
|
||||
})
|
||||
export class SourceProvidersModule {}
|
||||
60
backend/src/source-providers/source-providers.service.ts
Normal file
60
backend/src/source-providers/source-providers.service.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { PluginProvider, SourceProvider } from '@aleksilassila/reiverr-plugin';
|
||||
|
||||
@Injectable()
|
||||
export class SourceProvidersService {
|
||||
private providers: Record<string, SourceProvider> = {};
|
||||
|
||||
constructor() {
|
||||
console.log('Loading source plugins...');
|
||||
|
||||
this.providers = this.loadPlugins(
|
||||
path.join(require.main.path, '..', '..', 'plugins'),
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Loaded source plugins: ${Object.keys(this.providers).join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
async getProviders(): Promise<Record<string, SourceProvider>> {
|
||||
return this.providers;
|
||||
}
|
||||
|
||||
private loadPlugins(rootDirectory: string): Record<string, SourceProvider> {
|
||||
const pluginDirectories = fs.readdirSync(rootDirectory);
|
||||
|
||||
const pluginPaths = [];
|
||||
for (const directoryName of pluginDirectories) {
|
||||
const directoryPath = path.join(rootDirectory, directoryName);
|
||||
const directoryStat = fs.statSync(directoryPath);
|
||||
|
||||
if (directoryStat.isDirectory() && directoryName.endsWith('.plugin')) {
|
||||
pluginPaths.push(directoryPath);
|
||||
}
|
||||
}
|
||||
|
||||
const plugins: Record<string, SourceProvider> = {};
|
||||
|
||||
for (const pluginPath of pluginPaths) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const pluginModule = require(pluginPath);
|
||||
const provider: typeof PluginProvider = pluginModule.default;
|
||||
provider.getPlugins().forEach((plugin) => {
|
||||
plugins[plugin.name] = plugin;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(`Failed to load plugin from ${pluginPath}: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
getProvider(pluginName: string): SourceProvider | undefined {
|
||||
return this.providers[pluginName];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user