diff --git a/backend/src/common/common.dto.ts b/backend/src/common/common.dto.ts index eca2161..a100ffe 100644 --- a/backend/src/common/common.dto.ts +++ b/backend/src/common/common.dto.ts @@ -9,7 +9,7 @@ import { import { PaginatedResponse, PaginationParams, -} from '@aleksilassila/reiverr-shared'; +} from '@aleksilassila/reiverr-shared/dist/src/old'; export const PickAndPartial = ( clazz: Type, diff --git a/backend/src/media/dtos/media.dto.ts b/backend/src/media/dtos/media.dto.ts index c53ebce..4f9ed62 100644 --- a/backend/src/media/dtos/media.dto.ts +++ b/backend/src/media/dtos/media.dto.ts @@ -6,7 +6,7 @@ import { Subtitles, AudioTrack, VideoOptions, -} from '@aleksilassila/reiverr-shared'; +} from '@aleksilassila/reiverr-shared/dist/src/old'; import { ApiProperty } from '@nestjs/swagger'; /* diff --git a/backend/src/media/media-plugins.service.ts b/backend/src/media/media-plugins.service.ts index ced1291..18e18fa 100644 --- a/backend/src/media/media-plugins.service.ts +++ b/backend/src/media/media-plugins.service.ts @@ -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'; diff --git a/backend/src/source-providers/device-profile.dto.ts b/backend/src/source-providers/device-profile.dto.ts index cf056ba..16dd0f5 100644 --- a/backend/src/source-providers/device-profile.dto.ts +++ b/backend/src/source-providers/device-profile.dto.ts @@ -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({ diff --git a/backend/src/source-providers/source-provider.dto.ts b/backend/src/source-providers/source-provider.dto.ts index 9a556a3..e3f0a5a 100644 --- a/backend/src/source-providers/source-provider.dto.ts +++ b/backend/src/source-providers/source-provider.dto.ts @@ -17,7 +17,7 @@ import { StreamResponse, Subtitles, ValidationResponse, -} from '@aleksilassila/reiverr-shared'; +} from '@aleksilassila/reiverr-shared/dist/src/old'; import { ApiProperty, ApiPropertyOptional, diff --git a/backend/src/source-providers/source-providers.controller.ts b/backend/src/source-providers/source-providers.controller.ts index b48b026..838515a 100644 --- a/backend/src/source-providers/source-providers.controller.ts +++ b/backend/src/source-providers/source-providers.controller.ts @@ -1,4 +1,4 @@ -import { ReiverrPlugin } from '@aleksilassila/reiverr-shared'; +import { ReiverrPlugin } from '@aleksilassila/reiverr-shared/dist/src/old'; import { Body, Controller, diff --git a/backend/src/source-providers/source-providers.service.ts b/backend/src/source-providers/source-providers.service.ts index 33fb952..7c2360e 100644 --- a/backend/src/source-providers/source-providers.service.ts +++ b/backend/src/source-providers/source-providers.service.ts @@ -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'; diff --git a/backend/src/source-providers/ui.dto.ts b/backend/src/source-providers/ui.dto.ts index 3939108..489a3e0 100644 --- a/backend/src/source-providers/ui.dto.ts +++ b/backend/src/source-providers/ui.dto.ts @@ -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 { diff --git a/backend/src/users/media-sources/media-source.dto.ts b/backend/src/users/media-sources/media-source.dto.ts index 677a409..1c2ed93 100644 --- a/backend/src/users/media-sources/media-source.dto.ts +++ b/backend/src/users/media-sources/media-source.dto.ts @@ -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 { diff --git a/backend/src/users/media-sources/media-source.entity.ts b/backend/src/users/media-sources/media-source.entity.ts index f6d5a20..e2cae48 100644 --- a/backend/src/users/media-sources/media-source.entity.ts +++ b/backend/src/users/media-sources/media-source.entity.ts @@ -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, diff --git a/backend/src/users/media-sources/media-sources.controller.ts b/backend/src/users/media-sources/media-sources.controller.ts index 421abea..22caefa 100644 --- a/backend/src/users/media-sources/media-sources.controller.ts +++ b/backend/src/users/media-sources/media-sources.controller.ts @@ -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'); diff --git a/backend/src/users/media-sources/media-sources.service.ts b/backend/src/users/media-sources/media-sources.service.ts index f4b56ca..68d135b 100644 --- a/backend/src/users/media-sources/media-sources.service.ts +++ b/backend/src/users/media-sources/media-sources.service.ts @@ -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'; diff --git a/jellyfin.plugin/src/catalogue-provider.ts b/jellyfin.plugin/src/catalogue-provider.ts index 93a764b..cd869a1 100644 --- a/jellyfin.plugin/src/catalogue-provider.ts +++ b/jellyfin.plugin/src/catalogue-provider.ts @@ -6,7 +6,7 @@ import { OrderOption, PaginatedResponse, PaginationParams, -} from '@aleksilassila/reiverr-shared'; +} from '@aleksilassila/reiverr-shared/dist/src/old'; import { ItemSortBy, BaseItemKind, diff --git a/jellyfin.plugin/src/index.ts b/jellyfin.plugin/src/index.ts index aab7c98..b4be98e 100644 --- a/jellyfin.plugin/src/index.ts +++ b/jellyfin.plugin/src/index.ts @@ -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'; diff --git a/jellyfin.plugin/src/media-source-provider.ts b/jellyfin.plugin/src/media-source-provider.ts index a30c08c..c1fa5ca 100644 --- a/jellyfin.plugin/src/media-source-provider.ts +++ b/jellyfin.plugin/src/media-source-provider.ts @@ -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; diff --git a/shared/package.json b/shared/package.json index 271ff72..75364dd 100644 --- a/shared/package.json +++ b/shared/package.json @@ -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", diff --git a/shared/reiverr-plugin.proto b/shared/reiverr-plugin.proto new file mode 100644 index 0000000..aa6db7d --- /dev/null +++ b/shared/reiverr-plugin.proto @@ -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 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 settings = 1; +} + +message ValidationResponse { + bool is_valid = 1; + map errors = 2; + map validated_settings = 3; +} + +// User Context - passed with most requests +message UserContext { + string user_id = 1; + string token = 2; + string source_id = 3; + map 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 headers = 4; + bytes body = 5; +} + +message ProxyResponse { + int32 status_code = 1; + map 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 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 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; +} diff --git a/shared/src/index.ts b/shared/src/index.ts index ba4348b..a6034ec 100644 --- a/shared/src/index.ts +++ b/shared/src/index.ts @@ -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'; diff --git a/shared/src/old.ts b/shared/src/old.ts new file mode 100644 index 0000000..ba4348b --- /dev/null +++ b/shared/src/old.ts @@ -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'; diff --git a/shared/src/reiverr-plugin.ts b/shared/src/reiverr-plugin.ts new file mode 100644 index 0000000..7949758 --- /dev/null +++ b/shared/src/reiverr-plugin.ts @@ -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; +} + +/** Media Source Provider Service - handles user-specific requests */ +export interface MediaSourceProviderService { + /** Get available views for a media item */ + GetMediaSourceViews(request: MediaSourceViewsRequest): Promise; + /** Get a specific view */ + GetMediaSourceView(request: MediaSourceViewRequest): Promise; + /** Get autoplay stream */ + GetAutoplayStream(request: AutoplayStreamRequest): Promise; + /** Get stream details */ + GetStream(request: GetStreamRequest): Promise; + /** Handle stream actions (download, delete, etc.) */ + HandleAction(request: HandleActionRequest): Promise; + /** Proxy handler for streaming (bidirectional for video/subtitle streaming) */ + ProxyStream(request: Observable): Observable; + /** Legacy methods (deprecated) */ + GetTmdbMovieCandidates(request: TmdbMovieRequest): Promise; + GetTmdbEpisodeCandidates(request: TmdbEpisodeRequest): Promise; +} + +/** Catalogue Provider Service - handles library catalogues */ +export interface CatalogueProviderService { + /** Get catalogue capabilities */ + GetCatalogueCapabilities(request: Empty): Promise; + /** Get combined catalogue */ + GetCatalogue(request: CatalogueRequest): Promise; + /** Get movies catalogue */ + GetMovieCatalogue(request: CatalogueRequest): Promise; + /** Get series catalogue */ + GetSeriesCatalogue(request: CatalogueRequest): Promise; + /** Get missing items in catalogue */ + GetMissingInCatalogue(request: MissingCatalogueRequest): Promise; +} + +/** Management Profile Service - NEW for monitoring/management profiles */ +export interface ManagementProfileService { + /** Get available management profiles */ + GetManagementProfiles(request: Empty): Promise; + /** Get management profile for specific media */ + GetMediaManagementProfile(request: MediaManagementProfileRequest): Promise; + /** Update management profile for media */ + UpdateMediaManagementProfile(request: UpdateManagementProfileRequest): Promise; +} + +/** Job Management Service - NEW for monitoring downloads, transcoding, etc. */ +export interface JobManagementService { + /** Get all active jobs */ + GetActiveJobs(request: Empty): Promise; + /** Get job details */ + GetJobDetails(request: JobDetailsRequest): Promise; + /** Cancel a job */ + CancelJob(request: CancelJobRequest): Promise; + /** Get disk space usage */ + GetDiskSpaceUsage(request: Empty): Promise; + /** Get cleanup history */ + GetCleanupHistory(request: CleanupHistoryRequest): Promise; + /** Trigger cleanup */ + TriggerCleanup(request: TriggerCleanupRequest): Promise; +} diff --git a/torrent-stream.plugin/src/index.ts b/torrent-stream.plugin/src/index.ts index a116577..876ed8d 100644 --- a/torrent-stream.plugin/src/index.ts +++ b/torrent-stream.plugin/src/index.ts @@ -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'; diff --git a/torrent-stream.plugin/src/lib/jackett.api.ts b/torrent-stream.plugin/src/lib/jackett.api.ts index 83abffb..2c1cf29 100644 --- a/torrent-stream.plugin/src/lib/jackett.api.ts +++ b/torrent-stream.plugin/src/lib/jackett.api.ts @@ -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'; diff --git a/torrent-stream.plugin/src/media-source-provider.ts b/torrent-stream.plugin/src/media-source-provider.ts index a97d304..85520d0 100644 --- a/torrent-stream.plugin/src/media-source-provider.ts +++ b/torrent-stream.plugin/src/media-source-provider.ts @@ -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 = { diff --git a/torrent-stream.plugin/src/types.ts b/torrent-stream.plugin/src/types.ts index 2aab963..02eb34e 100644 --- a/torrent-stream.plugin/src/types.ts +++ b/torrent-stream.plugin/src/types.ts @@ -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;