mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-18 11:53:15 +02:00
feat: Improved movie playback and listing stream options
This commit is contained in:
@@ -13,7 +13,14 @@ import {
|
||||
Subtitles,
|
||||
UserContext,
|
||||
VideoStream,
|
||||
VideoStreamCandidate,
|
||||
} from 'plugins/plugin-types';
|
||||
import {
|
||||
bitrateQualities,
|
||||
formatSize,
|
||||
formatTicksToTime,
|
||||
getClosestBitrate,
|
||||
} from './utils';
|
||||
|
||||
interface JellyfinSettings extends PluginSettings {
|
||||
apiKey: string;
|
||||
@@ -27,68 +34,14 @@ interface JellyfinUserContext extends UserContext {
|
||||
|
||||
const JELLYFIN_DEVICE_ID = 'Reiverr Client';
|
||||
|
||||
const bitrateQualities = [
|
||||
{
|
||||
label: '4K - 120 Mbps',
|
||||
bitrate: 120000000,
|
||||
codec: undefined,
|
||||
},
|
||||
{
|
||||
label: '4K - 80 Mbps',
|
||||
bitrate: 80000000,
|
||||
codec: undefined,
|
||||
},
|
||||
{
|
||||
label: '1080p - 40 Mbps',
|
||||
bitrate: 40000000,
|
||||
codec: undefined,
|
||||
},
|
||||
{
|
||||
label: '1080p - 10 Mbps',
|
||||
bitrate: 10000000,
|
||||
codec: undefined,
|
||||
},
|
||||
{
|
||||
label: '720p - 8 Mbps',
|
||||
bitrate: 8000000,
|
||||
codec: undefined,
|
||||
},
|
||||
{
|
||||
label: '720p - 4 Mbps',
|
||||
bitrate: 4000000,
|
||||
codec: undefined,
|
||||
},
|
||||
{
|
||||
label: '480p - 3 Mbps',
|
||||
bitrate: 3000000,
|
||||
codec: undefined,
|
||||
},
|
||||
{
|
||||
label: '480p - 720 Kbps',
|
||||
bitrate: 720000,
|
||||
codec: undefined,
|
||||
},
|
||||
{
|
||||
label: '360p - 420 Kbps',
|
||||
bitrate: 420000,
|
||||
codec: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
function getClosestBitrate(qualities, bitrate) {
|
||||
return qualities.reduce(
|
||||
(prev, curr) =>
|
||||
Math.abs(curr.bitrate - bitrate) < Math.abs(prev.bitrate - bitrate)
|
||||
? curr
|
||||
: prev,
|
||||
qualities[0],
|
||||
);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export default class JellyfinPlugin implements SourcePlugin {
|
||||
name: string = 'jellyfin';
|
||||
|
||||
private getProxyUrl(tmdbId: string) {
|
||||
return `/api/movies/${tmdbId}/sources/${this.name}/stream/proxy`;
|
||||
}
|
||||
|
||||
validateSettings: (settings: JellyfinSettings) => Promise<{
|
||||
isValid: boolean;
|
||||
errors: Record<string, string>;
|
||||
@@ -213,8 +166,25 @@ export default class JellyfinPlugin implements SourcePlugin {
|
||||
.then((res) => res.data.Items ?? []);
|
||||
}
|
||||
|
||||
async getMovieStreams(
|
||||
tmdbId: string,
|
||||
userContext: JellyfinUserContext,
|
||||
config: PlaybackConfig = {
|
||||
audioStreamIndex: undefined,
|
||||
bitrate: undefined,
|
||||
progress: undefined,
|
||||
defaultLanguage: undefined,
|
||||
deviceProfile: undefined,
|
||||
},
|
||||
): Promise<VideoStreamCandidate[]> {
|
||||
return this.getMovieStream(tmdbId, '', userContext, config).then(
|
||||
(stream) => [stream],
|
||||
);
|
||||
}
|
||||
|
||||
async getMovieStream(
|
||||
tmdbId: string,
|
||||
key: string,
|
||||
userContext: JellyfinUserContext,
|
||||
config: PlaybackConfig = {
|
||||
audioStreamIndex: undefined,
|
||||
@@ -226,7 +196,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 = `/api/movies/${tmdbId}/sources/${this.name}/stream`;
|
||||
const proxyUrl = this.getProxyUrl(tmdbId);
|
||||
|
||||
const movie = items.find((item) => item.ProviderIds?.Tmdb === tmdbId);
|
||||
|
||||
@@ -288,22 +258,20 @@ export default class JellyfinPlugin implements SourcePlugin {
|
||||
index: s.Index,
|
||||
})) ?? [];
|
||||
|
||||
const qualities = [
|
||||
const qualities: VideoStream['qualities'] = [
|
||||
...bitrateQualities,
|
||||
{
|
||||
bitrate: mediasSource.Bitrate,
|
||||
label: 'Original',
|
||||
codec: undefined,
|
||||
original: true,
|
||||
},
|
||||
].map((q, i) => ({
|
||||
...q,
|
||||
index: i,
|
||||
}));
|
||||
|
||||
const bitrate = Math.min(
|
||||
maxStreamingBitrate,
|
||||
movie.MediaSources[0].Bitrate,
|
||||
);
|
||||
const bitrate = Math.min(maxStreamingBitrate, mediasSource.Bitrate);
|
||||
|
||||
const subtitles: Subtitles[] = mediasSource.MediaStreams.filter(
|
||||
(s) => s.Type === 'Subtitle' && s.DeliveryUrl,
|
||||
@@ -315,6 +283,32 @@ export default class JellyfinPlugin implements SourcePlugin {
|
||||
}));
|
||||
|
||||
return {
|
||||
key: '',
|
||||
title: movie.Name,
|
||||
properties: [
|
||||
{
|
||||
label: 'Video',
|
||||
value: mediasSource.Bitrate || 0,
|
||||
formatted:
|
||||
mediasSource.MediaStreams.find((s) => s.Type === 'Video')
|
||||
?.DisplayTitle || 'Unknown',
|
||||
},
|
||||
{
|
||||
label: 'Size',
|
||||
value: mediasSource.Size,
|
||||
formatted: formatSize(mediasSource.Size),
|
||||
},
|
||||
{
|
||||
label: 'Filename',
|
||||
value: mediasSource.Name,
|
||||
formatted: undefined,
|
||||
},
|
||||
{
|
||||
label: 'Runtime',
|
||||
value: mediasSource.RunTimeTicks,
|
||||
formatted: formatTicksToTime(mediasSource.RunTimeTicks),
|
||||
},
|
||||
],
|
||||
audioStreamIndex:
|
||||
config.audioStreamIndex ??
|
||||
mediasSource?.DefaultAudioStreamIndex ??
|
||||
@@ -322,7 +316,7 @@ export default class JellyfinPlugin implements SourcePlugin {
|
||||
audioStreams,
|
||||
progress: config.progress ?? 0,
|
||||
qualities,
|
||||
quality: getClosestBitrate(qualities, bitrate).index,
|
||||
qualityIndex: getClosestBitrate(qualities, bitrate).index,
|
||||
subtitles,
|
||||
uri: playbackUri,
|
||||
directPlay:
|
||||
|
||||
91
backend/plugins/jellyfin.plugin/src/utils.ts
Normal file
91
backend/plugins/jellyfin.plugin/src/utils.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
export function formatSize(size: number) {
|
||||
const gbs = size / 1024 / 1024 / 1024;
|
||||
const mbs = size / 1024 / 1024;
|
||||
|
||||
if (gbs >= 1) {
|
||||
return `${gbs.toFixed(2)} GB`;
|
||||
} else {
|
||||
return `${mbs.toFixed(2)} MB`;
|
||||
}
|
||||
}
|
||||
|
||||
export const bitrateQualities = [
|
||||
{
|
||||
label: '4K - 120 Mbps',
|
||||
bitrate: 120000000,
|
||||
codec: undefined,
|
||||
original: false,
|
||||
},
|
||||
{
|
||||
label: '4K - 80 Mbps',
|
||||
bitrate: 80000000,
|
||||
codec: undefined,
|
||||
original: false,
|
||||
},
|
||||
{
|
||||
label: '1080p - 40 Mbps',
|
||||
bitrate: 40000000,
|
||||
codec: undefined,
|
||||
original: false,
|
||||
},
|
||||
{
|
||||
label: '1080p - 10 Mbps',
|
||||
bitrate: 10000000,
|
||||
codec: undefined,
|
||||
original: false,
|
||||
},
|
||||
{
|
||||
label: '720p - 8 Mbps',
|
||||
bitrate: 8000000,
|
||||
codec: undefined,
|
||||
original: false,
|
||||
},
|
||||
{
|
||||
label: '720p - 4 Mbps',
|
||||
bitrate: 4000000,
|
||||
codec: undefined,
|
||||
original: false,
|
||||
},
|
||||
{
|
||||
label: '480p - 3 Mbps',
|
||||
bitrate: 3000000,
|
||||
codec: undefined,
|
||||
original: false,
|
||||
},
|
||||
{
|
||||
label: '480p - 720 Kbps',
|
||||
bitrate: 720000,
|
||||
codec: undefined,
|
||||
original: false,
|
||||
},
|
||||
{
|
||||
label: '360p - 420 Kbps',
|
||||
bitrate: 420000,
|
||||
codec: undefined,
|
||||
original: false,
|
||||
},
|
||||
];
|
||||
|
||||
export function getClosestBitrate(qualities, bitrate) {
|
||||
return qualities.reduce(
|
||||
(prev, curr) =>
|
||||
Math.abs(curr.bitrate - bitrate) < Math.abs(prev.bitrate - bitrate)
|
||||
? curr
|
||||
: prev,
|
||||
qualities[0],
|
||||
);
|
||||
}
|
||||
|
||||
export function formatTicksToTime(ticks: number) {
|
||||
return formatMinutesToTime(ticks / 10_000_000 / 60);
|
||||
}
|
||||
|
||||
export function formatMinutesToTime(minutes: number) {
|
||||
const days = Math.floor(minutes / 60 / 24);
|
||||
const hours = Math.floor((minutes / 60) % 24);
|
||||
const minutesLeft = Math.floor(minutes % 60);
|
||||
|
||||
return `${days > 0 ? days + 'd ' : ''}${hours > 0 ? hours + 'h ' : ''}${
|
||||
days > 0 ? '' : minutesLeft + 'min'
|
||||
}`;
|
||||
}
|
||||
31
backend/plugins/plugin-types.d.ts
vendored
31
backend/plugins/plugin-types.d.ts
vendored
@@ -40,6 +40,7 @@ export type Quality = {
|
||||
bitrate: number;
|
||||
label: string;
|
||||
codec: string | undefined;
|
||||
original: boolean;
|
||||
};
|
||||
|
||||
export type Subtitles = {
|
||||
@@ -49,14 +50,26 @@ export type Subtitles = {
|
||||
codec: string | undefined;
|
||||
};
|
||||
|
||||
export type VideoStream = {
|
||||
export type VideoStreamProperty = {
|
||||
label: string;
|
||||
value: string | number;
|
||||
formatted: string | undefined;
|
||||
};
|
||||
|
||||
export type VideoStreamCandidate = {
|
||||
key: string;
|
||||
title: string;
|
||||
properties: VideoStreamProperty[];
|
||||
};
|
||||
|
||||
export type VideoStream = VideoStreamCandidate & {
|
||||
uri: string;
|
||||
directPlay: boolean;
|
||||
progress: number;
|
||||
audioStreams: AudioStream[];
|
||||
audioStreamIndex: number;
|
||||
qualities: Quality[];
|
||||
quality: number;
|
||||
qualityIndex: number;
|
||||
subtitles: Subtitles[];
|
||||
};
|
||||
|
||||
@@ -93,10 +106,24 @@ export interface SourcePlugin {
|
||||
|
||||
getMovieStream: (
|
||||
tmdbId: string,
|
||||
key: string,
|
||||
context: UserContext,
|
||||
config?: PlaybackConfig,
|
||||
) => Promise<VideoStream>;
|
||||
|
||||
getMovieStreams: (
|
||||
tmdbId: string,
|
||||
context: UserContext,
|
||||
config?: PlaybackConfig,
|
||||
) => Promise<VideoStreamCandidate[]>;
|
||||
|
||||
getMovieStream: (
|
||||
tmdbId: string,
|
||||
context: UserContext,
|
||||
key: string,
|
||||
config?: PlaybackConfig,
|
||||
) => Promise<VideoStream>;
|
||||
|
||||
getEpisodeStream: (
|
||||
tmdbId: string,
|
||||
season: number,
|
||||
|
||||
Reference in New Issue
Block a user