mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-17 21:53:12 +02:00
feat: Plugin source improvements
This commit is contained in:
5
backend/.env.example
Normal file
5
backend/.env.example
Normal file
@@ -0,0 +1,5 @@
|
||||
JWT_SECRET=secret
|
||||
TMDB_API_KEY=
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=admin
|
||||
TMDB_CACHE_TTL=259200000 # 3 days
|
||||
@@ -21,5 +21,7 @@ module.exports = {
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'prettier/prettier': 'warn',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { generateApi, generateTemplates } from 'swagger-typescript-api';
|
||||
import {
|
||||
BaseItemKind,
|
||||
ItemFields,
|
||||
@@ -26,6 +25,7 @@ import {
|
||||
formatTicksToTime,
|
||||
getClosestBitrate,
|
||||
} from './utils';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
interface JellyfinSettings extends PluginSettings {
|
||||
apiKey: string;
|
||||
@@ -43,8 +43,8 @@ const JELLYFIN_DEVICE_ID = 'Reiverr Client';
|
||||
export default class JellyfinPlugin implements SourcePlugin {
|
||||
name: string = 'jellyfin';
|
||||
|
||||
private getProxyUrl(tmdbId: string) {
|
||||
return `/api/movies/${tmdbId}/sources/${this.name}/stream/proxy`;
|
||||
private getProxyUrl() {
|
||||
return `/api/sources/${this.name}/proxy`;
|
||||
}
|
||||
|
||||
validateSettings: (settings: JellyfinSettings) => Promise<{
|
||||
@@ -147,8 +147,6 @@ export default class JellyfinPlugin implements SourcePlugin {
|
||||
};
|
||||
};
|
||||
|
||||
getIsIndexable: () => boolean = () => true;
|
||||
|
||||
getSettingsTemplate: () => PluginSettingsTemplate = () => ({
|
||||
baseUrl: {
|
||||
type: 'string',
|
||||
@@ -173,15 +171,15 @@ export default class JellyfinPlugin implements SourcePlugin {
|
||||
episode: number,
|
||||
) => Promise<any>;
|
||||
|
||||
handleProxy({ uri, headers }, settings: JellyfinSettings) {
|
||||
return {
|
||||
url: `${settings.baseUrl}/${uri}`,
|
||||
headers: {
|
||||
...headers,
|
||||
Authorization: `MediaBrowser DeviceId="${JELLYFIN_DEVICE_ID}", Token="${settings.apiKey}"`,
|
||||
},
|
||||
};
|
||||
}
|
||||
// handleProxy({ uri, headers }, settings: JellyfinSettings) {
|
||||
// return {
|
||||
// url: `https://tmstr2.luminousstreamhaven.com/${uri}`,
|
||||
// headers: {
|
||||
// ...headers,
|
||||
// // Authorization: `MediaBrowser DeviceId="${JELLYFIN_DEVICE_ID}", Token="${settings.apiKey}"`,
|
||||
// },
|
||||
// };
|
||||
// }
|
||||
|
||||
private async getLibraryItems(context: PluginContext) {
|
||||
return context.api.items
|
||||
@@ -235,7 +233,7 @@ export default class JellyfinPlugin implements SourcePlugin {
|
||||
): Promise<VideoStream> {
|
||||
const context = new PluginContext(userContext.settings, userContext.token);
|
||||
const items = await this.getLibraryItems(context);
|
||||
const proxyUrl = this.getProxyUrl(tmdbId);
|
||||
const proxyUrl = this.getProxyUrl();
|
||||
|
||||
const movie = items.find((item) => item.ProviderIds?.Tmdb === tmdbId);
|
||||
|
||||
@@ -322,7 +320,7 @@ export default class JellyfinPlugin implements SourcePlugin {
|
||||
}));
|
||||
|
||||
return {
|
||||
key: '',
|
||||
key: '0',
|
||||
title: movie.Name,
|
||||
properties: [
|
||||
{
|
||||
@@ -361,11 +359,40 @@ export default class JellyfinPlugin implements SourcePlugin {
|
||||
qualityIndex: getClosestBitrate(qualities, bitrate).index,
|
||||
subtitles,
|
||||
uri: playbackUri,
|
||||
// uri:
|
||||
// proxyUrl +
|
||||
// '/stream_new2/H4sIAAAAAAAAAw3OWXKDIAAA0Cvhggn9TBqSuJARBcU_CloiYp2Ojcvpm3eCB2EXASWjIAwRUkd4AF7XdYdQAY0kVPIjDTghrElZT0EJqGlv5I_64V5UOk58vOSO7F8bcjKYnvmusRg0zLe5Lv2YaWsSUpFMuTXOAAS5O66s_H5RBpbWrmftnV4JuIdZ8LNrf1laHs_FTqkMmro4z7CsSS7sRNpx2liFotJ5TPY45Q6tms3R45NSdYWGWZ6yvTm14.lXAV7r67IyOy85n5JHjQeFzV0z0guHo2YcrCzQQoEumgIZxrlQgQir2m4suLyPK22t6eX7nmG.Sn8SxRNdH7dBNKMxxGucvgyj8Lind4D.AeRg7d1BAQAA/master.m3u8' +
|
||||
// `?reiverr_token=${userContext.token}`,
|
||||
directPlay:
|
||||
!!mediasSource?.SupportsDirectPlay ||
|
||||
!!mediasSource?.SupportsDirectStream,
|
||||
};
|
||||
}
|
||||
|
||||
proxyHandler?: (
|
||||
req: any,
|
||||
res: any,
|
||||
options: { context: UserContext; uri: string },
|
||||
) => Promise<any> = async (req, res, { context, uri }) => {
|
||||
const settings = context.settings as JellyfinSettings;
|
||||
|
||||
const url = settings.baseUrl + uri;
|
||||
|
||||
const proxyRes = await fetch(url, {
|
||||
method: req.method || 'GET',
|
||||
headers: {
|
||||
Authorization: `MediaBrowser DeviceId="${JELLYFIN_DEVICE_ID}", Token="${settings.apiKey}"`,
|
||||
},
|
||||
}).catch((e) => {
|
||||
console.error('error fetching proxy response', e);
|
||||
res.status(500).send('Error fetching proxy response');
|
||||
});
|
||||
|
||||
if (!proxyRes) return;
|
||||
|
||||
res.status(proxyRes.status);
|
||||
Readable.from(proxyRes.body).pipe(res);
|
||||
};
|
||||
}
|
||||
|
||||
class PluginContext {
|
||||
|
||||
@@ -112,9 +112,7 @@ export class PaginationParams {
|
||||
export interface SourcePlugin {
|
||||
name: string;
|
||||
|
||||
getIsIndexable: () => boolean;
|
||||
|
||||
getMovieIndex: (
|
||||
getMovieIndex?: (
|
||||
context: UserContext,
|
||||
pagination: PaginationParams,
|
||||
) => Promise<PaginatedResponse<IndexItem>>;
|
||||
@@ -147,13 +145,19 @@ export interface SourcePlugin {
|
||||
settings: PluginSettings,
|
||||
) => Promise<any>;
|
||||
|
||||
handleProxy(
|
||||
request: { uri: string; headers: any },
|
||||
settings: PluginSettings,
|
||||
): {
|
||||
url: string;
|
||||
headers: any;
|
||||
};
|
||||
// handleProxy(
|
||||
// request: { uri: string; headers: any },
|
||||
// settings: PluginSettings,
|
||||
// ): {
|
||||
// url: string;
|
||||
// headers: any;
|
||||
// };
|
||||
|
||||
proxyHandler?: (
|
||||
req: any,
|
||||
res: any,
|
||||
options: { context: UserContext; uri: string, targetUrl?: string },
|
||||
) => Promise<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,3 +24,4 @@ import { SourcePluginsModule } from './source-plugins/source-plugins.module';
|
||||
providers: [AppService],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -64,6 +64,7 @@ export class ValidateSourcePluginPipe implements PipeTransform {
|
||||
}
|
||||
}
|
||||
|
||||
@ApiTags('sources')
|
||||
@Controller()
|
||||
@UseGuards(UserAccessControl)
|
||||
export class SourcesController {
|
||||
@@ -72,7 +73,6 @@ export class SourcesController {
|
||||
private userSourcesService: UserSourcesService,
|
||||
) {}
|
||||
|
||||
@ApiTags('sources')
|
||||
@Get('sources')
|
||||
@ApiOkResponse({
|
||||
description: 'All source plugins found',
|
||||
@@ -85,7 +85,6 @@ export class SourcesController {
|
||||
.then((plugins) => Object.keys(plugins));
|
||||
}
|
||||
|
||||
@ApiTags('sources')
|
||||
@Get('sources/:sourceId/settings/template')
|
||||
@ApiOkResponse({
|
||||
description: 'Source settings template',
|
||||
@@ -107,7 +106,6 @@ export class SourcesController {
|
||||
};
|
||||
}
|
||||
|
||||
@ApiTags('sources')
|
||||
@Post('sources/:sourceId/settings/validate')
|
||||
@ApiOkResponse({
|
||||
description: 'Source settings validation',
|
||||
@@ -127,7 +125,6 @@ export class SourcesController {
|
||||
return plugin.validateSettings(settings.settings);
|
||||
}
|
||||
|
||||
@ApiTags('sources')
|
||||
@Get('sources/:sourceId/capabilities')
|
||||
@ApiOkResponse({
|
||||
type: SourcePluginCapabilitiesDto,
|
||||
@@ -152,7 +149,6 @@ export class SourcesController {
|
||||
});
|
||||
}
|
||||
|
||||
@ApiTags('sources')
|
||||
@Get('sources/:sourceId/index/movies')
|
||||
@PaginatedApiOkResponse(IndexItemDto)
|
||||
async getSourceMovieIndex(
|
||||
@@ -170,6 +166,10 @@ export class SourcesController {
|
||||
throw new BadRequestException('Source configuration not found');
|
||||
}
|
||||
|
||||
if (!plugin.getMovieIndex) {
|
||||
throw new BadRequestException('Plugin does not support indexing');
|
||||
}
|
||||
|
||||
return plugin.getMovieIndex(
|
||||
{
|
||||
settings,
|
||||
@@ -179,8 +179,7 @@ export class SourcesController {
|
||||
);
|
||||
}
|
||||
|
||||
@ApiTags('movies')
|
||||
@Get('movies/:tmdbId/sources/:sourceId/streams')
|
||||
@Get('sources/:sourceId/movies/tmdb/:tmdbId/streams')
|
||||
@ApiOkResponse({
|
||||
description: 'Movie sources',
|
||||
type: VideoStreamListDto,
|
||||
@@ -242,8 +241,7 @@ export class SourcesController {
|
||||
// };
|
||||
}
|
||||
|
||||
@ApiTags('movies')
|
||||
@Post('movies/:tmdbId/sources/:sourceId/stream')
|
||||
@Post('sources/:sourceId/movies/tmdb/:tmdbId/streams/:key')
|
||||
@ApiOkResponse({
|
||||
description: 'Movie stream',
|
||||
type: VideoStreamDto,
|
||||
@@ -251,7 +249,8 @@ export class SourcesController {
|
||||
async getMovieStream(
|
||||
@Param('tmdbId') tmdbId: string,
|
||||
@Param('sourceId') sourceId: string,
|
||||
@Query('key') key: string,
|
||||
// @Query('key') key: string,
|
||||
@Param('key') key: string,
|
||||
@GetAuthUser() user: User,
|
||||
@GetAuthToken() token: string,
|
||||
@Body() config: PlaybackConfigDto,
|
||||
@@ -288,38 +287,75 @@ export class SourcesController {
|
||||
});
|
||||
}
|
||||
|
||||
@ApiTags('movies')
|
||||
@All('movies/:tmdbId/sources/:sourceId/stream/proxy/*')
|
||||
async getMovieStreamProxy(
|
||||
/** @deprecated */
|
||||
@All(['sources/:sourceId/proxy', 'sources/:sourceId/proxy/*'])
|
||||
async movieStreamProxy(
|
||||
@Param() params: any,
|
||||
@Query() query: any,
|
||||
@Req() req: Request,
|
||||
@Res() res: Response,
|
||||
@GetAuthUser() user: User,
|
||||
@GetAuthToken() token: string,
|
||||
) {
|
||||
const sourceId = params.sourceId;
|
||||
const settings = this.userSourcesService.getSourceSettings(user, sourceId);
|
||||
|
||||
if (!settings) throw new UnauthorizedException();
|
||||
|
||||
const { url, headers } = this.sourcesService
|
||||
.getPlugin(sourceId)
|
||||
?.handleProxy(
|
||||
{
|
||||
uri: params[0] + '?' + req.url.split('?')[1],
|
||||
headers: req.headers,
|
||||
},
|
||||
settings,
|
||||
);
|
||||
const plugin = this.sourcesService.getPlugin(sourceId);
|
||||
|
||||
const proxyRes = await fetch(url, {
|
||||
method: req.method || 'GET',
|
||||
headers: {
|
||||
...headers,
|
||||
// Authorization: `MediaBrowser DeviceId="${JELLYFIN_DEVICE_ID}", Token="${settings.apiKey}"`,
|
||||
},
|
||||
if (!plugin) {
|
||||
throw new NotFoundException('Plugin not found');
|
||||
}
|
||||
|
||||
if (!plugin.proxyHandler) {
|
||||
throw new BadRequestException('Plugin does not support proxying');
|
||||
}
|
||||
|
||||
const targetUrl = query.reiverr_proxy_url || undefined;
|
||||
|
||||
await plugin.proxyHandler?.(req, res, {
|
||||
context: { settings, token },
|
||||
uri: `/${params[0]}?${req.url.split('?')[1] || ''}`,
|
||||
targetUrl,
|
||||
});
|
||||
|
||||
Readable.from(proxyRes.body).pipe(res);
|
||||
res.status(proxyRes.status);
|
||||
}
|
||||
|
||||
// @All('movies/:tmdbId/sources/:sourceId/stream/proxy/*')
|
||||
// async getMovieStreamProxy(
|
||||
// @Param() params: any,
|
||||
// @Req() req: Request,
|
||||
// @Res() res: Response,
|
||||
// @GetAuthUser() user: User,
|
||||
// ) {
|
||||
// const sourceId = params.sourceId;
|
||||
// const settings = this.userSourcesService.getSourceSettings(user, sourceId);
|
||||
|
||||
// if (!settings) throw new UnauthorizedException();
|
||||
|
||||
// const { url, headers } = this.sourcesService
|
||||
// .getPlugin(sourceId)
|
||||
// ?.handleProxy(
|
||||
// {
|
||||
// uri: params[0] + '?' + req.url.split('?')[1],
|
||||
// headers: req.headers,
|
||||
// },
|
||||
// settings,
|
||||
// );
|
||||
|
||||
// // console.log('url', url.split('?')[0]);
|
||||
// const proxyRes = await fetch(url.split('?')[0], {
|
||||
// method: req.method || 'GET',
|
||||
// headers: {
|
||||
// // ...headers,
|
||||
// // Authorization: `MediaBrowser DeviceId="${JELLYFIN_DEVICE_ID}", Token="${settings.apiKey}"`,
|
||||
// },
|
||||
// }).catch((e) => {
|
||||
// console.error('error fetching proxy response', e);
|
||||
// throw new InternalServerErrorException();
|
||||
// });
|
||||
|
||||
// Readable.from(proxyRes.body).pipe(res);
|
||||
// res.status(proxyRes.status);
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ export class PlayState {
|
||||
progress: number = 0;
|
||||
|
||||
@ApiProperty({
|
||||
type: 'date',
|
||||
type: 'string',
|
||||
description: 'Last time the user played this media',
|
||||
})
|
||||
@UpdateDateColumn()
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Repository } from 'typeorm';
|
||||
import { User } from './user.entity';
|
||||
import { CreateUserDto, UpdateUserDto } from './user.dto';
|
||||
import { USER_REPOSITORY } from './user.providers';
|
||||
|
||||
export enum UserServiceError {
|
||||
PasswordMismatch = 'PasswordMismatch',
|
||||
@@ -12,7 +13,7 @@ export enum UserServiceError {
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
@Inject('USER_REPOSITORY')
|
||||
@Inject(USER_REPOSITORY)
|
||||
private readonly userRepository: Repository<User>,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ export interface PlayState {
|
||||
*/
|
||||
progress?: number;
|
||||
/** Last time the user played this media */
|
||||
lastPlayedAt: date;
|
||||
lastPlayedAt: string;
|
||||
}
|
||||
|
||||
export interface LibraryItem {
|
||||
@@ -133,7 +133,7 @@ export interface PlayStateDto {
|
||||
*/
|
||||
progress?: number;
|
||||
/** Last time the user played this media */
|
||||
lastPlayedAt: date;
|
||||
lastPlayedAt: string;
|
||||
}
|
||||
|
||||
export interface MovieUserDataDto {
|
||||
@@ -155,7 +155,10 @@ export interface PaginatedResponseDto {
|
||||
itemsPerPage: number;
|
||||
}
|
||||
|
||||
export type MovieDto = object;
|
||||
export interface MovieDto {
|
||||
id?: string;
|
||||
tmdbId: string;
|
||||
}
|
||||
|
||||
export interface LibraryItemDto {
|
||||
tmdbId: string;
|
||||
@@ -946,75 +949,17 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||
path: `/api`,
|
||||
method: 'GET',
|
||||
...params
|
||||
})
|
||||
};
|
||||
movies = {
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags movies
|
||||
* @name GetMovieByTmdbId
|
||||
* @request GET:/api/movies/tmdb/{tmdbId}
|
||||
*/
|
||||
getMovieByTmdbId: (tmdbId: string, params: RequestParams = {}) =>
|
||||
this.request<MovieDto, any>({
|
||||
path: `/api/movies/tmdb/${tmdbId}`,
|
||||
method: 'GET',
|
||||
format: 'json',
|
||||
...params
|
||||
}),
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags movies
|
||||
* @name GetMovieStreams
|
||||
* @request GET:/api/movies/{tmdbId}/sources/{sourceId}/streams
|
||||
* @name TmdbProxyGet
|
||||
* @request GET:/api/tmdb/v3/proxy/*
|
||||
*/
|
||||
getMovieStreams: (tmdbId: string, sourceId: string, params: RequestParams = {}) =>
|
||||
this.request<VideoStreamListDto, any>({
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/streams`,
|
||||
method: 'GET',
|
||||
format: 'json',
|
||||
...params
|
||||
}),
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags movies
|
||||
* @name GetMovieStream
|
||||
* @request POST:/api/movies/{tmdbId}/sources/{sourceId}/stream
|
||||
*/
|
||||
getMovieStream: (
|
||||
tmdbId: string,
|
||||
sourceId: string,
|
||||
query: {
|
||||
key: string;
|
||||
},
|
||||
data: PlaybackConfigDto,
|
||||
params: RequestParams = {}
|
||||
) =>
|
||||
this.request<VideoStreamDto, any>({
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream`,
|
||||
method: 'POST',
|
||||
query: query,
|
||||
body: data,
|
||||
type: ContentType.Json,
|
||||
format: 'json',
|
||||
...params
|
||||
}),
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags movies
|
||||
* @name GetMovieStreamProxyGet
|
||||
* @request GET:/api/movies/{tmdbId}/sources/{sourceId}/stream/proxy/*
|
||||
*/
|
||||
getMovieStreamProxyGet: (tmdbId: string, sourceId: string, params: RequestParams = {}) =>
|
||||
tmdbProxyGet: (params: RequestParams = {}) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream/proxy/*`,
|
||||
path: `/api/tmdb/v3/proxy/*`,
|
||||
method: 'GET',
|
||||
...params
|
||||
}),
|
||||
@@ -1022,13 +967,12 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags movies
|
||||
* @name GetMovieStreamProxyPost
|
||||
* @request POST:/api/movies/{tmdbId}/sources/{sourceId}/stream/proxy/*
|
||||
* @name TmdbProxyPost
|
||||
* @request POST:/api/tmdb/v3/proxy/*
|
||||
*/
|
||||
getMovieStreamProxyPost: (tmdbId: string, sourceId: string, params: RequestParams = {}) =>
|
||||
tmdbProxyPost: (params: RequestParams = {}) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream/proxy/*`,
|
||||
path: `/api/tmdb/v3/proxy/*`,
|
||||
method: 'POST',
|
||||
...params
|
||||
}),
|
||||
@@ -1036,13 +980,12 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags movies
|
||||
* @name GetMovieStreamProxyPut
|
||||
* @request PUT:/api/movies/{tmdbId}/sources/{sourceId}/stream/proxy/*
|
||||
* @name TmdbProxyPut
|
||||
* @request PUT:/api/tmdb/v3/proxy/*
|
||||
*/
|
||||
getMovieStreamProxyPut: (tmdbId: string, sourceId: string, params: RequestParams = {}) =>
|
||||
tmdbProxyPut: (params: RequestParams = {}) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream/proxy/*`,
|
||||
path: `/api/tmdb/v3/proxy/*`,
|
||||
method: 'PUT',
|
||||
...params
|
||||
}),
|
||||
@@ -1050,13 +993,12 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags movies
|
||||
* @name GetMovieStreamProxyDelete
|
||||
* @request DELETE:/api/movies/{tmdbId}/sources/{sourceId}/stream/proxy/*
|
||||
* @name TmdbProxyDelete
|
||||
* @request DELETE:/api/tmdb/v3/proxy/*
|
||||
*/
|
||||
getMovieStreamProxyDelete: (tmdbId: string, sourceId: string, params: RequestParams = {}) =>
|
||||
tmdbProxyDelete: (params: RequestParams = {}) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream/proxy/*`,
|
||||
path: `/api/tmdb/v3/proxy/*`,
|
||||
method: 'DELETE',
|
||||
...params
|
||||
}),
|
||||
@@ -1064,13 +1006,12 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags movies
|
||||
* @name GetMovieStreamProxyPatch
|
||||
* @request PATCH:/api/movies/{tmdbId}/sources/{sourceId}/stream/proxy/*
|
||||
* @name TmdbProxyPatch
|
||||
* @request PATCH:/api/tmdb/v3/proxy/*
|
||||
*/
|
||||
getMovieStreamProxyPatch: (tmdbId: string, sourceId: string, params: RequestParams = {}) =>
|
||||
tmdbProxyPatch: (params: RequestParams = {}) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream/proxy/*`,
|
||||
path: `/api/tmdb/v3/proxy/*`,
|
||||
method: 'PATCH',
|
||||
...params
|
||||
}),
|
||||
@@ -1078,13 +1019,12 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags movies
|
||||
* @name GetMovieStreamProxyOptions
|
||||
* @request OPTIONS:/api/movies/{tmdbId}/sources/{sourceId}/stream/proxy/*
|
||||
* @name TmdbProxyOptions
|
||||
* @request OPTIONS:/api/tmdb/v3/proxy/*
|
||||
*/
|
||||
getMovieStreamProxyOptions: (tmdbId: string, sourceId: string, params: RequestParams = {}) =>
|
||||
tmdbProxyOptions: (params: RequestParams = {}) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream/proxy/*`,
|
||||
path: `/api/tmdb/v3/proxy/*`,
|
||||
method: 'OPTIONS',
|
||||
...params
|
||||
}),
|
||||
@@ -1092,13 +1032,12 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags movies
|
||||
* @name GetMovieStreamProxyHead
|
||||
* @request HEAD:/api/movies/{tmdbId}/sources/{sourceId}/stream/proxy/*
|
||||
* @name TmdbProxyHead
|
||||
* @request HEAD:/api/tmdb/v3/proxy/*
|
||||
*/
|
||||
getMovieStreamProxyHead: (tmdbId: string, sourceId: string, params: RequestParams = {}) =>
|
||||
tmdbProxyHead: (params: RequestParams = {}) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream/proxy/*`,
|
||||
path: `/api/tmdb/v3/proxy/*`,
|
||||
method: 'HEAD',
|
||||
...params
|
||||
}),
|
||||
@@ -1106,13 +1045,12 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags movies
|
||||
* @name GetMovieStreamProxySearch
|
||||
* @request SEARCH:/api/movies/{tmdbId}/sources/{sourceId}/stream/proxy/*
|
||||
* @name TmdbProxySearch
|
||||
* @request SEARCH:/api/tmdb/v3/proxy/*
|
||||
*/
|
||||
getMovieStreamProxySearch: (tmdbId: string, sourceId: string, params: RequestParams = {}) =>
|
||||
tmdbProxySearch: (params: RequestParams = {}) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/movies/${tmdbId}/sources/${sourceId}/stream/proxy/*`,
|
||||
path: `/api/tmdb/v3/proxy/*`,
|
||||
method: 'SEARCH',
|
||||
...params
|
||||
})
|
||||
@@ -1202,6 +1140,156 @@ export class Api<SecurityDataType extends unknown> extends HttpClient<SecurityDa
|
||||
method: 'GET',
|
||||
format: 'json',
|
||||
...params
|
||||
}),
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags sources
|
||||
* @name GetMovieStreams
|
||||
* @request GET:/api/sources/{sourceId}/movies/tmdb/{tmdbId}/streams
|
||||
*/
|
||||
getMovieStreams: (tmdbId: string, sourceId: string, params: RequestParams = {}) =>
|
||||
this.request<VideoStreamListDto, any>({
|
||||
path: `/api/sources/${sourceId}/movies/tmdb/${tmdbId}/streams`,
|
||||
method: 'GET',
|
||||
format: 'json',
|
||||
...params
|
||||
}),
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags sources
|
||||
* @name GetMovieStream
|
||||
* @request POST:/api/sources/{sourceId}/movies/tmdb/{tmdbId}/streams/{key}
|
||||
*/
|
||||
getMovieStream: (
|
||||
tmdbId: string,
|
||||
sourceId: string,
|
||||
key: string,
|
||||
data: PlaybackConfigDto,
|
||||
params: RequestParams = {}
|
||||
) =>
|
||||
this.request<VideoStreamDto, any>({
|
||||
path: `/api/sources/${sourceId}/movies/tmdb/${tmdbId}/streams/${key}`,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
type: ContentType.Json,
|
||||
format: 'json',
|
||||
...params
|
||||
}),
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags sources
|
||||
* @name MovieStreamProxyGet
|
||||
* @request GET:/api/sources/{sourceId}/proxy/*
|
||||
*/
|
||||
movieStreamProxyGet: (sourceId: string, params: RequestParams = {}) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/sources/${sourceId}/proxy/*`,
|
||||
method: 'GET',
|
||||
...params
|
||||
}),
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags sources
|
||||
* @name MovieStreamProxyPost
|
||||
* @request POST:/api/sources/{sourceId}/proxy/*
|
||||
*/
|
||||
movieStreamProxyPost: (sourceId: string, params: RequestParams = {}) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/sources/${sourceId}/proxy/*`,
|
||||
method: 'POST',
|
||||
...params
|
||||
}),
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags sources
|
||||
* @name MovieStreamProxyPut
|
||||
* @request PUT:/api/sources/{sourceId}/proxy/*
|
||||
*/
|
||||
movieStreamProxyPut: (sourceId: string, params: RequestParams = {}) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/sources/${sourceId}/proxy/*`,
|
||||
method: 'PUT',
|
||||
...params
|
||||
}),
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags sources
|
||||
* @name MovieStreamProxyDelete
|
||||
* @request DELETE:/api/sources/{sourceId}/proxy/*
|
||||
*/
|
||||
movieStreamProxyDelete: (sourceId: string, params: RequestParams = {}) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/sources/${sourceId}/proxy/*`,
|
||||
method: 'DELETE',
|
||||
...params
|
||||
}),
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags sources
|
||||
* @name MovieStreamProxyPatch
|
||||
* @request PATCH:/api/sources/{sourceId}/proxy/*
|
||||
*/
|
||||
movieStreamProxyPatch: (sourceId: string, params: RequestParams = {}) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/sources/${sourceId}/proxy/*`,
|
||||
method: 'PATCH',
|
||||
...params
|
||||
}),
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags sources
|
||||
* @name MovieStreamProxyOptions
|
||||
* @request OPTIONS:/api/sources/{sourceId}/proxy/*
|
||||
*/
|
||||
movieStreamProxyOptions: (sourceId: string, params: RequestParams = {}) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/sources/${sourceId}/proxy/*`,
|
||||
method: 'OPTIONS',
|
||||
...params
|
||||
}),
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags sources
|
||||
* @name MovieStreamProxyHead
|
||||
* @request HEAD:/api/sources/{sourceId}/proxy/*
|
||||
*/
|
||||
movieStreamProxyHead: (sourceId: string, params: RequestParams = {}) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/sources/${sourceId}/proxy/*`,
|
||||
method: 'HEAD',
|
||||
...params
|
||||
}),
|
||||
|
||||
/**
|
||||
* No description
|
||||
*
|
||||
* @tags sources
|
||||
* @name MovieStreamProxySearch
|
||||
* @request SEARCH:/api/sources/{sourceId}/proxy/*
|
||||
*/
|
||||
movieStreamProxySearch: (sourceId: string, params: RequestParams = {}) =>
|
||||
this.request<void, any>({
|
||||
path: `/api/sources/${sourceId}/proxy/*`,
|
||||
method: 'SEARCH',
|
||||
...params
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -60,20 +60,13 @@
|
||||
}
|
||||
|
||||
const refreshVideoStream = async (audioStreamIndex = 0) => {
|
||||
videoStreamP = reiverrApiNew.movies
|
||||
.getMovieStream(
|
||||
tmdbId,
|
||||
sourceId,
|
||||
{
|
||||
key
|
||||
},
|
||||
{
|
||||
// bitrate: getQualities(1080)?.[0]?.maxBitrate || 10000000,
|
||||
progress,
|
||||
audioStreamIndex,
|
||||
deviceProfile: getDeviceProfile() as any
|
||||
}
|
||||
)
|
||||
videoStreamP = reiverrApiNew.sources
|
||||
.getMovieStream(tmdbId, sourceId, key, {
|
||||
// bitrate: getQualities(1080)?.[0]?.maxBitrate || 10000000,
|
||||
progress,
|
||||
audioStreamIndex,
|
||||
deviceProfile: getDeviceProfile() as any
|
||||
})
|
||||
.then((r) => r.data)
|
||||
.then((d) => ({
|
||||
...d,
|
||||
|
||||
@@ -58,7 +58,7 @@ function usePlayerState() {
|
||||
if (!sourceId) {
|
||||
const streams = await Promise.all(
|
||||
get(sources).map((s) =>
|
||||
reiverrApiNew.movies
|
||||
reiverrApiNew.sources
|
||||
.getMovieStreams(tmdbId, s.source.id)
|
||||
.then((r) => ({ source: s.source, streams: r.data.streams }))
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
import { reiverrApiNew, sources, user } from '../../stores/user.store';
|
||||
import MovieStreams from './MovieStreams.MoviePage.svelte';
|
||||
import StreamDetailsDialog from './StreamDetailsDialog.MoviePage.svelte';
|
||||
import SelectDialog from '../../components/Dialog/SelectDialog.svelte';
|
||||
|
||||
export let id: string;
|
||||
const tmdbId = Number(id);
|
||||
@@ -47,7 +48,9 @@
|
||||
inLibrary.set(d?.inLibrary ?? false);
|
||||
progress.set(d?.playState?.progress ?? 0);
|
||||
});
|
||||
streams.forEach((p) => p.then((s) => availableForStreaming.update((p) => p || s.length > 0)));
|
||||
streams.forEach((p) =>
|
||||
p.streams.then((s) => availableForStreaming.update((p) => p || s.length > 0))
|
||||
);
|
||||
|
||||
const tmdbMovie = tmdbApi.getTmdbMovie(tmdbId);
|
||||
$: recommendations = tmdbApi.getMovieRecommendations(tmdbId);
|
||||
@@ -56,16 +59,16 @@
|
||||
id
|
||||
);
|
||||
|
||||
function getStreams(): Map<MediaSource, Promise<VideoStreamCandidateDto[]>> {
|
||||
const out = new Map();
|
||||
function getStreams() {
|
||||
const out: { source: MediaSource; streams: Promise<VideoStreamCandidateDto[]> }[] = [];
|
||||
|
||||
for (const source of get(sources)) {
|
||||
out.set(
|
||||
source.source,
|
||||
reiverrApiNew.movies
|
||||
out.push({
|
||||
source: source.source,
|
||||
streams: reiverrApiNew.sources
|
||||
.getMovieStreams(id, source.source.id)
|
||||
.then((r) => r.data?.streams ?? [])
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return out;
|
||||
@@ -168,6 +171,32 @@
|
||||
.then((r) => r.data.success);
|
||||
if (success) inLibrary.set(false);
|
||||
}
|
||||
|
||||
async function handlePlay() {
|
||||
const awaited = await Promise.all(
|
||||
streams.map(async (p) => ({ ...p, streams: await p.streams }))
|
||||
);
|
||||
|
||||
const numberOfStreams = awaited.reduce((acc, p) => acc + p.streams.length, 0);
|
||||
|
||||
// If more than 1 stream
|
||||
if (numberOfStreams > 1) {
|
||||
modalStack.create(SelectDialog, {
|
||||
title: 'Select Media Source',
|
||||
subtitle: 'Select the media source you want to use',
|
||||
options: awaited.map((p) => p.source.id),
|
||||
handleSelectOption: (sourceId) => {
|
||||
const key = awaited.find((p) => p.source.id === sourceId)?.streams[0]?.key;
|
||||
movieUserData.then((userData) => playerState.streamMovie(id, userData, sourceId, key));
|
||||
}
|
||||
});
|
||||
} else if (numberOfStreams === 1) {
|
||||
const sourceId = awaited[0]?.source.id;
|
||||
const key = awaited[0]?.streams[0]?.key;
|
||||
|
||||
movieUserData.then((userData) => playerState.streamMovie(id, userData, sourceId, key));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<DetachedPage let:handleGoBack let:registrar>
|
||||
@@ -228,11 +257,7 @@
|
||||
on:back={handleGoBack}
|
||||
on:mount={registrar}
|
||||
>
|
||||
<Button
|
||||
class="mr-4"
|
||||
action={() => movieUserData.then((userData) => playerState.streamMovie(id, userData))}
|
||||
disabled={!$availableForStreaming}
|
||||
>
|
||||
<Button class="mr-4" action={handlePlay} disabled={!$availableForStreaming}>
|
||||
Play
|
||||
<Play size={19} slot="icon" />
|
||||
</Button>
|
||||
@@ -347,8 +372,8 @@
|
||||
</Container>
|
||||
{/await}
|
||||
|
||||
{#if streams.size}
|
||||
<MovieStreams {streams} {createStreamDetailsDialog} />
|
||||
{#if streams.length}
|
||||
<MovieStreams sources={streams} {createStreamDetailsDialog} />
|
||||
{/if}
|
||||
|
||||
<!-- {#await Promise.all([tmdbMovie, radarrFiles, radarrDownloads]) then [movie, files, downloads]}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import StreamDetailsDialog from './StreamDetailsDialog.MoviePage.svelte';
|
||||
import { modalStack } from '../../components/Modal/modal.store';
|
||||
|
||||
export let streams: Map<MediaSource, Promise<VideoStreamCandidateDto[]>>;
|
||||
export let sources: { source: MediaSource; streams: Promise<VideoStreamCandidateDto[]> }[];
|
||||
export let createStreamDetailsDialog: (
|
||||
source: MediaSource,
|
||||
stream: VideoStreamCandidateDto
|
||||
@@ -18,13 +18,13 @@
|
||||
class="flex-1 bg-secondary-950 pt-8 pb-16 px-32 flex flex-col"
|
||||
on:enter={scrollIntoView({ top: 32 })}
|
||||
>
|
||||
{#each [...streams.keys()] as source}
|
||||
{#await streams.get(source)}
|
||||
{#each sources as source}
|
||||
{#await source.streams}
|
||||
Loading...
|
||||
{:then streams}
|
||||
{#if streams?.length}
|
||||
<h1 class="font-medium tracking-wide text-2xl text-zinc-300 mb-8">
|
||||
{capitalize(source.id)}
|
||||
{capitalize(source.source.id)}
|
||||
</h1>
|
||||
<Container
|
||||
direction="grid"
|
||||
@@ -46,7 +46,7 @@
|
||||
// 'bg-secondary-800 focus-within:border-zinc-300': !selected
|
||||
}
|
||||
)}
|
||||
on:clickOrSelect={() => createStreamDetailsDialog(source, stream)}
|
||||
on:clickOrSelect={() => createStreamDetailsDialog(source.source, stream)}
|
||||
on:enter={scrollIntoView({ vertical: 128 })}
|
||||
focusOnClick
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user