feat: Experimental movie playback through plugins

This commit is contained in:
Aleksi Lassila
2024-12-07 14:30:25 +02:00
parent d3818903b3
commit 96d52299b0
22 changed files with 1960 additions and 112 deletions

View File

@@ -6,7 +6,9 @@ import {
Get,
NotFoundException,
Param,
ParseIntPipe,
Post,
Query,
Req,
Res,
UnauthorizedException,
@@ -14,22 +16,24 @@ import {
} from '@nestjs/common';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { SourcePluginsService } from './source-plugins.service';
import { AuthGuard, GetUser } from 'src/auth/auth.guard';
import { AuthGuard, GetAuthToken, GetUser } from 'src/auth/auth.guard';
import { Request, Response } from 'express';
import { Readable } from 'stream';
import { User } from 'src/users/user.entity';
import { UserSourcesService } from 'src/users/user-sources/user-sources.service';
import { PluginSettingsTemplate } from 'plugins/plugin-types';
import {
PlaybackConfigDto,
PluginSettingsDto,
PluginSettingsTemplateDto,
ValidationResponsekDto,
SourceListDto as VideoStreamListDto,
ValidationResponsekDto as ValidationResponseDto,
VideoStreamDto,
} from './source-plugins.dto';
export const JELLYFIN_DEVICE_ID = 'Reiverr Client';
@ApiTags('sources')
@Controller('sources')
@Controller()
@UseGuards(AuthGuard)
export class SourcesController {
constructor(
@@ -37,7 +41,8 @@ export class SourcesController {
private userSourcesService: UserSourcesService,
) {}
@Get()
@ApiTags('sources')
@Get('sources')
@ApiOkResponse({
description: 'All source plugins found',
type: String,
@@ -45,11 +50,12 @@ export class SourcesController {
})
async getSourcePlugins() {
return this.sourcesService
.getLoadedPlugins()
.getPlugins()
.then((plugins) => Object.keys(plugins));
}
@Get(':sourceId/settings/template')
@ApiTags('sources')
@Get('sources/:sourceId/settings/template')
@ApiOkResponse({
description: 'Source settings template',
type: PluginSettingsTemplateDto,
@@ -70,16 +76,17 @@ export class SourcesController {
};
}
@Post(':sourceId/settings/validate')
@ApiTags('sources')
@Post('sources/:sourceId/settings/validate')
@ApiOkResponse({
description: 'Source settings validation',
type: ValidationResponsekDto,
type: ValidationResponseDto,
})
async validateSourceSettings(
@GetUser() callerUser: User,
@Param('sourceId') sourceId: string,
@Body() settings: PluginSettingsDto,
): Promise<ValidationResponsekDto> {
): Promise<ValidationResponseDto> {
const plugin = this.sourcesService.getPlugin(sourceId);
if (!plugin) {
@@ -89,12 +96,64 @@ export class SourcesController {
return plugin.validateSettings(settings.settings);
}
@Get(':sourceId/movies/:tmdbId/stream')
@ApiTags('movies')
@Get('movies/:tmdbId/sources')
@ApiOkResponse({
description: 'Movie sources',
type: VideoStreamListDto,
})
async getMovieSources(
@Param('tmdbId') tmdbId: string,
@GetUser() user: User,
@GetAuthToken() token: string,
): Promise<VideoStreamListDto> {
if (!user) {
throw new UnauthorizedException();
}
const plugins = await this.sourcesService.getPlugins();
const sources: VideoStreamListDto['sources'] = {};
for (const pluginId in plugins) {
const plugin = plugins[pluginId];
if (!plugin) continue;
const settings = this.userSourcesService.getSourceSettings(
user,
pluginId,
);
if (!settings) continue;
const videoStream = await plugin.getMovieStream(tmdbId, {
settings,
token,
});
if (!videoStream) continue;
sources[pluginId] = videoStream;
}
return {
sources,
};
}
@ApiTags('movies')
@Post('movies/:tmdbId/sources/:sourceId/stream')
@ApiOkResponse({
description: 'Movie stream',
type: VideoStreamDto,
})
async getMovieStream(
@Param('sourceId') sourceId: string,
@Param('tmdbId') tmdbId: string,
@GetUser() user: User,
) {
@GetAuthToken() token: string,
@Body() config: PlaybackConfigDto,
): Promise<VideoStreamDto> {
if (!user) {
throw new UnauthorizedException();
}
@@ -105,12 +164,18 @@ export class SourcesController {
throw new BadRequestException('Source configuration not found');
}
return this.sourcesService
.getPlugin(sourceId)
?.getMovieStream(tmdbId, settings);
return this.sourcesService.getPlugin(sourceId)?.getMovieStream(
tmdbId,
{
settings,
token,
},
config,
);
}
@All(':sourceId/movies/:tmdbId/stream/*')
@ApiTags('movies')
@All('movies/:tmdbId/sources/:sourceId/stream/*')
async getMovieStreamProxy(
@Param() params: any,
@Req() req: Request,
@@ -119,6 +184,9 @@ export class SourcesController {
) {
const sourceId = params.sourceId;
const settings = this.userSourcesService.getSourceSettings(user, sourceId);
if (!settings) throw new UnauthorizedException();
const { url, headers } = this.sourcesService
.getPlugin(sourceId)
?.handleProxy(
@@ -133,7 +201,7 @@ export class SourcesController {
method: req.method || 'GET',
headers: {
...headers,
Authorization: `MediaBrowser DeviceId="${JELLYFIN_DEVICE_ID}", Token="${settings.apiKey}"`,
// Authorization: `MediaBrowser DeviceId="${JELLYFIN_DEVICE_ID}", Token="${settings.apiKey}"`,
},
});