feat: Plugin source improvements

This commit is contained in:
Aleksi Lassila
2024-12-19 01:12:28 +02:00
parent fbe622e53f
commit 1e15dfac4c
13 changed files with 375 additions and 193 deletions

5
backend/.env.example Normal file
View File

@@ -0,0 +1,5 @@
JWT_SECRET=secret
TMDB_API_KEY=
ADMIN_USERNAME=admin
ADMIN_PASSWORD=admin
TMDB_CACHE_TTL=259200000 # 3 days

View File

@@ -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',
},
};

View File

@@ -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 {

View File

@@ -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>;
}
/**

View File

@@ -24,3 +24,4 @@ import { SourcePluginsModule } from './source-plugins/source-plugins.module';
providers: [AppService],
})
export class AppModule {}

View File

@@ -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);
// }
}

View File

@@ -59,7 +59,7 @@ export class PlayState {
progress: number = 0;
@ApiProperty({
type: 'date',
type: 'string',
description: 'Last time the user played this media',
})
@UpdateDateColumn()

View File

@@ -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>,
) {}

View File

@@ -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
})
};
}

View File

@@ -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,

View File

@@ -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 }))
)

View File

@@ -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]}

View File

@@ -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
>