docs: document plugin types

This commit is contained in:
Aleksi Lassila
2025-02-11 05:12:40 +02:00
parent 04a1021761
commit 23123d89d5
7 changed files with 112 additions and 16 deletions

View File

@@ -130,6 +130,7 @@ The roadmap includes plans to support the following platforms in the future:
To create the first user account, you can log in with any credentials and an admin account will be created.
Alternatively, you can define the admin username and password using environment variables,
as seen in the Docker Compose example. A new admin account is only created if there are no previous accounts with the same name.
To get access to media playback, connect to plugins by adding media sources in the settings.
Additional plugins can be installed by dropping them in the `/plugins` folder.
To get most out of Reiverr, it is recommended to also connect to TMDB.
@@ -195,9 +196,9 @@ the built-in plugins. Couple of things to note:
- Plugins have to implement (and default export) the `PluginProvider`
class that can be imported from the [](backend/packages/reiverr-plugin)
package.
- To install
[the github npm registry package](https://github.com/aleksilassila/reiverr/pkgs/npm/reiverr-plugin), follow the steps outlined here:
[Working with the npm registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry#installing-a-package)
- To install the
[reiverr-plugin package](https://github.com/aleksilassila/reiverr/pkgs/npm/reiverr-plugin) from Github's npm registry, follow the steps outlined here:
[Working with the Github npm registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-npm-registry#installing-a-package)
(you'll need a .npmrc file)
- The built-in packages use npm workspaces, which doesn't work if
you're developing outisde of the repository
@@ -209,8 +210,6 @@ the built-in plugins. Couple of things to note:
- https://developer.themoviedb.org/reference
- https://api.jellyfin.org/
- https://sonarr.tv/docs/api/
- https://radarr.video/docs/api/
- https://github.com/jellyfin/jellyfin-web
- Network tab in the browser in Jellyfin, Radarr & Sonarr web UIs

View File

@@ -20,13 +20,11 @@ import {
*
* @see SourceProvider
*/
export class PluginProvider {
export abstract class PluginProvider {
/**
* @returns {SourceProvider[]} A list of SourceProvider instances that the plugin provides.
*/
static getPlugins(): SourceProvider[] {
return [];
}
abstract getPlugins(): SourceProvider[];
}
export class SettingsManager {
@@ -41,21 +39,48 @@ export class SettingsManager {
});
}
/**
* SourceProvider is a class that provides a set of methods to interact with a streaming source.
*
* Important distinction between SourceProvider and MediaSource:
* An user doesn't directly add a SourceProvider to their account, but instead users can configure
* `MediaSources`. MediaSource is essentially all the user-specific configuration that SourceProvider
* needs to function. This way different users can have different configurations for the same
* SourceProvider - for example, two users can use the same JellyfinPlugin (JellyfinSourceProvider)
* to access two different Jellyfin servers, because they access the provider with their own
* MediaSource instances.
*
* UserContext is used to pass the user-specific configuration to the SourceProvider methods.
*
* @see UserContext
* @see PluginProvider
*/
export abstract class SourceProvider {
abstract name: string;
settingsManager: SettingsManager = new SettingsManager();
/**
* Returns an index of all movies available in the source.
*/
getMovieCatalogue?: (
context: UserContext,
pagination: PaginationParams,
) => Promise<PaginatedResponse<IndexItem>>;
/**
* Returns an index of all episodes available in the source.
*/
getEpisodeCatalogue?: (
context: UserContext,
pagination: PaginationParams,
) => Promise<PaginatedResponse<IndexItem>>;
/**
* Returns a list of stream candidates for a movie that the user can choose to stream from.
*
* @see StreamCandidate
*/
getMovieStreams?: (
tmdbId: string,
metadata: MovieMetadata,
@@ -63,6 +88,11 @@ export abstract class SourceProvider {
config?: PlaybackConfig,
) => Promise<{ candidates: StreamCandidate[] }>;
/**
* Returns a list of stream candidates for an episode that the user can choose to stream from.
*
* @see StreamCandidate
*/
getEpisodeStreams?: (
tmdbId: string,
metadata: EpisodeMetadata,
@@ -70,6 +100,11 @@ export abstract class SourceProvider {
config?: PlaybackConfig,
) => Promise<{ candidates: StreamCandidate[] }>;
/**
* Returns a specific stream for a movie that the user can stream from.
*
* @see Stream
*/
getMovieStream?: (
tmdbId: string,
metadata: MovieMetadata,
@@ -78,6 +113,11 @@ export abstract class SourceProvider {
config?: PlaybackConfig,
) => Promise<Stream | undefined>;
/**
* Returns a specific stream for an episode that the user can stream from.
*
* @see Stream
*/
getEpisodeStream?: (
tmdbId: string,
metadata: EpisodeMetadata,
@@ -86,6 +126,14 @@ export abstract class SourceProvider {
config?: PlaybackConfig,
) => Promise<Stream | undefined>;
/**
* This method will be called when the client makes a request to the provider's
* proxy endpoint (e.g. /api/proxy/:providerName/:path). This can be used to
* relay video streams and subtitles to the client, by making a request to an
* external service and then returning the response to the client. Ideally,
* the stream url pointed to by a `Stream` object should use the proxy endpoint
* so that the plugin can handle the video requests here.
*/
proxyHandler?: (
req: any,
res: any,

View File

@@ -21,13 +21,35 @@ export type SourceProviderSettingsTemplate = Record<
SourceProviderSettingsLink | SourceProviderSettingsInput
>;
/**
* UserContext is used to pass the user-specific configuration to the SourceProvider methods.
*/
export type UserContext = {
/**
* An id unique to each Reiverr user
*/
userId: string;
/**
* The access token of the user that can be used to authenticate requests to the backend
* (e.g. proxy requests)
*/
token: string;
/**
* The id of the MediaSource instance that the user is using to access the SourceProvider
*/
sourceId: string;
/**
* @see SourceProviderSettings
*/
settings: SourceProviderSettings;
};
/**
* The settings/configuration defined in the `SourceProvider` and
* provided by the user's MediaSource instance
*/
export type SourceProviderSettings = Record<string, any>;
export type ValidationResponse = {
@@ -59,14 +81,44 @@ export type Subtitles = {
};
export type StreamProperty = {
/**
* The label of the property
* @example "Resolution"
*/
label: string;
/**
* Used for sorting and filtering, or displayed if `formatted` is not provided.
* @example 1080
*/
value: string | number;
/**
* The formatted value of the property
* @example "1080p"
*/
formatted: string | undefined;
};
/**
* `StreamCandidate` represents a stream that can be played by the user,
* and contains all the information that is presented to the user in the
* stream selection UI.
*/
export type StreamCandidate = {
/**
* Unique id for the stream, that can be used to later stream the specific stream.
*/
key: string;
/**
* Title of the stream, presented to the user.
*/
title: string;
/**
* A list of properties that are shown to the user in the stream selection UI.
*/
properties: StreamProperty[];
};

View File

@@ -31,7 +31,7 @@ import {
} from './utils';
export default class JellyfinPluginProvider extends PluginProvider {
static getPlugins(): SourceProvider[] {
getPlugins(): SourceProvider[] {
return [new JellyfinProvider()];
}
}

View File

@@ -26,7 +26,7 @@ import {
} from './utils';
export default class TorrentPluginsProvider extends PluginProvider {
static getPlugins(): SourceProvider[] {
getPlugins(): SourceProvider[] {
return [new TorrentProvider()];
}
}

View File

@@ -57,10 +57,7 @@ type MediaSourceConnection = {
@Injectable()
export class ServiceOwnershipValidator implements CanActivate {
constructor(
private mediaSourcesService: MediaSourcesService,
private sourceProvidersService: SourceProvidersService,
) {}
constructor(private mediaSourcesService: MediaSourcesService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();

View File

@@ -42,7 +42,7 @@ export class SourceProvidersService {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const pluginModule = require(pluginPath);
const provider: typeof PluginProvider = pluginModule.default;
const provider: PluginProvider = new pluginModule.default();
provider.getPlugins().forEach((plugin) => {
plugins[plugin.name] = plugin;
});