feat: add proto file for grpc api in shared/ with generated types

This commit is contained in:
Aleksi Lassila
2026-01-31 23:28:43 +02:00
parent 35675e2544
commit 32cc4cf89b
24 changed files with 1211 additions and 101 deletions

View File

@@ -9,7 +9,7 @@ import {
import {
PaginatedResponse,
PaginationParams,
} from '@aleksilassila/reiverr-shared';
} from '@aleksilassila/reiverr-shared/dist/src/old';
export const PickAndPartial = <T, K extends keyof T>(
clazz: Type<T>,

View File

@@ -6,7 +6,7 @@ import {
Subtitles,
AudioTrack,
VideoOptions,
} from '@aleksilassila/reiverr-shared';
} from '@aleksilassila/reiverr-shared/dist/src/old';
import { ApiProperty } from '@nestjs/swagger';
/*

View File

@@ -7,7 +7,7 @@ import {
MediaPluginSettings,
MediaPluginSettingsResponseDto,
mediaPluginVersion,
} from '@aleksilassila/reiverr-shared';
} from '@aleksilassila/reiverr-shared/dist/src/old';
import { Inject, Injectable, Logger } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { firstValueFrom, lastValueFrom } from 'rxjs';

View File

@@ -7,7 +7,7 @@ import {
ProfileCondition,
SubtitleProfile,
TranscodingProfile,
} from '@aleksilassila/reiverr-shared';
} from '@aleksilassila/reiverr-shared/dist/src/old';
export class DirectPlayProfileDto implements DirectPlayProfile {
@ApiProperty({

View File

@@ -17,7 +17,7 @@ import {
StreamResponse,
Subtitles,
ValidationResponse,
} from '@aleksilassila/reiverr-shared';
} from '@aleksilassila/reiverr-shared/dist/src/old';
import {
ApiProperty,
ApiPropertyOptional,

View File

@@ -1,4 +1,4 @@
import { ReiverrPlugin } from '@aleksilassila/reiverr-shared';
import { ReiverrPlugin } from '@aleksilassila/reiverr-shared/dist/src/old';
import {
Body,
Controller,

View File

@@ -1,7 +1,7 @@
import {
getReiverrPluginVersion,
ReiverrPlugin,
} from '@aleksilassila/reiverr-shared';
} from '@aleksilassila/reiverr-shared/dist/src/old';
import { Injectable, Logger } from '@nestjs/common';
import * as fs from 'fs';
import * as path from 'path';

View File

@@ -1,4 +1,4 @@
import { type ViewBase } from '@aleksilassila/reiverr-shared';
import { type ViewBase } from '@aleksilassila/reiverr-shared/dist/src/old';
import { ApiProperty } from '@nestjs/swagger';
enum ViewType {

View File

@@ -30,7 +30,7 @@ import {
SortableProperty,
StreamActionElement,
MediaSourceProvider,
} from '@aleksilassila/reiverr-shared';
} from '@aleksilassila/reiverr-shared/dist/src/old';
import { ViewBaseDto } from 'src/source-providers/ui.dto';
class CatalogueOrderDirectionOption implements DirectionOption {

View File

@@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { SourceProviderSettings } from '@aleksilassila/reiverr-shared';
import { SourceProviderSettings } from '@aleksilassila/reiverr-shared/dist/src/old';
import { User } from 'src/users/user.entity';
import {
Column,

View File

@@ -1,4 +1,4 @@
import { SourceProviderError } from '@aleksilassila/reiverr-shared';
import { SourceProviderError } from '@aleksilassila/reiverr-shared/dist/src/old';
import {
All,
BadRequestException,
@@ -59,9 +59,8 @@ export class ServiceOwnershipValidator implements CanActivate {
if (!sourceId) return true;
const mediaSource = await this.mediaSourcesService.findMediaSource(
sourceId,
);
const mediaSource =
await this.mediaSourcesService.findMediaSource(sourceId);
if (!mediaSource) throw new NotFoundException('Source not found');
@@ -110,9 +109,8 @@ export class MediaSourcesController {
const providers = await Promise.all(
user.mediaSources.map(async (ms) => {
const mediaSourceDto = await this.mediaSourcesService.getMediaSourceDto(
ms,
);
const mediaSourceDto =
await this.mediaSourcesService.getMediaSourceDto(ms);
const connection = await this.getConnection({
sourceId: ms.id,
@@ -425,9 +423,8 @@ export class MediaSourcesController {
@GetAuthToken() token: string,
) {
const sourceId = params.sourceId;
const mediaSource = await this.mediaSourcesService.findMediaSource(
sourceId,
);
const mediaSource =
await this.mediaSourcesService.findMediaSource(sourceId);
if (!mediaSource) throw new NotFoundException('Source not found');

View File

@@ -2,7 +2,7 @@ import {
CatalogueProvider,
MediaSourceProvider,
ValidationResponse,
} from '@aleksilassila/reiverr-shared';
} from '@aleksilassila/reiverr-shared/dist/src/old';
import { Inject, Injectable } from '@nestjs/common';
import { SourceProvidersService } from 'src/source-providers/source-providers.service';
import { User } from 'src/users/user.entity';

View File

@@ -6,7 +6,7 @@ import {
OrderOption,
PaginatedResponse,
PaginationParams,
} from '@aleksilassila/reiverr-shared';
} from '@aleksilassila/reiverr-shared/dist/src/old';
import {
ItemSortBy,
BaseItemKind,

View File

@@ -5,7 +5,7 @@ import {
SourceProviderSettings,
SourceProviderSettingsTemplate,
ValidationResponse,
} from '@aleksilassila/reiverr-shared';
} from '@aleksilassila/reiverr-shared/dist/src/old';
import { JellyfinMediaSourceProvider } from './media-source-provider';
import { JellyfinCatalogueProvider } from './catalogue-provider';

View File

@@ -12,24 +12,24 @@ import {
UserContext,
MediaSourceView,
MediaSourceViews,
} from "@aleksilassila/reiverr-shared";
import { Readable } from "stream";
} from '@aleksilassila/reiverr-shared/dist/src/old';
import { Readable } from 'stream';
import {
BaseItemKind,
ItemFields,
Api as JellyfinApi,
} from "./jellyfin.openapi";
} from './jellyfin.openapi';
import {
bitrateQualities,
formatSize,
formatTicksToTime,
getClosestBitrate,
JELLYFIN_DEVICE_ID,
} from "./utils";
} from './utils';
enum View {
StreamMovie = "stream-movie",
StreamEpisode = "stream-episode",
StreamMovie = 'stream-movie',
StreamEpisode = 'stream-episode',
}
export interface JellyfinSettings extends SourceProviderSettings {
@@ -72,8 +72,8 @@ export class JellyfinMediaSourceProvider extends MediaSourceProvider {
views: [
{
id: View.StreamMovie,
label: "Stream",
type: "list-with-details",
label: 'Stream',
type: 'list-with-details',
},
],
};
@@ -82,8 +82,8 @@ export class JellyfinMediaSourceProvider extends MediaSourceProvider {
views: [
{
id: View.StreamEpisode,
label: "Stream",
type: "list-with-details",
label: 'Stream',
type: 'list-with-details',
},
],
};
@@ -110,16 +110,16 @@ export class JellyfinMediaSourceProvider extends MediaSourceProvider {
if (id === View.StreamMovie && tmdbMovie) {
const candidates = await this.getTmdbMovieCandidates({ tmdbMovie });
view = {
type: "list-with-details",
type: 'list-with-details',
id,
label: "Stream",
label: 'Stream',
items: candidates.candidates.map((c) => ({
...c,
id: c.streamId,
label: c.title,
actions: c.actions.map((a) => ({
label: a.label,
type: "action",
type: 'action',
action: a.type,
})),
})),
@@ -132,16 +132,16 @@ export class JellyfinMediaSourceProvider extends MediaSourceProvider {
});
view = {
type: "list-with-details",
type: 'list-with-details',
id,
label: "Stream",
label: 'Stream',
items: candidates.candidates.map((c) => ({
...c,
id: c.streamId,
label: c.title,
actions: c.actions.map((a) => ({
label: a.label,
type: "action",
type: 'action',
action: a.type,
})),
})),
@@ -170,7 +170,7 @@ export class JellyfinMediaSourceProvider extends MediaSourceProvider {
});
const movie = movies.data.Items.find(
(i) => i.ProviderIds?.Tmdb === String(tmdbMovie.id)
(i) => i.ProviderIds?.Tmdb === String(tmdbMovie.id),
);
if (!movie || !movie.MediaSources || movie.MediaSources.length === 0) {
@@ -182,36 +182,36 @@ export class JellyfinMediaSourceProvider extends MediaSourceProvider {
{
id: movie.ProviderIds?.Tmdb,
tmdbId: movie.ProviderIds?.Tmdb,
mediaType: "movie" as const,
mediaType: 'movie' as const,
streamId: movie.Id,
title: movie.Name,
actions: [
{
label: "Stream",
type: "stream",
label: 'Stream',
type: 'stream',
},
],
properties: [
{
label: "Video",
label: 'Video',
value: movie.MediaSources[0].Bitrate || 0,
formatted:
movie.MediaSources[0].MediaStreams.find(
(s) => s.Type === "Video"
)?.DisplayTitle || "Unknown",
(s) => s.Type === 'Video',
)?.DisplayTitle || 'Unknown',
},
{
label: "Size",
label: 'Size',
value: movie.MediaSources[0].Size,
formatted: formatSize(movie.MediaSources[0].Size),
},
{
label: "Filename",
label: 'Filename',
value: movie.MediaSources[0].Name,
formatted: undefined,
},
{
label: "Runtime",
label: 'Runtime',
value: movie.MediaSources[0].RunTimeTicks,
formatted: formatTicksToTime(movie.MediaSources[0].RunTimeTicks),
},
@@ -251,14 +251,14 @@ export class JellyfinMediaSourceProvider extends MediaSourceProvider {
});
const show = series.data.Items.find(
(i) => i.ProviderIds?.Tmdb === String(tmdbSeries.id)
(i) => i.ProviderIds?.Tmdb === String(tmdbSeries.id),
);
if (!show) {
console.error(
"series not found",
'series not found',
series.data?.Items?.map((i) => i.ProviderIds),
tmdbSeries
tmdbSeries,
);
throw SourceProviderError.StreamNotFound;
}
@@ -281,7 +281,7 @@ export class JellyfinMediaSourceProvider extends MediaSourceProvider {
(e) =>
e.SeriesId === show.Id &&
e.ParentIndexNumber === tmdbEpisode.season_number &&
e.IndexNumber === tmdbEpisode.episode_number
e.IndexNumber === tmdbEpisode.episode_number,
);
if (
@@ -289,7 +289,7 @@ export class JellyfinMediaSourceProvider extends MediaSourceProvider {
!episode.MediaSources ||
episode.MediaSources.length === 0
) {
console.error("episode not found", episode, episodes.data.Items.length);
console.error('episode not found', episode, episodes.data.Items.length);
throw SourceProviderError.StreamNotFound;
}
@@ -298,39 +298,39 @@ export class JellyfinMediaSourceProvider extends MediaSourceProvider {
{
id: episode.ProviderIds?.Tmdb,
tmdbId: episode.ProviderIds?.Tmdb,
mediaType: "episode" as const,
mediaType: 'episode' as const,
streamId: episode.Id,
title: episode.Name,
actions: [
{
label: "Stream",
type: "stream",
label: 'Stream',
type: 'stream',
},
],
properties: [
{
label: "Video",
label: 'Video',
value: episode.MediaSources[0].Bitrate || 0,
formatted:
episode.MediaSources[0].MediaStreams.find(
(s) => s.Type === "Video"
)?.DisplayTitle || "Unknown",
(s) => s.Type === 'Video',
)?.DisplayTitle || 'Unknown',
},
{
label: "Size",
label: 'Size',
value: episode.MediaSources[0].Size,
formatted: formatSize(episode.MediaSources[0].Size),
},
{
label: "Filename",
label: 'Filename',
value: episode.MediaSources[0].Name,
formatted: undefined,
},
{
label: "Runtime",
label: 'Runtime',
value: episode.MediaSources[0].RunTimeTicks,
formatted: formatTicksToTime(
episode.MediaSources[0].RunTimeTicks
episode.MediaSources[0].RunTimeTicks,
),
},
],
@@ -376,7 +376,7 @@ export class JellyfinMediaSourceProvider extends MediaSourceProvider {
return {
error: {
message: "Action not supported",
message: 'Action not supported',
},
};
};
@@ -445,7 +445,7 @@ export class JellyfinMediaSourceProvider extends MediaSourceProvider {
// deviceId: JELLYFIN_DEVICE_ID,
// mediaSourceId: movie.MediaSources[0].Id,
// maxBitrate: 8000000,
}
},
);
const mediasSource = playbackInfo.data?.MediaSources?.[0];
@@ -456,19 +456,19 @@ export class JellyfinMediaSourceProvider extends MediaSourceProvider {
`/Videos/${mediasSource?.Id}/stream.mp4?Static=true&mediaSourceId=${mediasSource?.Id}&deviceId=${JELLYFIN_DEVICE_ID}&api_key=${this.settings.apiKey}&Tag=${mediasSource?.ETag}`) +
`&reiverr_token=${this.token}`;
const audioStreams: Stream["audioStreams"] =
mediasSource?.MediaStreams.filter((s) => s.Type === "Audio").map((s) => ({
const audioStreams: Stream['audioStreams'] =
mediasSource?.MediaStreams.filter((s) => s.Type === 'Audio').map((s) => ({
bitrate: s.BitRate,
label: s.Language,
codec: s.Codec,
index: s.Index,
})) ?? [];
const qualities: Stream["qualities"] = [
const qualities: Stream['qualities'] = [
...bitrateQualities,
{
bitrate: mediasSource.Bitrate,
label: "Original",
label: 'Original',
codec: undefined,
original: true,
},
@@ -480,37 +480,37 @@ export class JellyfinMediaSourceProvider extends MediaSourceProvider {
const bitrate = Math.min(maxStreamingBitrate, mediasSource.Bitrate);
const subtitles: Subtitles[] = mediasSource.MediaStreams.filter(
(s) => s.Type === "Subtitle" && s.DeliveryUrl
(s) => s.Type === 'Subtitle' && s.DeliveryUrl,
).map((s, i) => ({
src: this.getProxyUrl() + `${s.DeliveryUrl}&reiverr_token=${this.token}`,
lang: s.Language,
kind: "subtitles",
kind: 'subtitles',
label: s.DisplayTitle,
}));
const stream = {
streamId: "0",
streamId: '0',
title: movie.Name,
properties: [
{
label: "Video",
label: 'Video',
value: mediasSource.Bitrate || 0,
formatted:
mediasSource.MediaStreams.find((s) => s.Type === "Video")
?.DisplayTitle || "Unknown",
mediasSource.MediaStreams.find((s) => s.Type === 'Video')
?.DisplayTitle || 'Unknown',
},
{
label: "Size",
label: 'Size',
value: mediasSource.Size,
formatted: formatSize(mediasSource.Size),
},
{
label: "Filename",
label: 'Filename',
value: mediasSource.Name,
formatted: undefined,
},
{
label: "Runtime",
label: 'Runtime',
value: mediasSource.RunTimeTicks,
formatted: formatTicksToTime(mediasSource.RunTimeTicks),
},
@@ -554,19 +554,19 @@ export class JellyfinMediaSourceProvider extends MediaSourceProvider {
const headers = {};
for (const key in req.headers) {
if (key === "host") continue;
if (key === 'host') continue;
headers[key] = req.headers[key];
}
const proxyRes = await fetch(url, {
method: req.method || "GET",
method: req.method || 'GET',
headers: {
...headers,
Authorization: `MediaBrowser DeviceId="${JELLYFIN_DEVICE_ID}", Token="${this.settings.apiKey}"`,
},
}).catch((e) => {
console.error("error fetching proxy response", e);
res.status(500).send("Error fetching proxy response");
console.error('error fetching proxy response', e);
res.status(500).send('Error fetching proxy response');
});
if (!proxyRes) return;

View File

@@ -5,12 +5,14 @@
"types": "./dist/src/index.d.ts",
"private": true,
"scripts": {
"build": "tsc",
"build": "protoc --plugin=..\\node_modules\\.bin\\protoc-gen-ts_proto --ts_proto_out=.\\src --ts_proto_opt=outputEncodeMethods=false,outputJsonMethods=false,outputClientImpl=false --proto_path=. *.proto&& tsc",
"clean": "rm -rf dist"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@protobuf-ts/protoc": "^2.11.1",
"ts-proto": "^2.11.1",
"typescript": "^5.2.2"
},
"description": "Shared types and utilities for Reiverr",

540
shared/reiverr-plugin.proto Normal file
View File

@@ -0,0 +1,540 @@
syntax = "proto3";
package aleksilassila.reiverr.plugin.v1;
// Plugin Service - handles plugin metadata and configuration
service PluginService {
// Get plugin metadata and version
rpc GetInfo(Empty) returns (PluginInfo);
}
// Media Source Provider Service - handles user-specific requests
service MediaSourceProviderService {
// Get available views for a media item
rpc GetMediaSourceViews(MediaSourceViewsRequest) returns (MediaSourceViewsResponse);
// Get a specific view
rpc GetMediaSourceView(MediaSourceViewRequest) returns (MediaSourceViewResponse);
// Get autoplay stream
rpc GetAutoplayStream(AutoplayStreamRequest) returns (AutoplayStreamResponse);
// Get stream details
rpc GetStream(GetStreamRequest) returns (StreamResponse);
// Handle stream actions (download, delete, etc.)
rpc HandleAction(HandleActionRequest) returns (ActionResponse);
// Proxy handler for streaming (bidirectional for video/subtitle streaming)
rpc ProxyStream(stream ProxyRequest) returns (stream ProxyResponse);
// Legacy methods (deprecated)
rpc GetTmdbMovieCandidates(TmdbMovieRequest) returns (StreamCandidatesResponse);
rpc GetTmdbEpisodeCandidates(TmdbEpisodeRequest) returns (StreamCandidatesResponse);
}
// Catalogue Provider Service - handles library catalogues
service CatalogueProviderService {
// Get catalogue capabilities
rpc GetCatalogueCapabilities(Empty) returns (CatalogueCapabilities);
// Get combined catalogue
rpc GetCatalogue(CatalogueRequest) returns (CatalogueResponse);
// Get movies catalogue
rpc GetMovieCatalogue(CatalogueRequest) returns (CatalogueResponse);
// Get series catalogue
rpc GetSeriesCatalogue(CatalogueRequest) returns (CatalogueResponse);
// Get missing items in catalogue
rpc GetMissingInCatalogue(MissingCatalogueRequest) returns (MissingCatalogueResponse);
}
// Management Profile Service - NEW for monitoring/management profiles
service ManagementProfileService {
// Get available management profiles
rpc GetManagementProfiles(Empty) returns (ManagementProfilesResponse);
// Get management profile for specific media
rpc GetMediaManagementProfile(MediaManagementProfileRequest) returns (ManagementProfile);
// Update management profile for media
rpc UpdateMediaManagementProfile(UpdateManagementProfileRequest) returns (ManagementProfile);
}
// Job Management Service - NEW for monitoring downloads, transcoding, etc.
service JobManagementService {
// Get all active jobs
rpc GetActiveJobs(Empty) returns (JobsResponse);
// Get job details
rpc GetJobDetails(JobDetailsRequest) returns (JobDetails);
// Cancel a job
rpc CancelJob(CancelJobRequest) returns (ActionResponse);
// Get disk space usage
rpc GetDiskSpaceUsage(Empty) returns (DiskSpaceUsage);
// Get cleanup history
rpc GetCleanupHistory(CleanupHistoryRequest) returns (CleanupHistoryResponse);
// Trigger cleanup
rpc TriggerCleanup(TriggerCleanupRequest) returns (ActionResponse);
}
// Common Messages
message Empty {}
message PluginInfo {
string name = 1;
string version = 2;
string description = 3;
bool streaming_supported = 4;
bool catalogue_supported = 5;
}
message SettingsTemplate {
map<string, SettingField> fields = 1;
}
message SettingField {
string type = 1; // "string", "number", "boolean", "password", "link"
string label = 2;
string placeholder = 3;
bool required = 4;
// For link type
string url = 5;
}
message ValidateSettingsRequest {
map<string, string> settings = 1;
}
message ValidationResponse {
bool is_valid = 1;
map<string, string> errors = 2;
map<string, string> validated_settings = 3;
}
// User Context - passed with most requests
message UserContext {
string user_id = 1;
string token = 2;
string source_id = 3;
map<string, string> settings = 4;
}
// Playable Context
message PlayableContext {
optional string tmdb_movie_json = 1;
optional string tmdb_series_json = 2;
optional string tmdb_episode_json = 3;
}
// Media Source Views
message MediaSourceViewsRequest {
UserContext user_context = 1;
PlayableContext playable_context = 2;
}
message MediaSourceViewsResponse {
repeated MediaSourceView views = 1;
}
message MediaSourceView {
string id = 1;
string title = 2;
repeated StreamProperty properties = 3;
repeated StreamAction actions = 4;
}
message MediaSourceViewRequest {
UserContext user_context = 1;
PlayableContext playable_context = 2;
string view_id = 3;
}
message MediaSourceViewResponse {
optional MediaSourceView view = 1;
}
// Autoplay Stream
message AutoplayStreamRequest {
UserContext user_context = 1;
PlayableContext playable_context = 2;
}
message AutoplayStreamResponse {
optional StreamBase candidate = 1;
}
// Stream Messages
message GetStreamRequest {
UserContext user_context = 1;
string stream_id = 2;
optional PlaybackConfig config = 3;
}
message StreamResponse {
optional Stream stream = 1;
optional Toast toast = 2;
optional ErrorMessage error = 3;
}
message Stream {
string stream_id = 1;
string title = 2;
repeated StreamProperty properties = 3;
string src = 4;
bool direct_play = 5;
double progress = 6;
double duration = 7;
repeated AudioStream audio_streams = 8;
int32 audio_stream_index = 9;
repeated Quality qualities = 10;
int32 quality_index = 11;
repeated Subtitle subtitles = 12;
}
message StreamBase {
string stream_id = 1;
string title = 2;
repeated StreamProperty properties = 3;
}
message StreamProperty {
string label = 1;
string value = 2;
optional string formatted = 3;
}
message AudioStream {
int32 index = 1;
string label = 2;
optional string codec = 3;
optional int32 bitrate = 4;
}
message Quality {
int32 index = 1;
int32 bitrate = 2;
string label = 3;
optional string codec = 4;
bool original = 5;
}
message Subtitle {
string src = 1;
string lang = 2;
string kind = 3; // "subtitles", "captions", "descriptions"
string label = 4;
}
message PlaybackConfig {
optional int32 bitrate = 1;
optional int32 audio_stream_index = 2;
optional double progress = 3;
optional string device_profile_json = 4;
optional string default_language = 5;
}
// Actions
message StreamAction {
string label = 1;
string type = 2;
}
message HandleActionRequest {
UserContext user_context = 1;
string target_id = 2;
string action = 3;
}
message ActionResponse {
optional Toast toast = 1;
optional ErrorMessage error = 2;
optional ActionResult result = 3;
}
message ActionResult {
bool success = 1;
optional string message = 2;
}
message Toast {
string title = 1;
string message = 2;
string type = 3; // "info", "success", "error"
}
message ErrorMessage {
string message = 1;
}
// Proxy Streaming
message ProxyRequest {
UserContext user_context = 1;
string uri = 2;
optional string target_url = 3;
map<string, string> headers = 4;
bytes body = 5;
}
message ProxyResponse {
int32 status_code = 1;
map<string, string> headers = 2;
bytes chunk = 3;
bool is_final = 4;
}
// Legacy Stream Candidates
message TmdbMovieRequest {
UserContext user_context = 1;
string tmdb_movie_json = 2;
}
message TmdbEpisodeRequest {
UserContext user_context = 1;
string tmdb_series_json = 2;
string tmdb_episode_json = 3;
}
message StreamCandidatesResponse {
repeated StreamCandidate candidates = 1;
}
message StreamCandidate {
string stream_id = 1;
string title = 2;
repeated StreamProperty properties = 3;
repeated StreamAction actions = 4;
}
// Catalogue Messages
message CatalogueCapabilities {
CatalogueCapability movies_catalogue = 1;
CatalogueCapability series_catalogue = 2;
CatalogueCapability combined_catalogue = 3;
CatalogueCapability missing_catalogue = 4;
}
message CatalogueCapability {
bool is_supported = 1;
repeated OrderOption order_options = 2;
}
message OrderOption {
string label = 1;
string value = 2;
repeated DirectionOption directions = 3;
}
message DirectionOption {
string label = 1;
string value = 2;
}
message CatalogueRequest {
UserContext user_context = 1;
PaginationParams pagination = 2;
optional string order = 3;
optional string direction = 4;
}
message CatalogueResponse {
repeated CatalogueItem items = 1;
int32 total = 2;
int32 page = 3;
int32 items_per_page = 4;
}
message CatalogueItem {
string tmdb_id = 1;
string media_type = 2; // "movie" or "series"
}
message PaginationParams {
int32 page = 1;
int32 items_per_page = 2;
}
message MissingCatalogueRequest {
UserContext user_context = 1;
PaginationParams pagination = 2;
optional string order = 3;
optional string direction = 4;
map<string, string> my_list_items_json = 5;
}
message MissingCatalogueResponse {
repeated string items_json = 1;
int32 total = 2;
int32 page = 3;
int32 items_per_page = 4;
}
// Management Profile Messages (NEW)
message ManagementProfilesResponse {
repeated ManagementProfile profiles = 1;
}
message ManagementProfile {
string id = 1;
string name = 2;
string description = 3;
AutoRequestOptions auto_requests = 4;
AutoRemoveOptions auto_remove = 5;
}
message AutoRequestOptions {
// Episodes: Next up + x episodes (0-n)
optional int32 next_episodes_count = 1;
// Episodes: Current season + x next seasons
optional int32 next_seasons_count = 2;
// Watch RSS feed
bool watch_rss = 3;
}
message AutoRemoveOptions {
// After watched + x days
optional int32 days_after_watched = 1;
// After downloaded + x days
optional int32 days_after_downloaded = 2;
// When seeding ratio > x
optional double min_seeding_ratio = 3;
// Lazy deletion (delete when disk space needed)
bool lazy_deletion = 4;
// Priority for lazy deletion
optional int32 deletion_priority = 5;
}
message MediaManagementProfileRequest {
UserContext user_context = 1;
string tmdb_id = 2;
string media_type = 3; // "movie" or "series"
}
message UpdateManagementProfileRequest {
UserContext user_context = 1;
string tmdb_id = 2;
string media_type = 3;
string profile_id = 4;
}
// Job Management Messages (NEW)
message JobsResponse {
repeated Job jobs = 1;
}
message Job {
string id = 1;
string type = 2; // "download", "transcode", "seed"
string status = 3; // "pending", "active", "completed", "failed", "cancelled"
string tmdb_id = 4;
string media_type = 5;
optional int32 season = 6;
optional int32 episode = 7;
double progress = 8;
optional string eta = 9;
map<string, string> metadata = 10;
}
message JobDetailsRequest {
UserContext user_context = 1;
string job_id = 2;
}
message JobDetails {
Job job = 1;
repeated JobLogEntry logs = 2;
JobStats stats = 3;
}
message JobLogEntry {
string timestamp = 1;
string level = 2; // "info", "warning", "error"
string message = 3;
}
message JobStats {
optional double download_speed = 1;
optional double upload_speed = 2;
optional int32 seeders = 3;
optional int32 peers = 4;
optional double seeding_ratio = 5;
optional int64 size_bytes = 6;
optional int64 downloaded_bytes = 7;
}
message CancelJobRequest {
UserContext user_context = 1;
string job_id = 2;
}
message DiskSpaceUsage {
int64 total_bytes = 1;
int64 used_bytes = 2;
int64 available_bytes = 3;
repeated MediaFileInfo media_files = 4;
}
message MediaFileInfo {
string id = 1;
string tmdb_id = 2;
string media_type = 3;
optional int32 season = 4;
optional int32 episode = 5;
int64 size_bytes = 6;
string file_path = 7;
bool watched = 8;
optional double seeding_ratio = 9;
optional string added_date = 10;
optional string watched_date = 11;
bool marked_for_deletion = 12;
}
message CleanupHistoryRequest {
UserContext user_context = 1;
PaginationParams pagination = 2;
}
message CleanupHistoryResponse {
repeated CleanupHistoryEntry entries = 1;
int32 total = 2;
int32 page = 3;
int32 items_per_page = 4;
}
message CleanupHistoryEntry {
string timestamp = 1;
string tmdb_id = 2;
string media_type = 3;
optional int32 season = 4;
optional int32 episode = 5;
int64 size_bytes = 6;
string reason = 7; // "watched_timeout", "download_timeout", "seeding_complete", "disk_space_needed", "manual"
}
message TriggerCleanupRequest {
UserContext user_context = 1;
optional int64 target_free_space_bytes = 2;
}

View File

@@ -1,7 +1,2 @@
export * from './catalogue';
export * from './common';
export * from './dtos';
export * from './permissions';
export * from './settings';
export * from './video';
export * from './plugin';
export * as old from './old';
export * from './reiverr-plugin';

7
shared/src/old.ts Normal file
View File

@@ -0,0 +1,7 @@
export * from './catalogue';
export * from './common';
export * from './dtos';
export * from './permissions';
export * from './settings';
export * from './video';
export * from './plugin';

View File

@@ -0,0 +1,568 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions:
// protoc-gen-ts_proto v2.11.1
// protoc v6.33.5
// source: reiverr-plugin.proto
/* eslint-disable */
import { Observable } from "rxjs";
export const protobufPackage = "aleksilassila.reiverr.plugin.v1";
export interface Empty {
}
export interface PluginInfo {
name: string;
version: string;
description: string;
streamingSupported: boolean;
catalogueSupported: boolean;
}
export interface SettingsTemplate {
fields: { [key: string]: SettingField };
}
export interface SettingsTemplate_FieldsEntry {
key: string;
value: SettingField | undefined;
}
export interface SettingField {
/** "string", "number", "boolean", "password", "link" */
type: string;
label: string;
placeholder: string;
required: boolean;
/** For link type */
url: string;
}
export interface ValidateSettingsRequest {
settings: { [key: string]: string };
}
export interface ValidateSettingsRequest_SettingsEntry {
key: string;
value: string;
}
export interface ValidationResponse {
isValid: boolean;
errors: { [key: string]: string };
validatedSettings: { [key: string]: string };
}
export interface ValidationResponse_ErrorsEntry {
key: string;
value: string;
}
export interface ValidationResponse_ValidatedSettingsEntry {
key: string;
value: string;
}
/** User Context - passed with most requests */
export interface UserContext {
userId: string;
token: string;
sourceId: string;
settings: { [key: string]: string };
}
export interface UserContext_SettingsEntry {
key: string;
value: string;
}
/** Playable Context */
export interface PlayableContext {
tmdbMovieJson?: string | undefined;
tmdbSeriesJson?: string | undefined;
tmdbEpisodeJson?: string | undefined;
}
export interface MediaSourceViewsRequest {
userContext: UserContext | undefined;
playableContext: PlayableContext | undefined;
}
export interface MediaSourceViewsResponse {
views: MediaSourceView[];
}
export interface MediaSourceView {
id: string;
title: string;
properties: StreamProperty[];
actions: StreamAction[];
}
export interface MediaSourceViewRequest {
userContext: UserContext | undefined;
playableContext: PlayableContext | undefined;
viewId: string;
}
export interface MediaSourceViewResponse {
view?: MediaSourceView | undefined;
}
export interface AutoplayStreamRequest {
userContext: UserContext | undefined;
playableContext: PlayableContext | undefined;
}
export interface AutoplayStreamResponse {
candidate?: StreamBase | undefined;
}
export interface GetStreamRequest {
userContext: UserContext | undefined;
streamId: string;
config?: PlaybackConfig | undefined;
}
export interface StreamResponse {
stream?: Stream | undefined;
toast?: Toast | undefined;
error?: ErrorMessage | undefined;
}
export interface Stream {
streamId: string;
title: string;
properties: StreamProperty[];
src: string;
directPlay: boolean;
progress: number;
duration: number;
audioStreams: AudioStream[];
audioStreamIndex: number;
qualities: Quality[];
qualityIndex: number;
subtitles: Subtitle[];
}
export interface StreamBase {
streamId: string;
title: string;
properties: StreamProperty[];
}
export interface StreamProperty {
label: string;
value: string;
formatted?: string | undefined;
}
export interface AudioStream {
index: number;
label: string;
codec?: string | undefined;
bitrate?: number | undefined;
}
export interface Quality {
index: number;
bitrate: number;
label: string;
codec?: string | undefined;
original: boolean;
}
export interface Subtitle {
src: string;
lang: string;
/** "subtitles", "captions", "descriptions" */
kind: string;
label: string;
}
export interface PlaybackConfig {
bitrate?: number | undefined;
audioStreamIndex?: number | undefined;
progress?: number | undefined;
deviceProfileJson?: string | undefined;
defaultLanguage?: string | undefined;
}
export interface StreamAction {
label: string;
type: string;
}
export interface HandleActionRequest {
userContext: UserContext | undefined;
targetId: string;
action: string;
}
export interface ActionResponse {
toast?: Toast | undefined;
error?: ErrorMessage | undefined;
result?: ActionResult | undefined;
}
export interface ActionResult {
success: boolean;
message?: string | undefined;
}
export interface Toast {
title: string;
message: string;
/** "info", "success", "error" */
type: string;
}
export interface ErrorMessage {
message: string;
}
export interface ProxyRequest {
userContext: UserContext | undefined;
uri: string;
targetUrl?: string | undefined;
headers: { [key: string]: string };
body: Uint8Array;
}
export interface ProxyRequest_HeadersEntry {
key: string;
value: string;
}
export interface ProxyResponse {
statusCode: number;
headers: { [key: string]: string };
chunk: Uint8Array;
isFinal: boolean;
}
export interface ProxyResponse_HeadersEntry {
key: string;
value: string;
}
export interface TmdbMovieRequest {
userContext: UserContext | undefined;
tmdbMovieJson: string;
}
export interface TmdbEpisodeRequest {
userContext: UserContext | undefined;
tmdbSeriesJson: string;
tmdbEpisodeJson: string;
}
export interface StreamCandidatesResponse {
candidates: StreamCandidate[];
}
export interface StreamCandidate {
streamId: string;
title: string;
properties: StreamProperty[];
actions: StreamAction[];
}
export interface CatalogueCapabilities {
moviesCatalogue: CatalogueCapability | undefined;
seriesCatalogue: CatalogueCapability | undefined;
combinedCatalogue: CatalogueCapability | undefined;
missingCatalogue: CatalogueCapability | undefined;
}
export interface CatalogueCapability {
isSupported: boolean;
orderOptions: OrderOption[];
}
export interface OrderOption {
label: string;
value: string;
directions: DirectionOption[];
}
export interface DirectionOption {
label: string;
value: string;
}
export interface CatalogueRequest {
userContext: UserContext | undefined;
pagination: PaginationParams | undefined;
order?: string | undefined;
direction?: string | undefined;
}
export interface CatalogueResponse {
items: CatalogueItem[];
total: number;
page: number;
itemsPerPage: number;
}
export interface CatalogueItem {
tmdbId: string;
/** "movie" or "series" */
mediaType: string;
}
export interface PaginationParams {
page: number;
itemsPerPage: number;
}
export interface MissingCatalogueRequest {
userContext: UserContext | undefined;
pagination: PaginationParams | undefined;
order?: string | undefined;
direction?: string | undefined;
myListItemsJson: { [key: string]: string };
}
export interface MissingCatalogueRequest_MyListItemsJsonEntry {
key: string;
value: string;
}
export interface MissingCatalogueResponse {
itemsJson: string[];
total: number;
page: number;
itemsPerPage: number;
}
export interface ManagementProfilesResponse {
profiles: ManagementProfile[];
}
export interface ManagementProfile {
id: string;
name: string;
description: string;
autoRequests: AutoRequestOptions | undefined;
autoRemove: AutoRemoveOptions | undefined;
}
export interface AutoRequestOptions {
/** Episodes: Next up + x episodes (0-n) */
nextEpisodesCount?:
| number
| undefined;
/** Episodes: Current season + x next seasons */
nextSeasonsCount?:
| number
| undefined;
/** Watch RSS feed */
watchRss: boolean;
}
export interface AutoRemoveOptions {
/** After watched + x days */
daysAfterWatched?:
| number
| undefined;
/** After downloaded + x days */
daysAfterDownloaded?:
| number
| undefined;
/** When seeding ratio > x */
minSeedingRatio?:
| number
| undefined;
/** Lazy deletion (delete when disk space needed) */
lazyDeletion: boolean;
/** Priority for lazy deletion */
deletionPriority?: number | undefined;
}
export interface MediaManagementProfileRequest {
userContext: UserContext | undefined;
tmdbId: string;
/** "movie" or "series" */
mediaType: string;
}
export interface UpdateManagementProfileRequest {
userContext: UserContext | undefined;
tmdbId: string;
mediaType: string;
profileId: string;
}
export interface JobsResponse {
jobs: Job[];
}
export interface Job {
id: string;
/** "download", "transcode", "seed" */
type: string;
/** "pending", "active", "completed", "failed", "cancelled" */
status: string;
tmdbId: string;
mediaType: string;
season?: number | undefined;
episode?: number | undefined;
progress: number;
eta?: string | undefined;
metadata: { [key: string]: string };
}
export interface Job_MetadataEntry {
key: string;
value: string;
}
export interface JobDetailsRequest {
userContext: UserContext | undefined;
jobId: string;
}
export interface JobDetails {
job: Job | undefined;
logs: JobLogEntry[];
stats: JobStats | undefined;
}
export interface JobLogEntry {
timestamp: string;
/** "info", "warning", "error" */
level: string;
message: string;
}
export interface JobStats {
downloadSpeed?: number | undefined;
uploadSpeed?: number | undefined;
seeders?: number | undefined;
peers?: number | undefined;
seedingRatio?: number | undefined;
sizeBytes?: number | undefined;
downloadedBytes?: number | undefined;
}
export interface CancelJobRequest {
userContext: UserContext | undefined;
jobId: string;
}
export interface DiskSpaceUsage {
totalBytes: number;
usedBytes: number;
availableBytes: number;
mediaFiles: MediaFileInfo[];
}
export interface MediaFileInfo {
id: string;
tmdbId: string;
mediaType: string;
season?: number | undefined;
episode?: number | undefined;
sizeBytes: number;
filePath: string;
watched: boolean;
seedingRatio?: number | undefined;
addedDate?: string | undefined;
watchedDate?: string | undefined;
markedForDeletion: boolean;
}
export interface CleanupHistoryRequest {
userContext: UserContext | undefined;
pagination: PaginationParams | undefined;
}
export interface CleanupHistoryResponse {
entries: CleanupHistoryEntry[];
total: number;
page: number;
itemsPerPage: number;
}
export interface CleanupHistoryEntry {
timestamp: string;
tmdbId: string;
mediaType: string;
season?: number | undefined;
episode?: number | undefined;
sizeBytes: number;
/** "watched_timeout", "download_timeout", "seeding_complete", "disk_space_needed", "manual" */
reason: string;
}
export interface TriggerCleanupRequest {
userContext: UserContext | undefined;
targetFreeSpaceBytes?: number | undefined;
}
/** Plugin Service - handles plugin metadata and configuration */
export interface PluginService {
/** Get plugin metadata and version */
GetInfo(request: Empty): Promise<PluginInfo>;
}
/** Media Source Provider Service - handles user-specific requests */
export interface MediaSourceProviderService {
/** Get available views for a media item */
GetMediaSourceViews(request: MediaSourceViewsRequest): Promise<MediaSourceViewsResponse>;
/** Get a specific view */
GetMediaSourceView(request: MediaSourceViewRequest): Promise<MediaSourceViewResponse>;
/** Get autoplay stream */
GetAutoplayStream(request: AutoplayStreamRequest): Promise<AutoplayStreamResponse>;
/** Get stream details */
GetStream(request: GetStreamRequest): Promise<StreamResponse>;
/** Handle stream actions (download, delete, etc.) */
HandleAction(request: HandleActionRequest): Promise<ActionResponse>;
/** Proxy handler for streaming (bidirectional for video/subtitle streaming) */
ProxyStream(request: Observable<ProxyRequest>): Observable<ProxyResponse>;
/** Legacy methods (deprecated) */
GetTmdbMovieCandidates(request: TmdbMovieRequest): Promise<StreamCandidatesResponse>;
GetTmdbEpisodeCandidates(request: TmdbEpisodeRequest): Promise<StreamCandidatesResponse>;
}
/** Catalogue Provider Service - handles library catalogues */
export interface CatalogueProviderService {
/** Get catalogue capabilities */
GetCatalogueCapabilities(request: Empty): Promise<CatalogueCapabilities>;
/** Get combined catalogue */
GetCatalogue(request: CatalogueRequest): Promise<CatalogueResponse>;
/** Get movies catalogue */
GetMovieCatalogue(request: CatalogueRequest): Promise<CatalogueResponse>;
/** Get series catalogue */
GetSeriesCatalogue(request: CatalogueRequest): Promise<CatalogueResponse>;
/** Get missing items in catalogue */
GetMissingInCatalogue(request: MissingCatalogueRequest): Promise<MissingCatalogueResponse>;
}
/** Management Profile Service - NEW for monitoring/management profiles */
export interface ManagementProfileService {
/** Get available management profiles */
GetManagementProfiles(request: Empty): Promise<ManagementProfilesResponse>;
/** Get management profile for specific media */
GetMediaManagementProfile(request: MediaManagementProfileRequest): Promise<ManagementProfile>;
/** Update management profile for media */
UpdateMediaManagementProfile(request: UpdateManagementProfileRequest): Promise<ManagementProfile>;
}
/** Job Management Service - NEW for monitoring downloads, transcoding, etc. */
export interface JobManagementService {
/** Get all active jobs */
GetActiveJobs(request: Empty): Promise<JobsResponse>;
/** Get job details */
GetJobDetails(request: JobDetailsRequest): Promise<JobDetails>;
/** Cancel a job */
CancelJob(request: CancelJobRequest): Promise<ActionResponse>;
/** Get disk space usage */
GetDiskSpaceUsage(request: Empty): Promise<DiskSpaceUsage>;
/** Get cleanup history */
GetCleanupHistory(request: CleanupHistoryRequest): Promise<CleanupHistoryResponse>;
/** Trigger cleanup */
TriggerCleanup(request: TriggerCleanupRequest): Promise<ActionResponse>;
}

View File

@@ -6,7 +6,7 @@ import {
SourceProviderSettingsTemplate,
UserContext,
ValidationResponse,
} from '@aleksilassila/reiverr-shared';
} from '@aleksilassila/reiverr-shared/dist/src/old';
import { testConnection } from './lib/jackett.api';
import { TorrentMediaSourceProvider } from './media-source-provider';

View File

@@ -1,6 +1,6 @@
import axios, { AxiosError } from 'axios';
import { XMLParser } from 'fast-xml-parser';
import { StreamCandidate } from '@aleksilassila/reiverr-shared';
import { StreamCandidate } from '@aleksilassila/reiverr-shared/dist/src/old';
import { TorrentSettings } from '../types';
import { formatSize, formatBitrate, EPISODE_SEPARATOR } from '../utils';

View File

@@ -13,7 +13,7 @@ import {
Subtitles,
UserContext,
ViewBase,
} from '@aleksilassila/reiverr-shared';
} from '@aleksilassila/reiverr-shared/dist/src/old';
import {
getEpisodeTorrents,
getMovieTorrents,
@@ -274,9 +274,9 @@ export class TorrentMediaSourceProvider extends MediaSourceProvider {
throw new Error('Torrent not found');
}
let src = `${this.proxyUrl}/magnet?link=${encodeURIComponent(link)}&reiverr_token=${
this.token
}`;
let src = `${this.proxyUrl}/magnet?link=${encodeURIComponent(
link,
)}&reiverr_token=${this.token}`;
if (season && episode) {
src += `&season=${season}&episode=${episode}`;
@@ -288,11 +288,12 @@ export class TorrentMediaSourceProvider extends MediaSourceProvider {
.filter((f) => subtitleExtensions.some((ext) => f.name.endsWith(ext)))
.map((f) => ({
kind: 'subtitles',
src: `${this.proxyUrl}/magnet?link=${encodeURIComponent(link)}&reiverr_token=${
this.token
}&file=${f.name}`,
src: `${this.proxyUrl}/magnet?link=${encodeURIComponent(
link,
)}&reiverr_token=${this.token}&file=${f.name}`,
label: f.name,
lang: 'unknown',
default: false,
}));
const stream = {

View File

@@ -1,4 +1,4 @@
import type { SourceProviderSettings } from '@aleksilassila/reiverr-shared';
import type { SourceProviderSettings } from '@aleksilassila/reiverr-shared/dist/src/old';
export interface TorrentSettings extends SourceProviderSettings {
apiKey: string;