Compare commits

...

72 Commits

Author SHA1 Message Date
Joshua M. Boniface
ac82fead82 Bump version to 10.7.0-rc2 2020-12-31 19:23:19 -05:00
dkanada
9a59ff3c87 Merge pull request #4902 from BaronGreenback/NetmaskFix
Fixed loopback subnet

(cherry picked from commit d6db0e4b02)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-31 18:50:02 -05:00
Joshua M. Boniface
a16cf8ec0a Merge pull request #4890 from nielsvanvelzen/4888-fix-search-hints
Fix search hint endpoint error

(cherry picked from commit 7caba04c3c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-31 18:50:02 -05:00
Joshua M. Boniface
4a2b143028 Merge pull request #4884 from crobibero/json-nullable-guid-converter
Add JsonConverter for Nullable Guids

(cherry picked from commit 0de45d8724)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-31 18:50:02 -05:00
Joshua M. Boniface
d737c2b84a Merge pull request #4729 from BaronGreenback/19.0RC---Fix-networkInterfaceFix
(cherry picked from commit ccc1b8bf92)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-31 18:50:02 -05:00
Joshua M. Boniface
1ad8e54035 Merge pull request #4709 from BaronGreenback/PluginDowngrade
(cherry picked from commit 406ae3e43a)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-31 18:49:53 -05:00
Joshua M. Boniface
83dd3e2201 Merge pull request #4891 from Artiume/patch-1
(cherry picked from commit eb084f9021)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 19:13:04 -05:00
Bond-009
d9634b7fc0 Merge pull request #4874 from MrTimscampi/enable-tmdb-omdb
Enable TMDB and OMDB by default

(cherry picked from commit f8681aa518)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Bond-009
05b34b2710 Merge pull request #4872 from BaronGreenback/NetworkManagerFix
Removed workaround code as web is now fixed.

(cherry picked from commit 4ed20c75f7)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Joshua M. Boniface
2dab55a8f2 Merge pull request #4863 from nyanmisaka/boxes-backdrop
Fix boxes in library name backdrop

(cherry picked from commit f2e05bd183)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Joshua M. Boniface
124ab090bc Merge pull request #4861 from crobibero/null-ref
Fix null reference when logging

(cherry picked from commit 93b754c366)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
dkanada
03c8216946 Merge pull request #4860 from nyanmisaka/3ch-transcode-hls
Avoid transcoding to 3ch audio for HLS streaming

(cherry picked from commit 2913e2604c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Claus Vium
f72f27ff45 Merge pull request #4856 from nyanmisaka/amf-profile
Fix some profiles for H264 AMF encoder

(cherry picked from commit afdc98746b)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Bond-009
71188ad27a Merge pull request #4855 from crobibero/cache-json-serializer
Initialize JsonSerializerOptions statically

(cherry picked from commit f42208673f)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Claus Vium
0d9e8b4f00 Merge pull request #4852 from ryanpetris/fix-schedulesdirect-refresh
SchedulesDirect no longer refreshes channels properly

(cherry picked from commit e36881f4fd)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Joshua M. Boniface
fbfb23abab Merge pull request #4850 from BaronGreenback/NetworkApiFix
Null reference fix

(cherry picked from commit bfdd4727b5)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Joshua M. Boniface
622c71ce1c Merge pull request #4847 from crobibero/display-prefs-migration-x251256
Fix another key collision in MigrateDisplayPreferencesDatabase

(cherry picked from commit ca535b2fbe)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Claus Vium
dbc9256945 Merge pull request #4842 from crobibero/date-time-formatter
Add JsonDateTimeConverter

(cherry picked from commit c8a89b0fe3)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Claus Vium
6b20aaaa6a Merge pull request #4836 from crobibero/dashboard-theme
Return dashboardTheme when requesting DisplayPreferences

(cherry picked from commit 98da2c67a5)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Claus Vium
c2097ba5fe Merge pull request #4833 from Ullmie02/similar-fix
Fix similar items endpoint for movies and TV

(cherry picked from commit c90c382e2f)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Bond-009
8884f4f288 Merge pull request #4828 from joshuaboniface/linux-alt-arch
(cherry picked from commit 2e8e96874f)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Claus Vium
ce741f541c Merge pull request #4824 from crobibero/livestream-post-body
Add request parameters to OpenLiveStreamDto

(cherry picked from commit 53119ed2a1)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Joshua M. Boniface
783d6409af Merge pull request #4821 from BaronGreenback/disableDlna
(cherry picked from commit a77788906c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:51 -05:00
Joshua M. Boniface
5bf25ce2cc Merge pull request #4819 from crobibero/download-name
Set filename when downloading file

(cherry picked from commit 668e168c47)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:50 -05:00
Claus Vium
6c2ddd9758 Merge pull request #4816 from nyanmisaka/profiles
Fix some video profiles for Android client

(cherry picked from commit e9db47cd20)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:50 -05:00
Claus Vium
8760a298b1 Merge pull request #4807 from nyanmisaka/ps4-dlna
Correct DLNA audio codecs for PS3 and PS4

(cherry picked from commit 1f2488d7d9)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:50 -05:00
Claus Vium
c08933c9d9 Merge pull request #4803 from ryanpetris/fix-getuser
Fix Live TV Recording Scheduling

(cherry picked from commit 6274cf8fcc)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:50 -05:00
Bond-009
c86c652006 Merge pull request #4794 from cvium/fix_image_upload
Convert from base64 when saving item images

(cherry picked from commit e84622b4ab)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:50 -05:00
Bond-009
c08ce82a04 Merge pull request #4792 from cvium/fix_missing_seasons
Add missing seasons during AfterMetadataRefresh

(cherry picked from commit 8eeed82523)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:50 -05:00
Claus Vium
b10178bd1e Merge pull request #4789 from crobibero/provider-search
Fix get provider id extension

(cherry picked from commit bc8f1bdcac)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:50 -05:00
Bond-009
00a608dfab Merge pull request #4781 from crobibero/map-xmltv
Use request body for mapping xml channels

(cherry picked from commit 906ee4f962)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:50 -05:00
Claus Vium
480ded0671 Merge pull request #4771 from crobibero/typed-get-preference
Use typed UserManager GetPreference

(cherry picked from commit 21d2e9ff0c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-30 18:55:50 -05:00
Bond-009
98c081f0ce Merge pull request #4774 from nyanmisaka/finetune-tonemap
Fine tune some tone mapping params

(cherry picked from commit 381341c83b)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:33 -05:00
Joshua M. Boniface
e77d0ab5fd Merge pull request #4773 from Artiume/patch-10
Remove opf extension for book types

(cherry picked from commit e7ae712437)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:33 -05:00
Claus Vium
28ef1bc8b2 Merge pull request #4769 from crobibero/typo
Check correct fetcher list for provider name

(cherry picked from commit 4ecb30ef10)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:33 -05:00
Claus Vium
7ebf7014e0 Merge pull request #4767 from nyanmisaka/fix-ssl-save
(cherry picked from commit f9a78625b7)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:33 -05:00
Claus Vium
89a649cc23 Merge pull request #4762 from crobibero/api-file-return
Fix openapi file schema

(cherry picked from commit b56feac841)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:33 -05:00
Claus Vium
24b2991def Merge pull request #4761 from crobibero/playlist
(cherry picked from commit 13bb5e1ead)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:33 -05:00
Claus Vium
87dde66e92 Merge pull request #4758 from nyanmisaka/fix-landingScreen-options
(cherry picked from commit f8ef38c0ea)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:33 -05:00
Claus Vium
1b82ef905e Merge pull request #4757 from cvium/semi_revert_defer_image_loading
(cherry picked from commit 0cddfbcff5)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:33 -05:00
Claus Vium
02cc83b807 Merge pull request #4756 from crobibero/api-key-inverted-condition
Fix inverted condition when authenticating with an ApiKey

(cherry picked from commit c1bb29532f)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
0d8fa795a0 Merge pull request #4753 from crobibero/5.0.1
Update to dotnet 5.0.1

(cherry picked from commit a56ac65449)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Bond-009
b32f15ab1e Merge pull request #4751 from nyanmisaka/mpegts-batchsize
(cherry picked from commit 9e601ba731)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Joshua M. Boniface
26993e39f7 Merge pull request #4750 from crobibero/skia
Fix blueberry

(cherry picked from commit 7b5319bb07)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Bond-009
ddedb2d7f1 Merge pull request #4749 from crobibero/guid-standard
(cherry picked from commit 933e7fa159)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
5c0d930dc3 Merge pull request #4743 from crobibero/metadata-options
Actually use library options when filtering metadata providers

(cherry picked from commit e85884ddb9)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
13d62c5977 Merge pull request #4741 from jellyfin/tests8
Add tests for HdHomerunHost.GetLineup

(cherry picked from commit 31e8273795)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
9799b6ae81 Merge pull request #4738 from jellyfin/tests8
Add tests for HdHomerunHost.GetModelInfo

(cherry picked from commit e6650651b3)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
c1dd8f2050 Merge pull request #4737 from crobibero/missing-ensure-success
(cherry picked from commit f322866127)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
853c328763 Merge pull request #4736 from nyanmisaka/fix-custom-order
Fix custom library order

(cherry picked from commit b83bc0a589)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Anthony Lavado
126753a1fe Merge pull request #4735 from crobibero/json-recursion
Fix JsonConverter recursion

(cherry picked from commit 94d805d03d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Bond-009
24e4fcc3b7 Merge pull request #4733 from crobibero/omdb-null
Fix potential null reference in OMDB

(cherry picked from commit 87e13b858a)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
aae90a8480 Merge pull request #4730 from crobibero/base-item-dto-guid-nullable
Don't serialize empty GUID to null

(cherry picked from commit b6ecaccf92)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
11a37884f0 Merge pull request #4726 from BaronGreenback/19.0RC--Fix-CertificateLoadError
Fix - Access Denied on using certificates in windows as user.

(cherry picked from commit c5a5f77b9e)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
49f3579c1b Merge pull request #4724 from BaronGreenback/17.0RC----DLNA_PlayTo_Fix
(cherry picked from commit e6ea5a776d)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
259d811b95 Merge pull request #4722 from crobibero/forbid
Fix API forbidden response

(cherry picked from commit 80ff564143)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Joshua M. Boniface
7e01cce884 Merge pull request #4716 from OancaAndrei/syncplay-new-auth-policies
(cherry picked from commit 4f6a585424)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
2e5333c1d4 Merge pull request #4715 from crobibero/hdhr-json-bool
Add number to bool json converter

(cherry picked from commit 0aad17554c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
e8e1bbffd9 Merge pull request #4713 from crobibero/robots
(cherry picked from commit a3a7467f49)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
723fe43d2e Merge pull request #4711 from barronpm/api-fixes
Add required attributes to parameters

(cherry picked from commit 4c199ac9a5)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
e70a6d41f4 Merge pull request #4710 from OancaAndrei/syncplay-fix-session-restore
Restore sessions in SyncPlay groups upon reconnection

(cherry picked from commit a57e465de9)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
9d4417eee3 Merge pull request #4706 from cvium/fix_aspectratio_again
Only apply series image aspect ratio if episode/season has no primary image

(cherry picked from commit 0d5f651ae9)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
67f41386ba Merge pull request #4701 from crobibero/plugin-version
(cherry picked from commit 7455de1f85)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Bond-009
b1af8db423 Merge pull request #4699 from crobibero/display_prefs_index
Fix CustomItemDisplayPreferences unique key collision in the migration

(cherry picked from commit b3caa51173)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
91656acabb Merge pull request #4678 from BaronGreenback/network_routing_fix_17.0rc
Change logging level and message in NetworkManager

(cherry picked from commit 885d42cb65)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Claus Vium
5fa8c83ba4 Merge pull request #4675 from BaronGreenback/ProxyDNS
(cherry picked from commit 8c00fbea9c)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:32 -05:00
Bond-009
683bc27b27 Merge pull request #4672 from cvium/fix_mergeversions_which_was_unrelated_to_my_bughunt
Fix MergeVersions endpoint

(cherry picked from commit 26919eed26)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:31 -05:00
Bond-009
0b6a05cf82 Merge pull request #4671 from cvium/allow_proxy
Clear KnownNetworks and KnownProxies if none are configured explicitly

(cherry picked from commit 804dd00425)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-13 20:34:31 -05:00
Joshua M. Boniface
2647935b96 Merge pull request #4669 from MrTimscampi/fix-npm-public
Fix NPM command in CI

(cherry picked from commit 8976b22ac4)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-05 01:36:00 -05:00
Joshua M. Boniface
2a4023c6c7 Merge pull request #4667 from joshuaboniface/fix-nuget-ci
Remove obsolete erroring command

(cherry picked from commit f2c2beca0f)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-05 01:35:18 -05:00
Joshua M. Boniface
2a2630098b Merge pull request #4662 from joshuaboniface/fix-bump-version
Fix bad do in bump_version

(cherry picked from commit e3a1991fe9)
Signed-off-by: Joshua M. Boniface <joshua@boniface.me>
2020-12-05 01:12:47 -05:00
Joshua M. Boniface
79472dce70 Bump version to 10.7.0~rc1 2020-12-04 21:03:57 -05:00
158 changed files with 4007 additions and 1627 deletions

View File

@@ -52,7 +52,8 @@ jobs:
- task: Npm@1
displayName: 'Publish stable typescript axios client'
inputs:
command: publish
command: custom
customCommand: publish --access public
publishRegistry: useExternalRegistry
publishEndpoint: 'jellyfin-bot for NPM'
workingDir: ./apiclient/generated/typescript/axios

View File

@@ -22,6 +22,12 @@ jobs:
BuildConfiguration: ubuntu.armhf
Linux.amd64:
BuildConfiguration: linux.amd64
Linux.amd64-musl:
BuildConfiguration: linux.amd64-musl
Linux.arm64:
BuildConfiguration: linux.arm64
Linux.armhf:
BuildConfiguration: linux.armhf
Windows.amd64:
BuildConfiguration: windows.amd64
MacOS:
@@ -221,12 +227,6 @@ jobs:
pathToPublish: $(Build.ArtifactStagingDirectory)
artifactName: Jellyfin Nuget Packages
- task: NuGetAuthenticate@0
displayName: 'Authenticate to stable Nuget feed'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
inputs:
nuGetServiceConnections: 'NugetOrg'
- task: NuGetCommand@2
displayName: 'Push Nuget packages to stable feed'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')

View File

@@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna.PlayTo;
using Emby.Dlna.Ssdp;
using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.Manager;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
@@ -52,6 +53,8 @@ namespace Emby.Dlna.Main
private readonly ISocketFactory _socketFactory;
private readonly INetworkManager _networkManager;
private readonly object _syncLock = new object();
private readonly NetworkConfiguration _netConfig;
private readonly bool _disabled;
private PlayToManager _manager;
private SsdpDevicePublisher _publisher;
@@ -122,10 +125,22 @@ namespace Emby.Dlna.Main
httpClientFactory,
config);
Current = this;
_netConfig = config.GetConfiguration<NetworkConfiguration>("network");
_disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
if (_disabled)
{
_logger.LogError("The DLNA specification does not support HTTPS.");
}
}
public static DlnaEntryPoint Current { get; private set; }
/// <summary>
/// Gets a value indicating whether the dlna server is enabled.
/// </summary>
public static bool Enabled { get; private set; }
public IContentDirectory ContentDirectory { get; private set; }
public IConnectionManager ConnectionManager { get; private set; }
@@ -136,6 +151,12 @@ namespace Emby.Dlna.Main
{
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
if (_disabled)
{
// No use starting as dlna won't work, as we're running purely on HTTPS.
return;
}
ReloadComponents();
_config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
@@ -152,6 +173,7 @@ namespace Emby.Dlna.Main
private void ReloadComponents()
{
var options = _config.GetDlnaConfiguration();
Enabled = options.EnableServer;
StartSsdpHandler();
@@ -290,12 +312,15 @@ namespace Emby.Dlna.Main
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
var uri = new Uri(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
// DLNA will only work over http, so we must reset to http:// : {port}
uri.Scheme = "http://";
uri.Port = _netConfig.PublicPort;
var device = new SsdpRootDevice
{
CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
Location = uri, // Must point to the URL that serves your devices UPnP description document.
Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document.
Address = address.Address,
PrefixLength = address.PrefixLength,
FriendlyName = "Jellyfin",

View File

@@ -340,10 +340,19 @@ namespace Emby.Dlna.PlayTo
}
var playlist = new PlaylistItem[len];
playlist[0] = CreatePlaylistItem(items[0], user, command.StartPositionTicks.Value, command.MediaSourceId, command.AudioStreamIndex, command.SubtitleStreamIndex);
// Not nullable enabled - so this is required.
playlist[0] = CreatePlaylistItem(
items[0],
user,
command.StartPositionTicks ?? 0,
command.MediaSourceId ?? string.Empty,
command.AudioStreamIndex,
command.SubtitleStreamIndex);
for (int i = 1; i < len; i++)
{
playlist[i] = CreatePlaylistItem(items[i], user, 0, null, null, null);
playlist[i] = CreatePlaylistItem(items[i], user, 0, string.Empty, null, null);
}
_logger.LogDebug("{0} - Playlist created", _session.DeviceName);

View File

@@ -52,7 +52,7 @@ namespace Emby.Dlna.Profiles
Container = "ts,mpegts",
Type = DlnaProfileType.Video,
VideoCodec = "mpeg1video,mpeg2video,h264",
AudioCodec = "ac3,mp2,mp3,aac"
AudioCodec = "aac,ac3,mp2"
},
new DirectPlayProfile
{
@@ -92,7 +92,7 @@ namespace Emby.Dlna.Profiles
{
Container = "ts",
VideoCodec = "h264",
AudioCodec = "ac3,aac,mp3",
AudioCodec = "aac,ac3,mp2",
Type = DlnaProfileType.Video
},
new TranscodingProfile

View File

@@ -52,7 +52,7 @@ namespace Emby.Dlna.Profiles
Container = "ts,mpegts",
Type = DlnaProfileType.Video,
VideoCodec = "mpeg1video,mpeg2video,h264",
AudioCodec = "ac3,mp2,mp3,aac"
AudioCodec = "aac,ac3,mp2"
},
new DirectPlayProfile
{
@@ -94,7 +94,7 @@ namespace Emby.Dlna.Profiles
{
Container = "ts",
VideoCodec = "h264",
AudioCodec = "mp3",
AudioCodec = "aac,ac3,mp2",
Type = DlnaProfileType.Video
},
new TranscodingProfile

View File

@@ -38,7 +38,7 @@
<XmlRootAttributes />
<DirectPlayProfiles>
<DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" />
<DirectPlayProfile container="ts,mpegts" audioCodec="ac3,mp2,mp3,aac" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
<DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,mp2" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
<DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" />
<DirectPlayProfile container="mp4" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" />
<DirectPlayProfile container="aac,mp3,wav" type="Audio" />
@@ -46,7 +46,7 @@
</DirectPlayProfiles>
<TranscodingProfiles>
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac,ac3,mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
</TranscodingProfiles>
<ContainerProfiles>

View File

@@ -38,7 +38,7 @@
<XmlRootAttributes />
<DirectPlayProfiles>
<DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" />
<DirectPlayProfile container="ts,mpegts" audioCodec="ac3,mp2,mp3,aac" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
<DirectPlayProfile container="ts,mpegts" audioCodec="aac,ac3,mp2" videoCodec="mpeg1video,mpeg2video,h264" type="Video" />
<DirectPlayProfile container="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" />
<DirectPlayProfile container="mp4,mkv,m4v" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" />
<DirectPlayProfile container="aac,mp3,wav" type="Audio" />
@@ -46,7 +46,7 @@
</DirectPlayProfiles>
<TranscodingProfiles>
<TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Bytes" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
<TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac,ac3,mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
</TranscodingProfiles>
<ContainerProfiles>

View File

@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@@ -119,7 +120,9 @@ namespace Emby.Server.Implementations
private readonly IXmlSerializer _xmlSerializer;
private readonly IJsonSerializer _jsonSerializer;
private readonly IStartupOptions _startupOptions;
private readonly IPluginManager _pluginManager;
private List<Type> _creatingInstances;
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
private string[] _urlPrefixes;
@@ -181,16 +184,6 @@ namespace Emby.Server.Implementations
protected IServiceCollection ServiceCollection { get; }
private IPlugin[] _plugins;
private IReadOnlyList<LocalPlugin> _pluginsManifests;
/// <summary>
/// Gets the plugins.
/// </summary>
/// <value>The plugins.</value>
public IReadOnlyList<IPlugin> Plugins => _plugins;
/// <summary>
/// Gets the logger factory.
/// </summary>
@@ -284,16 +277,16 @@ namespace Emby.Server.Implementations
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
CertificateInfo = new CertificateInfo
{
Path = ServerConfigurationManager.Configuration.CertificatePath,
Password = ServerConfigurationManager.Configuration.CertificatePassword
};
Certificate = GetCertificate(CertificateInfo);
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3);
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
_pluginManager = new PluginManager(
LoggerFactory.CreateLogger<PluginManager>(),
this,
ServerConfigurationManager.Configuration,
ApplicationPaths.PluginsPath,
ApplicationVersion);
}
/// <summary>
@@ -393,16 +386,41 @@ namespace Emby.Server.Implementations
/// <returns>System.Object.</returns>
protected object CreateInstanceSafe(Type type)
{
if (_creatingInstances == null)
{
_creatingInstances = new List<Type>();
}
if (_creatingInstances.IndexOf(type) != -1)
{
Logger.LogError("DI Loop detected in the attempted creation of {Type}", type.FullName);
foreach (var entry in _creatingInstances)
{
Logger.LogError("Called from: {TypeName}", entry.FullName);
}
_pluginManager.FailPlugin(type.Assembly);
throw new ExternalException("DI Loop detected.");
}
try
{
_creatingInstances.Add(type);
Logger.LogDebug("Creating instance of {Type}", type);
return ActivatorUtilities.CreateInstance(ServiceProvider, type);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error creating {Type}", type);
// If this is a plugin fail it.
_pluginManager.FailPlugin(type.Assembly);
return null;
}
finally
{
_creatingInstances.Remove(type);
}
}
/// <summary>
@@ -412,11 +430,7 @@ namespace Emby.Server.Implementations
/// <returns>``0.</returns>
public T Resolve<T>() => ServiceProvider.GetService<T>();
/// <summary>
/// Gets the export types.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <returns>IEnumerable{Type}.</returns>
/// <inheritdoc/>
public IEnumerable<Type> GetExportTypes<T>()
{
var currentType = typeof(T);
@@ -445,6 +459,27 @@ namespace Emby.Server.Implementations
return parts;
}
/// <inheritdoc />
public IReadOnlyCollection<T> GetExports<T>(CreationDelegate defaultFunc, bool manageLifetime = true)
{
// Convert to list so this isn't executed for each iteration
var parts = GetExportTypes<T>()
.Select(i => defaultFunc(i))
.Where(i => i != null)
.Cast<T>()
.ToList();
if (manageLifetime)
{
lock (_disposableParts)
{
_disposableParts.AddRange(parts.OfType<IDisposable>());
}
}
return parts;
}
/// <summary>
/// Runs the startup tasks.
/// </summary>
@@ -456,6 +491,7 @@ namespace Emby.Server.Implementations
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated;
_mediaEncoder.SetFFmpegPath();
@@ -505,11 +541,18 @@ namespace Emby.Server.Implementations
HttpsPort = NetworkConfiguration.DefaultHttpsPort;
}
CertificateInfo = new CertificateInfo
{
Path = networkConfiguration.CertificatePath,
Password = networkConfiguration.CertificatePassword
};
Certificate = GetCertificate(CertificateInfo);
DiscoverTypes();
RegisterServices();
RegisterPluginServices();
_pluginManager.RegisterServices(ServiceCollection);
}
/// <summary>
@@ -523,7 +566,7 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton(ConfigurationManager);
ServiceCollection.AddSingleton<IApplicationHost>(this);
ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
@@ -714,7 +757,7 @@ namespace Emby.Server.Implementations
// Don't use an empty string password
var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password;
var localCert = new X509Certificate2(certificateLocation, password);
var localCert = new X509Certificate2(certificateLocation, password, X509KeyStorageFlags.UserKeySet);
// localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA;
if (!localCert.HasPrivateKey)
{
@@ -768,34 +811,7 @@ namespace Emby.Server.Implementations
}
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
_plugins = GetExports<IPlugin>()
.Where(i => i != null)
.ToArray();
if (Plugins != null)
{
foreach (var plugin in Plugins)
{
if (_pluginsManifests != null && plugin is IPluginAssembly assemblyPlugin)
{
// Ensure the version number matches the Plugin Manifest information.
foreach (var item in _pluginsManifests)
{
if (Path.GetDirectoryName(plugin.AssemblyFilePath).Equals(item.Path, StringComparison.OrdinalIgnoreCase))
{
// Update version number to that of the manifest.
assemblyPlugin.SetAttributes(
plugin.AssemblyFilePath,
Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(plugin.AssemblyFilePath)),
item.Version);
break;
}
}
}
Logger.LogInformation("Loaded plugin: {PluginName} {PluginVersion}", plugin.Name, plugin.Version);
}
}
_pluginManager.CreatePlugins();
_urlPrefixes = GetUrlPrefixes().ToArray();
@@ -834,22 +850,6 @@ namespace Emby.Server.Implementations
_allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray();
}
private void RegisterPluginServices()
{
foreach (var pluginServiceRegistrator in GetExportTypes<IPluginServiceRegistrator>())
{
try
{
var instance = (IPluginServiceRegistrator)Activator.CreateInstance(pluginServiceRegistrator);
instance.RegisterServices(ServiceCollection);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error registering plugin services from {Assembly}.", pluginServiceRegistrator.Assembly);
}
}
}
private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies)
{
foreach (var ass in assemblies)
@@ -862,11 +862,13 @@ namespace Emby.Server.Implementations
catch (FileNotFoundException ex)
{
Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
_pluginManager.FailPlugin(ass);
continue;
}
catch (TypeLoadException ex)
{
Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName);
_pluginManager.FailPlugin(ass);
continue;
}
@@ -912,11 +914,11 @@ namespace Emby.Server.Implementations
protected void OnConfigurationUpdated(object sender, EventArgs e)
{
var requiresRestart = false;
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
// Don't do anything if these haven't been set yet
if (HttpPort != 0 && HttpsPort != 0)
{
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
// Need to restart if ports have changed
if (networkConfiguration.HttpServerPortNumber != HttpPort ||
networkConfiguration.HttpsPortNumber != HttpsPort)
@@ -936,10 +938,7 @@ namespace Emby.Server.Implementations
requiresRestart = true;
}
var currentCertPath = CertificateInfo?.Path;
var newCertPath = ServerConfigurationManager.Configuration.CertificatePath;
if (!string.Equals(currentCertPath, newCertPath, StringComparison.OrdinalIgnoreCase))
if (ValidateSslCertificate(networkConfiguration))
{
requiresRestart = true;
}
@@ -952,6 +951,33 @@ namespace Emby.Server.Implementations
}
}
/// <summary>
/// Validates the SSL certificate.
/// </summary>
/// <param name="networkConfig">The new configuration.</param>
/// <exception cref="FileNotFoundException">The certificate path doesn't exist.</exception>
private bool ValidateSslCertificate(NetworkConfiguration networkConfig)
{
var newPath = networkConfig.CertificatePath;
if (!string.IsNullOrWhiteSpace(newPath)
&& !string.Equals(CertificateInfo?.Path, newPath, StringComparison.Ordinal))
{
if (File.Exists(newPath))
{
return true;
}
throw new FileNotFoundException(
string.Format(
CultureInfo.InvariantCulture,
"Certificate file '{0}' does not exist.",
newPath));
}
return false;
}
/// <summary>
/// Notifies that the kernel that a change has been made that requires a restart.
/// </summary>
@@ -1005,129 +1031,15 @@ namespace Emby.Server.Implementations
protected abstract void RestartInternal();
/// <inheritdoc/>
public IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true)
{
var minimumVersion = new Version(0, 0, 0, 1);
var versions = new List<LocalPlugin>();
if (!Directory.Exists(path))
{
// Plugin path doesn't exist, don't try to enumerate subfolders.
return Enumerable.Empty<LocalPlugin>();
}
var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
foreach (var dir in directories)
{
try
{
var metafile = Path.Combine(dir, "meta.json");
if (File.Exists(metafile))
{
var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
{
targetAbi = minimumVersion;
}
if (!Version.TryParse(manifest.Version, out var version))
{
version = minimumVersion;
}
if (ApplicationVersion >= targetAbi)
{
// Only load Plugins if the plugin is built for this version or below.
versions.Add(new LocalPlugin(manifest.Guid, manifest.Name, version, dir));
}
}
else
{
// No metafile, so lets see if the folder is versioned.
metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1];
int versionIndex = dir.LastIndexOf('_');
if (versionIndex != -1 && Version.TryParse(dir.AsSpan()[(versionIndex + 1)..], out Version parsedVersion))
{
// Versioned folder.
versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir));
}
else
{
// Un-versioned folder - Add it under the path name and version 0.0.0.1.
versions.Add(new LocalPlugin(Guid.Empty, metafile, minimumVersion, dir));
}
}
}
catch
{
continue;
}
}
string lastName = string.Empty;
versions.Sort(LocalPlugin.Compare);
// Traverse backwards through the list.
// The first item will be the latest version.
for (int x = versions.Count - 1; x >= 0; x--)
{
if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase))
{
versions[x].DllFiles.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
lastName = versions[x].Name;
continue;
}
if (!string.IsNullOrEmpty(lastName) && cleanup)
{
// Attempt a cleanup of old folders.
try
{
Logger.LogDebug("Deleting {Path}", versions[x].Path);
Directory.Delete(versions[x].Path, true);
}
catch (Exception e)
{
Logger.LogWarning(e, "Unable to delete {Path}", versions[x].Path);
}
versions.RemoveAt(x);
}
}
return versions;
}
/// <summary>
/// Gets the composable part assemblies.
/// </summary>
/// <returns>IEnumerable{Assembly}.</returns>
protected IEnumerable<Assembly> GetComposablePartAssemblies()
{
if (Directory.Exists(ApplicationPaths.PluginsPath))
foreach (var p in _pluginManager.LoadAssemblies())
{
_pluginsManifests = GetLocalPlugins(ApplicationPaths.PluginsPath).ToList();
foreach (var plugin in _pluginsManifests)
{
foreach (var file in plugin.DllFiles)
{
Assembly plugAss;
try
{
plugAss = Assembly.LoadFrom(file);
}
catch (FileLoadException ex)
{
Logger.LogError(ex, "Failed to load assembly {Path}", file);
continue;
}
Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
yield return plugAss;
}
}
yield return p;
}
// Include composable parts in the Model assembly
@@ -1369,17 +1281,6 @@ namespace Emby.Server.Implementations
}
}
/// <summary>
/// Removes the plugin.
/// </summary>
/// <param name="plugin">The plugin.</param>
public void RemovePlugin(IPlugin plugin)
{
var list = _plugins.ToList();
list.Remove(plugin);
_plugins = list.ToArray();
}
public IEnumerable<Assembly> GetApiPluginAssemblies()
{
var assemblies = _allConcreteTypes

View File

@@ -88,38 +88,12 @@ namespace Emby.Server.Implementations.Configuration
var newConfig = (ServerConfiguration)newConfiguration;
ValidateMetadataPath(newConfig);
ValidateSslCertificate(newConfig);
ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration>(newConfig));
base.ReplaceConfiguration(newConfiguration);
}
/// <summary>
/// Validates the SSL certificate.
/// </summary>
/// <param name="newConfig">The new configuration.</param>
/// <exception cref="FileNotFoundException">The certificate path doesn't exist.</exception>
private void ValidateSslCertificate(BaseApplicationConfiguration newConfig)
{
var serverConfig = (ServerConfiguration)newConfig;
var newPath = serverConfig.CertificatePath;
if (!string.IsNullOrWhiteSpace(newPath)
&& !string.Equals(Configuration.CertificatePath, newPath, StringComparison.Ordinal))
{
if (!File.Exists(newPath))
{
throw new FileNotFoundException(
string.Format(
CultureInfo.InvariantCulture,
"Certificate file '{0}' does not exist.",
newPath));
}
}
}
/// <summary>
/// Validates the metadata path.
/// </summary>

View File

@@ -1138,7 +1138,10 @@ namespace Emby.Server.Implementations.Dto
if (episodeSeries != null)
{
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
AttachPrimaryImageAspectRatio(dto, episodeSeries);
if (!dto.ImageTags.ContainsKey(ImageType.Primary))
{
AttachPrimaryImageAspectRatio(dto, episodeSeries);
}
}
}
@@ -1185,7 +1188,10 @@ namespace Emby.Server.Implementations.Dto
if (series != null)
{
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
AttachPrimaryImageAspectRatio(dto, series);
if (!dto.ImageTags.ContainsKey(ImageType.Primary))
{
AttachPrimaryImageAspectRatio(dto, series);
}
}
}
}

View File

@@ -31,7 +31,7 @@
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
@@ -74,5 +74,4 @@
<EmbeddedResource Include="Localization\Core\*.json" />
<EmbeddedResource Include="Localization\Ratings\*.csv" />
</ItemGroup>
</Project>

View File

@@ -185,11 +185,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
updateToken = true;
}
authInfo.IsApiKey = true;
authInfo.IsApiKey = false;
}
else
{
authInfo.IsApiKey = false;
authInfo.IsApiKey = true;
}
if (updateToken)

View File

@@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
public User GetUser(object requestContext)
{
return GetUser((HttpContext)requestContext);
return GetUser(((HttpRequest)requestContext).HttpContext);
}
}
}

View File

@@ -1,130 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Net;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library
{
/// <summary>
/// A library post scan/refresh task for pre-fetching remote images.
/// </summary>
public class ImageFetcherPostScanTask : ILibraryPostScanTask
{
private readonly ILibraryManager _libraryManager;
private readonly IProviderManager _providerManager;
private readonly ILogger<ImageFetcherPostScanTask> _logger;
private readonly SemaphoreSlim _imageFetcherLock;
private ConcurrentDictionary<Guid, (BaseItem item, ItemUpdateType updateReason)> _queuedItems;
/// <summary>
/// Initializes a new instance of the <see cref="ImageFetcherPostScanTask"/> class.
/// </summary>
/// <param name="libraryManager">An instance of <see cref="ILibraryManager"/>.</param>
/// <param name="providerManager">An instance of <see cref="IProviderManager"/>.</param>
/// <param name="logger">An instance of <see cref="ILogger{ImageFetcherPostScanTask}"/>.</param>
public ImageFetcherPostScanTask(
ILibraryManager libraryManager,
IProviderManager providerManager,
ILogger<ImageFetcherPostScanTask> logger)
{
_libraryManager = libraryManager;
_providerManager = providerManager;
_logger = logger;
_queuedItems = new ConcurrentDictionary<Guid, (BaseItem item, ItemUpdateType updateReason)>();
_imageFetcherLock = new SemaphoreSlim(1, 1);
_libraryManager.ItemAdded += OnLibraryManagerItemAddedOrUpdated;
_libraryManager.ItemUpdated += OnLibraryManagerItemAddedOrUpdated;
_providerManager.RefreshCompleted += OnProviderManagerRefreshCompleted;
}
/// <inheritdoc />
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
// Sometimes a library scan will cause this to run twice if there's an item refresh going on.
await _imageFetcherLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
var now = DateTime.UtcNow;
var itemGuids = _queuedItems.Keys.ToList();
for (var i = 0; i < itemGuids.Count; i++)
{
if (!_queuedItems.TryGetValue(itemGuids[i], out var queuedItem))
{
continue;
}
var itemId = queuedItem.item.Id.ToString("N", CultureInfo.InvariantCulture);
var itemType = queuedItem.item.GetType();
_logger.LogDebug(
"Updating remote images for item {ItemId} with media type {ItemMediaType}",
itemId,
itemType);
try
{
await _libraryManager.UpdateImagesAsync(queuedItem.item, queuedItem.updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch images for {Type} item with id {ItemId}", itemType, itemId);
}
_queuedItems.TryRemove(queuedItem.item.Id, out _);
}
if (itemGuids.Count > 0)
{
_logger.LogInformation(
"Finished updating/pre-fetching {NumberOfImages} images. Elapsed time: {TimeElapsed}s.",
itemGuids.Count.ToString(CultureInfo.InvariantCulture),
(DateTime.UtcNow - now).TotalSeconds.ToString(CultureInfo.InvariantCulture));
}
else
{
_logger.LogDebug("No images were updated.");
}
}
finally
{
_imageFetcherLock.Release();
}
}
private void OnLibraryManagerItemAddedOrUpdated(object sender, ItemChangeEventArgs itemChangeEventArgs)
{
if (!_queuedItems.ContainsKey(itemChangeEventArgs.Item.Id) && itemChangeEventArgs.Item.ImageInfos.Length > 0)
{
_queuedItems.AddOrUpdate(
itemChangeEventArgs.Item.Id,
(itemChangeEventArgs.Item, itemChangeEventArgs.UpdateReason),
(key, existingValue) => existingValue);
}
}
private void OnProviderManagerRefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
{
if (!_queuedItems.ContainsKey(e.Argument.Id) && e.Argument.ImageInfos.Length > 0)
{
_queuedItems.AddOrUpdate(
e.Argument.Id,
(e.Argument, ItemUpdateType.None),
(key, existingValue) => existingValue);
}
// The RefreshCompleted event is a bit awkward in that it seems to _only_ be fired on
// the item that was refreshed regardless of children refreshes. So we take it as a signal
// that the refresh is entirely completed.
Run(null, CancellationToken.None).GetAwaiter().GetResult();
}
}
}

View File

@@ -42,7 +42,6 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Library;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.MediaInfo;
@@ -1955,9 +1954,12 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
public Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
{
RunMetadataSavers(items, updateReason);
foreach (var item in items)
{
await RunMetadataSavers(item, updateReason).ConfigureAwait(false);
}
_itemRepository.SaveItems(items, cancellationToken);
@@ -1988,25 +1990,22 @@ namespace Emby.Server.Implementations.Library
}
}
}
return Task.CompletedTask;
}
/// <inheritdoc />
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
=> UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken);
public void RunMetadataSavers(IReadOnlyList<BaseItem> items, ItemUpdateType updateReason)
public Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
{
foreach (var item in items)
if (item.IsFileProtocol)
{
if (item.IsFileProtocol)
{
ProviderManager.SaveMetadata(item, updateReason);
}
item.DateLastSaved = DateTime.UtcNow;
ProviderManager.SaveMetadata(item, updateReason);
}
item.DateLastSaved = DateTime.UtcNow;
return UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate);
}
/// <summary>

View File

@@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{
public class BookResolver : MediaBrowser.Controller.Resolvers.ItemResolver<Book>
{
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".opf", ".pdf" };
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" };
protected override Book Resolve(ItemResolveArgs args)
{

View File

@@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.Library
if (query.Limit.HasValue)
{
results = results.GetRange(0, query.Limit.Value);
results = results.GetRange(0, Math.Min(query.Limit.Value, results.Count));
}
return new QueryResult<SearchHintInfo>

View File

@@ -14,6 +14,7 @@ using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using Book = MediaBrowser.Controller.Entities.Book;
using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
namespace Emby.Server.Implementations.Library
{
@@ -219,7 +220,7 @@ namespace Emby.Server.Implementations.Library
var hasRuntime = runtimeTicks > 0;
// If a position has been reported, and if we know the duration
if (positionTicks > 0 && hasRuntime)
if (positionTicks > 0 && hasRuntime && !(item is AudioBook))
{
var pctIn = decimal.Divide(positionTicks, runtimeTicks) * 100;
@@ -245,6 +246,23 @@ namespace Emby.Server.Implementations.Library
}
}
}
else if (positionTicks > 0 && hasRuntime && item is AudioBook)
{
var minIn = TimeSpan.FromTicks(positionTicks).TotalMinutes;
var minOut = TimeSpan.FromTicks(runtimeTicks - positionTicks).TotalMinutes;
if (minIn > _config.Configuration.MinAudiobookResume)
{
// ignore progress during the beginning
positionTicks = 0;
}
else if (minOut < _config.Configuration.MaxAudiobookResume || positionTicks >= runtimeTicks)
{
// mark as completed close to the end
positionTicks = 0;
data.Played = playedToCompletion = true;
}
}
else if (!hasRuntime)
{
// If we don't know the runtime we'll just have to assume it was fully played

View File

@@ -129,23 +129,23 @@ namespace Emby.Server.Implementations.Library
if (!query.IncludeHidden)
{
list = list.Where(i => !user.GetPreference(PreferenceKind.MyMediaExcludes).Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))).ToList();
list = list.Where(i => !user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes).Contains(i.Id)).ToList();
}
var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
var orders = user.GetPreference(PreferenceKind.OrderedViews).ToList();
var orders = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews);
return list
.OrderBy(i =>
{
var index = orders.IndexOf(i.Id.ToString("N", CultureInfo.InvariantCulture));
var index = Array.IndexOf(orders, i.Id);
if (index == -1
&& i is UserView view
&& view.DisplayParentId != Guid.Empty)
{
index = orders.IndexOf(view.DisplayParentId.ToString("N", CultureInfo.InvariantCulture));
index = Array.IndexOf(orders, view.DisplayParentId);
}
return index == -1 ? int.MaxValue : index;
@@ -280,8 +280,8 @@ namespace Emby.Server.Implementations.Library
{
parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
.Where(i => i is Folder)
.Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes)
.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
.Where(i => !user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes)
.Contains(i.Id))
.ToList();
}

View File

@@ -611,25 +611,25 @@ namespace Emby.Server.Implementations.LiveTv.Listings
CancellationToken cancellationToken,
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
{
try
var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false);
}
catch (HttpRequestException ex)
{
_tokens.Clear();
if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500)
{
enableRetry = false;
}
if (!enableRetry)
{
throw;
}
return response;
}
// Response is automatically disposed in the calling function,
// so dispose manually if not returning.
response.Dispose();
if (!enableRetry || (int)response.StatusCode >= 500)
{
throw new HttpRequestException(
string.Format(CultureInfo.InvariantCulture, "Request failed: {0}", response.ReasonPhrase),
null,
response.StatusCode);
}
_tokens.Clear();
options.Headers.TryAddWithoutValidation("token", await GetToken(providerInfo, cancellationToken).ConfigureAwait(false));
return await Send(options, false, providerInfo, cancellationToken).ConfigureAwait(false);
}
@@ -647,6 +647,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(stream).ConfigureAwait(false);
if (string.Equals(root.message, "OK", StringComparison.Ordinal))
@@ -701,6 +702,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
try
{
using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
httpResponse.EnsureSuccessStatusCode();
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var response = httpResponse.Content;
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(stream).ConfigureAwait(false);
@@ -709,7 +711,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
catch (HttpRequestException ex)
{
// Apparently we're supposed to swallow this
// SchedulesDirect returns 400 if no lineups are configured.
if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest)
{
return false;
@@ -782,18 +784,17 @@ namespace Emby.Server.Implementations.LiveTv.Listings
var allStations = root.stations ?? new List<ScheduleDirect.Station>();
var map = root.map;
int len = map.Count;
var array = new List<ChannelInfo>(len);
for (int i = 0; i < len; i++)
var list = new List<ChannelInfo>(map.Count);
foreach (var channel in map)
{
var channelNumber = GetChannelNumber(map[i]);
var channelNumber = GetChannelNumber(channel);
var station = allStations.Find(item => string.Equals(item.stationID, map[i].stationID, StringComparison.OrdinalIgnoreCase));
var station = allStations.Find(item => string.Equals(item.stationID, channel.stationID, StringComparison.OrdinalIgnoreCase));
if (station == null)
{
station = new ScheduleDirect.Station
{
stationID = map[i].stationID
stationID = channel.stationID
};
}
@@ -810,10 +811,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
channelInfo.ImageUrl = station.logo.URL;
}
array[i] = channelInfo;
list.Add(channelInfo);
}
return array;
return list;
}
private static string NormalizeName(string value)

View File

@@ -1928,7 +1928,7 @@ namespace Emby.Server.Implementations.LiveTv
foreach (var programDto in currentProgramDtos)
{
if (currentChannelsDict.TryGetValue(programDto.ChannelId, out BaseItemDto channelDto))
if (programDto.ChannelId.HasValue && currentChannelsDict.TryGetValue(programDto.ChannelId.Value, out BaseItemDto channelDto))
{
channelDto.CurrentProgram = programDto;
}
@@ -2018,7 +2018,7 @@ namespace Emby.Server.Implementations.LiveTv
info.DayPattern = _tvDtoService.GetDayPattern(info.Days);
info.Name = program.Name;
info.ChannelId = programDto.ChannelId;
info.ChannelId = programDto.ChannelId ?? Guid.Empty;
info.ChannelName = programDto.ChannelName;
info.StartDate = program.StartDate;
info.Name = program.Name;

View File

@@ -0,0 +1,21 @@
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
internal class Channels
{
public string GuideNumber { get; set; }
public string GuideName { get; set; }
public string VideoCodec { get; set; }
public string AudioCodec { get; set; }
public string URL { get; set; }
public bool Favorite { get; set; }
public bool DRM { get; set; }
public bool HD { get; set; }
}
}

View File

@@ -0,0 +1,40 @@
using System;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
internal class DiscoverResponse
{
public string FriendlyName { get; set; }
public string ModelNumber { get; set; }
public string FirmwareName { get; set; }
public string FirmwareVersion { get; set; }
public string DeviceID { get; set; }
public string DeviceAuth { get; set; }
public string BaseURL { get; set; }
public string LineupURL { get; set; }
public int TunerCount { get; set; }
public bool SupportsTranscoding
{
get
{
var model = ModelNumber ?? string.Empty;
if (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)
{
return true;
}
return false;
}
}
}
}

View File

@@ -8,10 +8,12 @@ using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
@@ -37,6 +39,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private readonly INetworkManager _networkManager;
private readonly IStreamHelper _streamHelper;
private readonly JsonSerializerOptions _jsonOptions;
private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
public HdHomerunHost(
@@ -56,6 +60,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_socketFactory = socketFactory;
_networkManager = networkManager;
_streamHelper = streamHelper;
_jsonOptions = JsonDefaults.GetOptions();
}
public string Name => "HD Homerun";
@@ -67,13 +73,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private string GetChannelId(TunerHostInfo info, Channels i)
=> ChannelIdPrefix + i.GuideNumber;
private async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
internal async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken)
{
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, cancellationToken: cancellationToken)
var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, _jsonOptions, cancellationToken)
.ConfigureAwait(false) ?? new List<Channels>();
if (info.ImportFavoritesOnly)
@@ -100,7 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Id = GetChannelId(info, i),
IsFavorite = i.Favorite,
TunerHostId = info.Id,
IsHD = i.HD == 1,
IsHD = i.HD,
AudioCodec = i.AudioCodec,
VideoCodec = i.VideoCodec,
ChannelType = ChannelType.TV,
@@ -109,7 +115,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}).Cast<ChannelInfo>().ToList();
}
private async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken)
internal async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken)
{
var cacheKey = info.Id;
@@ -127,10 +133,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
try
{
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.GetAsync(GetApiUrl(info) + "/discover.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, cancellationToken: cancellationToken)
var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, _jsonOptions, cancellationToken)
.ConfigureAwait(false);
if (!string.IsNullOrEmpty(cacheKey))
@@ -328,25 +335,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return new Uri(url).AbsoluteUri.TrimEnd('/');
}
private class Channels
{
public string GuideNumber { get; set; }
public string GuideName { get; set; }
public string VideoCodec { get; set; }
public string AudioCodec { get; set; }
public string URL { get; set; }
public bool Favorite { get; set; }
public bool DRM { get; set; }
public int HD { get; set; }
}
protected EncodingOptions GetEncodingOptions()
{
return Config.GetConfiguration<EncodingOptions>("encoding");
@@ -674,42 +662,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
}
public class DiscoverResponse
{
public string FriendlyName { get; set; }
public string ModelNumber { get; set; }
public string FirmwareName { get; set; }
public string FirmwareVersion { get; set; }
public string DeviceID { get; set; }
public string DeviceAuth { get; set; }
public string BaseURL { get; set; }
public string LineupURL { get; set; }
public int TunerCount { get; set; }
public bool SupportsTranscoding
{
get
{
var model = ModelNumber ?? string.Empty;
if (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1)
{
return true;
}
return false;
}
}
}
public async Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken)
{
lock (_modelCache)
@@ -762,7 +714,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return list;
}
private async Task<TunerHostInfo> TryGetTunerHostInfo(string url, CancellationToken cancellationToken)
internal async Task<TunerHostInfo> TryGetTunerHostInfo(string url, CancellationToken cancellationToken)
{
var hostInfo = new TunerHostInfo
{
@@ -774,6 +726,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
hostInfo.DeviceId = modelInfo.DeviceID;
hostInfo.FriendlyName = modelInfo.FriendlyName;
hostInfo.TunerCount = modelInfo.TunerCount;
return hostInfo;
}

View File

@@ -0,0 +1,688 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using MediaBrowser.Common;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Json.Converters;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Plugins;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Plugins
{
/// <summary>
/// Defines the <see cref="PluginManager" />.
/// </summary>
public class PluginManager : IPluginManager
{
private readonly string _pluginsPath;
private readonly Version _appVersion;
private readonly JsonSerializerOptions _jsonOptions;
private readonly ILogger<PluginManager> _logger;
private readonly IApplicationHost _appHost;
private readonly ServerConfiguration _config;
private readonly IList<LocalPlugin> _plugins;
private readonly Version _minimumVersion;
/// <summary>
/// Initializes a new instance of the <see cref="PluginManager"/> class.
/// </summary>
/// <param name="logger">The <see cref="ILogger"/>.</param>
/// <param name="appHost">The <see cref="IApplicationHost"/>.</param>
/// <param name="config">The <see cref="ServerConfiguration"/>.</param>
/// <param name="pluginsPath">The plugin path.</param>
/// <param name="appVersion">The application version.</param>
public PluginManager(
ILogger<PluginManager> logger,
IApplicationHost appHost,
ServerConfiguration config,
string pluginsPath,
Version appVersion)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_pluginsPath = pluginsPath;
_appVersion = appVersion ?? throw new ArgumentNullException(nameof(appVersion));
_jsonOptions = new JsonSerializerOptions(JsonDefaults.GetOptions())
{
WriteIndented = true
};
// We need to use the default GUID converter, so we need to remove any custom ones.
for (int a = _jsonOptions.Converters.Count - 1; a >= 0; a--)
{
if (_jsonOptions.Converters[a] is JsonGuidConverter convertor)
{
_jsonOptions.Converters.Remove(convertor);
break;
}
}
_config = config;
_appHost = appHost;
_minimumVersion = new Version(0, 0, 0, 1);
_plugins = Directory.Exists(_pluginsPath) ? DiscoverPlugins().ToList() : new List<LocalPlugin>();
}
/// <summary>
/// Gets the Plugins.
/// </summary>
public IList<LocalPlugin> Plugins => _plugins;
/// <summary>
/// Returns all the assemblies.
/// </summary>
/// <returns>An IEnumerable{Assembly}.</returns>
public IEnumerable<Assembly> LoadAssemblies()
{
// Attempt to remove any deleted plugins and change any successors to be active.
for (int i = _plugins.Count - 1; i >= 0; i--)
{
var plugin = _plugins[i];
if (plugin.Manifest.Status == PluginStatus.Deleted && DeletePlugin(plugin))
{
// See if there is another version, and if so make that active.
ProcessAlternative(plugin);
}
}
// Now load the assemblies..
foreach (var plugin in _plugins)
{
UpdatePluginSuperceedStatus(plugin);
if (plugin.IsEnabledAndSupported == false)
{
_logger.LogInformation("Skipping disabled plugin {Version} of {Name} ", plugin.Version, plugin.Name);
continue;
}
foreach (var file in plugin.DllFiles)
{
Assembly assembly;
try
{
assembly = Assembly.LoadFrom(file);
// This force loads all reference dll's that the plugin uses in the try..catch block.
// Removing this will cause JF to bomb out if referenced dll's cause issues.
assembly.GetExportedTypes();
}
catch (FileLoadException ex)
{
_logger.LogError(ex, "Failed to load assembly {Path}. Disabling plugin.", file);
ChangePluginState(plugin, PluginStatus.Malfunctioned);
continue;
}
_logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, file);
yield return assembly;
}
}
}
/// <summary>
/// Creates all the plugin instances.
/// </summary>
public void CreatePlugins()
{
_ = _appHost.GetExports<IPlugin>(CreatePluginInstance)
.Where(i => i != null)
.ToArray();
}
/// <summary>
/// Registers the plugin's services with the DI.
/// Note: DI is not yet instantiated yet.
/// </summary>
/// <param name="serviceCollection">A <see cref="ServiceCollection"/> instance.</param>
public void RegisterServices(IServiceCollection serviceCollection)
{
foreach (var pluginServiceRegistrator in _appHost.GetExportTypes<IPluginServiceRegistrator>())
{
var plugin = GetPluginByAssembly(pluginServiceRegistrator.Assembly);
if (plugin == null)
{
_logger.LogError("Unable to find plugin in assembly {Assembly}", pluginServiceRegistrator.Assembly.FullName);
continue;
}
UpdatePluginSuperceedStatus(plugin);
if (!plugin.IsEnabledAndSupported)
{
continue;
}
try
{
var instance = (IPluginServiceRegistrator?)Activator.CreateInstance(pluginServiceRegistrator);
instance?.RegisterServices(serviceCollection);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
#pragma warning restore CA1031 // Do not catch general exception types
{
_logger.LogError(ex, "Error registering plugin services from {Assembly}.", pluginServiceRegistrator.Assembly.FullName);
if (ChangePluginState(plugin, PluginStatus.Malfunctioned))
{
_logger.LogInformation("Disabling plugin {Path}", plugin.Path);
}
}
}
}
/// <summary>
/// Imports a plugin manifest from <paramref name="folder"/>.
/// </summary>
/// <param name="folder">Folder of the plugin.</param>
public void ImportPluginFrom(string folder)
{
if (string.IsNullOrEmpty(folder))
{
throw new ArgumentNullException(nameof(folder));
}
// Load the plugin.
var plugin = LoadManifest(folder);
// Make sure we haven't already loaded this.
if (_plugins.Any(p => p.Manifest.Equals(plugin.Manifest)))
{
return;
}
_plugins.Add(plugin);
EnablePlugin(plugin);
}
/// <summary>
/// Removes the plugin reference '<paramref name="plugin"/>.
/// </summary>
/// <param name="plugin">The plugin.</param>
/// <returns>Outcome of the operation.</returns>
public bool RemovePlugin(LocalPlugin plugin)
{
if (plugin == null)
{
throw new ArgumentNullException(nameof(plugin));
}
if (DeletePlugin(plugin))
{
ProcessAlternative(plugin);
return true;
}
_logger.LogWarning("Unable to delete {Path}, so marking as deleteOnStartup.", plugin.Path);
// Unable to delete, so disable.
if (ChangePluginState(plugin, PluginStatus.Deleted))
{
ProcessAlternative(plugin);
return true;
}
return false;
}
/// <summary>
/// Attempts to find the plugin with and id of <paramref name="id"/>.
/// </summary>
/// <param name="id">The <see cref="Guid"/> of plugin.</param>
/// <param name="version">Optional <see cref="Version"/> of the plugin to locate.</param>
/// <returns>A <see cref="LocalPlugin"/> if located, or null if not.</returns>
public LocalPlugin? GetPlugin(Guid id, Version? version = null)
{
LocalPlugin? plugin;
if (version == null)
{
// If no version is given, return the current instance.
var plugins = _plugins.Where(p => p.Id.Equals(id)).ToList();
plugin = plugins.FirstOrDefault(p => p.Instance != null);
if (plugin == null)
{
plugin = plugins.OrderByDescending(p => p.Version).FirstOrDefault();
}
}
else
{
// Match id and version number.
plugin = _plugins.FirstOrDefault(p => p.Id.Equals(id) && p.Version.Equals(version));
}
return plugin;
}
/// <summary>
/// Enables the plugin, disabling all other versions.
/// </summary>
/// <param name="plugin">The <see cref="LocalPlugin"/> of the plug to disable.</param>
public void EnablePlugin(LocalPlugin plugin)
{
if (plugin == null)
{
throw new ArgumentNullException(nameof(plugin));
}
if (ChangePluginState(plugin, PluginStatus.Active))
{
// See if there is another version, and if so, supercede it.
ProcessAlternative(plugin);
}
}
/// <summary>
/// Disable the plugin.
/// </summary>
/// <param name="plugin">The <see cref="LocalPlugin"/> of the plug to disable.</param>
public void DisablePlugin(LocalPlugin plugin)
{
if (plugin == null)
{
throw new ArgumentNullException(nameof(plugin));
}
// Update the manifest on disk
if (ChangePluginState(plugin, PluginStatus.Disabled))
{
// If there is another version, activate it.
ProcessAlternative(plugin);
}
}
/// <summary>
/// Disable the plugin.
/// </summary>
/// <param name="assembly">The <see cref="Assembly"/> of the plug to disable.</param>
public void FailPlugin(Assembly assembly)
{
// Only save if disabled.
if (assembly == null)
{
throw new ArgumentNullException(nameof(assembly));
}
var plugin = _plugins.FirstOrDefault(p => p.DllFiles.Contains(assembly.Location));
if (plugin == null)
{
// A plugin's assembly didn't cause this issue, so ignore it.
return;
}
ChangePluginState(plugin, PluginStatus.Malfunctioned);
}
/// <summary>
/// Saves the manifest back to disk.
/// </summary>
/// <param name="manifest">The <see cref="PluginManifest"/> to save.</param>
/// <param name="path">The path where to save the manifest.</param>
/// <returns>True if successful.</returns>
public bool SaveManifest(PluginManifest manifest, string path)
{
if (manifest == null)
{
return false;
}
try
{
var data = JsonSerializer.Serialize(manifest, _jsonOptions);
File.WriteAllText(Path.Combine(path, "meta.json"), data, Encoding.UTF8);
return true;
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception e)
#pragma warning restore CA1031 // Do not catch general exception types
{
_logger.LogWarning(e, "Unable to save plugin manifest. {Path}", path);
return false;
}
}
/// <summary>
/// Changes a plugin's load status.
/// </summary>
/// <param name="plugin">The <see cref="LocalPlugin"/> instance.</param>
/// <param name="state">The <see cref="PluginStatus"/> of the plugin.</param>
/// <returns>Success of the task.</returns>
private bool ChangePluginState(LocalPlugin plugin, PluginStatus state)
{
if (plugin.Manifest.Status == state || string.IsNullOrEmpty(plugin.Path))
{
// No need to save as the state hasn't changed.
return true;
}
plugin.Manifest.Status = state;
return SaveManifest(plugin.Manifest, plugin.Path);
}
/// <summary>
/// Finds the plugin record using the assembly.
/// </summary>
/// <param name="assembly">The <see cref="Assembly"/> being sought.</param>
/// <returns>The matching record, or null if not found.</returns>
private LocalPlugin? GetPluginByAssembly(Assembly assembly)
{
// Find which plugin it is by the path.
return _plugins.FirstOrDefault(p => string.Equals(p.Path, Path.GetDirectoryName(assembly.Location), StringComparison.Ordinal));
}
/// <summary>
/// Creates the instance safe.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>System.Object.</returns>
private IPlugin? CreatePluginInstance(Type type)
{
// Find the record for this plugin.
var plugin = GetPluginByAssembly(type.Assembly);
if (plugin?.Manifest.Status < PluginStatus.Active)
{
return null;
}
try
{
_logger.LogDebug("Creating instance of {Type}", type);
var instance = (IPlugin)ActivatorUtilities.CreateInstance(_appHost.ServiceProvider, type);
if (plugin == null)
{
// Create a dummy record for the providers.
// TODO: remove this code, if all provided have been released as separate plugins.
plugin = new LocalPlugin(
instance.AssemblyFilePath,
true,
new PluginManifest
{
Id = instance.Id,
Status = PluginStatus.Active,
Name = instance.Name,
Version = instance.Version.ToString()
})
{
Instance = instance
};
_plugins.Add(plugin);
plugin.Manifest.Status = PluginStatus.Active;
}
else
{
plugin.Instance = instance;
var manifest = plugin.Manifest;
var pluginStr = plugin.Instance.Version.ToString();
bool changed = false;
if (string.Equals(manifest.Version, pluginStr, StringComparison.Ordinal))
{
// If a plugin without a manifest failed to load due to an external issue (eg config),
// this updates the manifest to the actual plugin values.
manifest.Version = pluginStr;
manifest.Name = plugin.Instance.Name;
manifest.Description = plugin.Instance.Description;
changed = true;
}
changed = changed || manifest.Status != PluginStatus.Active;
manifest.Status = PluginStatus.Active;
if (changed)
{
SaveManifest(manifest, plugin.Path);
}
}
_logger.LogInformation("Loaded plugin: {PluginName} {PluginVersion}", plugin.Name, plugin.Version);
return instance;
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
#pragma warning restore CA1031 // Do not catch general exception types
{
_logger.LogError(ex, "Error creating {Type}", type.FullName);
if (plugin != null)
{
if (ChangePluginState(plugin, PluginStatus.Malfunctioned))
{
_logger.LogInformation("Plugin {Path} has been disabled.", plugin.Path);
return null;
}
}
_logger.LogDebug("Unable to auto-disable.");
return null;
}
}
private void UpdatePluginSuperceedStatus(LocalPlugin plugin)
{
if (plugin.Manifest.Status != PluginStatus.Superceded)
{
return;
}
var predecessor = _plugins.OrderByDescending(p => p.Version)
.FirstOrDefault(p => p.Id.Equals(plugin.Id) && p.IsEnabledAndSupported && p.Version != plugin.Version);
if (predecessor != null)
{
return;
}
plugin.Manifest.Status = PluginStatus.Active;
}
/// <summary>
/// Attempts to delete a plugin.
/// </summary>
/// <param name="plugin">A <see cref="LocalPlugin"/> instance to delete.</param>
/// <returns>True if successful.</returns>
private bool DeletePlugin(LocalPlugin plugin)
{
// Attempt a cleanup of old folders.
try
{
Directory.Delete(plugin.Path, true);
_logger.LogDebug("Deleted {Path}", plugin.Path);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch
#pragma warning restore CA1031 // Do not catch general exception types
{
return false;
}
return _plugins.Remove(plugin);
}
private LocalPlugin LoadManifest(string dir)
{
Version? version;
PluginManifest? manifest = null;
var metafile = Path.Combine(dir, "meta.json");
if (File.Exists(metafile))
{
try
{
var data = File.ReadAllText(metafile, Encoding.UTF8);
manifest = JsonSerializer.Deserialize<PluginManifest>(data, _jsonOptions);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
#pragma warning restore CA1031 // Do not catch general exception types
{
_logger.LogError(ex, "Error deserializing {Path}.", dir);
}
}
if (manifest != null)
{
if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
{
targetAbi = _minimumVersion;
}
if (!Version.TryParse(manifest.Version, out version))
{
manifest.Version = _minimumVersion.ToString();
}
return new LocalPlugin(dir, _appVersion >= targetAbi, manifest);
}
// No metafile, so lets see if the folder is versioned.
// TODO: Phase this support out in future versions.
metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1];
int versionIndex = dir.LastIndexOf('_');
if (versionIndex != -1)
{
// Get the version number from the filename if possible.
metafile = Path.GetFileName(dir[..versionIndex]) ?? dir[..versionIndex];
version = Version.TryParse(dir.AsSpan()[(versionIndex + 1)..], out Version? parsedVersion) ? parsedVersion : _appVersion;
}
else
{
// Un-versioned folder - Add it under the path name and version it suitable for this instance.
version = _appVersion;
}
// Auto-create a plugin manifest, so we can disable it, if it fails to load.
manifest = new PluginManifest
{
Status = PluginStatus.Restart,
Name = metafile,
AutoUpdate = false,
Id = metafile.GetMD5(),
TargetAbi = _appVersion.ToString(),
Version = version.ToString()
};
return new LocalPlugin(dir, true, manifest);
}
/// <summary>
/// Gets the list of local plugins.
/// </summary>
/// <returns>Enumerable of local plugins.</returns>
private IEnumerable<LocalPlugin> DiscoverPlugins()
{
var versions = new List<LocalPlugin>();
if (!Directory.Exists(_pluginsPath))
{
// Plugin path doesn't exist, don't try to enumerate sub-folders.
return Enumerable.Empty<LocalPlugin>();
}
var directories = Directory.EnumerateDirectories(_pluginsPath, "*.*", SearchOption.TopDirectoryOnly);
foreach (var dir in directories)
{
versions.Add(LoadManifest(dir));
}
string lastName = string.Empty;
versions.Sort(LocalPlugin.Compare);
// Traverse backwards through the list.
// The first item will be the latest version.
for (int x = versions.Count - 1; x >= 0; x--)
{
var entry = versions[x];
if (!string.Equals(lastName, entry.Name, StringComparison.OrdinalIgnoreCase))
{
entry.DllFiles.AddRange(Directory.EnumerateFiles(entry.Path, "*.dll", SearchOption.AllDirectories));
if (entry.IsEnabledAndSupported)
{
lastName = entry.Name;
continue;
}
}
if (string.IsNullOrEmpty(lastName))
{
continue;
}
var manifest = entry.Manifest;
var cleaned = false;
var path = entry.Path;
if (_config.RemoveOldPlugins)
{
// Attempt a cleanup of old folders.
try
{
_logger.LogDebug("Deleting {Path}", path);
Directory.Delete(path, true);
cleaned = true;
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception e)
#pragma warning restore CA1031 // Do not catch general exception types
{
_logger.LogWarning(e, "Unable to delete {Path}", path);
}
if (cleaned)
{
versions.RemoveAt(x);
}
else
{
if (manifest == null)
{
_logger.LogWarning("Unable to disable plugin {Path}", entry.Path);
continue;
}
ChangePluginState(entry, PluginStatus.Deleted);
}
}
}
// Only want plugin folders which have files.
return versions.Where(p => p.DllFiles.Count != 0);
}
/// <summary>
/// Changes the status of the other versions of the plugin to "Superceded".
/// </summary>
/// <param name="plugin">The <see cref="LocalPlugin"/> that's master.</param>
private void ProcessAlternative(LocalPlugin plugin)
{
// Detect whether there is another version of this plugin that needs disabling.
var previousVersion = _plugins.OrderByDescending(p => p.Version)
.FirstOrDefault(
p => p.Id.Equals(plugin.Id)
&& p.IsEnabledAndSupported
&& p.Version != plugin.Version);
if (previousVersion == null)
{
// This value is memory only - so that the web will show restart required.
plugin.Manifest.Status = PluginStatus.Restart;
return;
}
if (plugin.Manifest.Status == PluginStatus.Active && !ChangePluginState(previousVersion, PluginStatus.Superceded))
{
_logger.LogError("Unable to enable version {Version} of {Name}", previousVersion.Version, previousVersion.Name);
}
else if (plugin.Manifest.Status == PluginStatus.Superceded && !ChangePluginState(previousVersion, PluginStatus.Active))
{
_logger.LogError("Unable to supercede version {Version} of {Name}", previousVersion.Version, previousVersion.Name);
}
// This value is memory only - so that the web will show restart required.
plugin.Manifest.Status = PluginStatus.Restart;
}
}
}

View File

@@ -1,60 +0,0 @@
using System;
namespace Emby.Server.Implementations.Plugins
{
/// <summary>
/// Defines a Plugin manifest file.
/// </summary>
public class PluginManifest
{
/// <summary>
/// Gets or sets the category of the plugin.
/// </summary>
public string Category { get; set; }
/// <summary>
/// Gets or sets the changelog information.
/// </summary>
public string Changelog { get; set; }
/// <summary>
/// Gets or sets the description of the plugin.
/// </summary>
public string Description { get; set; }
/// <summary>
/// Gets or sets the Global Unique Identifier for the plugin.
/// </summary>
public Guid Guid { get; set; }
/// <summary>
/// Gets or sets the Name of the plugin.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets an overview of the plugin.
/// </summary>
public string Overview { get; set; }
/// <summary>
/// Gets or sets the owner of the plugin.
/// </summary>
public string Owner { get; set; }
/// <summary>
/// Gets or sets the compatibility version for the plugin.
/// </summary>
public string TargetAbi { get; set; }
/// <summary>
/// Gets or sets the timestamp of the plugin.
/// </summary>
public DateTime Timestamp { get; set; }
/// <summary>
/// Gets or sets the Version number of the plugin.
/// </summary>
public string Version { get; set; }
}
}

View File

@@ -1,5 +1,6 @@
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
@@ -14,6 +15,7 @@ using System.Runtime.InteropServices;
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")]
[assembly: InternalsVisibleTo("Jellyfin.Server.Implementations.Tests")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from

View File

@@ -8,10 +8,10 @@ using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Updates;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
using MediaBrowser.Model.Globalization;
namespace Emby.Server.Implementations.ScheduledTasks
{

View File

@@ -128,6 +128,9 @@ namespace Emby.Server.Implementations.Session
/// <inheritdoc />
public event EventHandler<SessionEventArgs> SessionActivity;
/// <inheritdoc />
public event EventHandler<SessionEventArgs> SessionControllerConnected;
/// <summary>
/// Gets all connections.
/// </summary>
@@ -312,6 +315,19 @@ namespace Emby.Server.Implementations.Session
return session;
}
/// <inheritdoc />
public void OnSessionControllerConnected(SessionInfo info)
{
EventHelper.QueueEventIfNotNull(
SessionControllerConnected,
this,
new SessionEventArgs
{
SessionInfo = info
},
_logger);
}
/// <inheritdoc />
public void CloseIfNeeded(SessionInfo session)
{

View File

@@ -133,6 +133,8 @@ namespace Emby.Server.Implementations.Session
var controller = (WebSocketController)controllerInfo.Item1;
controller.AddWebSocket(connection);
_sessionManager.OnSessionControllerConnected(session);
}
/// <summary>

View File

@@ -41,6 +41,12 @@ namespace Emby.Server.Implementations.SyncPlay
/// </summary>
private readonly ILibraryManager _libraryManager;
/// <summary>
/// The map between users and counter of active sessions.
/// </summary>
private readonly ConcurrentDictionary<Guid, int> _activeUsers =
new ConcurrentDictionary<Guid, int>();
/// <summary>
/// The map between sessions and groups.
/// </summary>
@@ -81,7 +87,7 @@ namespace Emby.Server.Implementations.SyncPlay
_sessionManager = sessionManager;
_libraryManager = libraryManager;
_logger = loggerFactory.CreateLogger<SyncPlayManager>();
_sessionManager.SessionStarted += OnSessionManagerSessionStarted;
_sessionManager.SessionControllerConnected += OnSessionControllerConnected;
}
/// <inheritdoc />
@@ -122,6 +128,7 @@ namespace Emby.Server.Implementations.SyncPlay
throw new InvalidOperationException("Could not add session to group!");
}
UpdateSessionsCounter(session.UserId, 1);
group.CreateGroup(session, request, cancellationToken);
}
}
@@ -172,6 +179,7 @@ namespace Emby.Server.Implementations.SyncPlay
if (existingGroup.GroupId.Equals(request.GroupId))
{
// Restore session.
UpdateSessionsCounter(session.UserId, 1);
group.SessionJoin(session, request, cancellationToken);
return;
}
@@ -185,6 +193,7 @@ namespace Emby.Server.Implementations.SyncPlay
throw new InvalidOperationException("Could not add session to group!");
}
UpdateSessionsCounter(session.UserId, 1);
group.SessionJoin(session, request, cancellationToken);
}
}
@@ -223,6 +232,7 @@ namespace Emby.Server.Implementations.SyncPlay
throw new InvalidOperationException("Could not remove session from group!");
}
UpdateSessionsCounter(session.UserId, -1);
group.SessionLeave(session, request, cancellationToken);
if (group.IsGroupEmpty())
@@ -318,6 +328,19 @@ namespace Emby.Server.Implementations.SyncPlay
}
}
/// <inheritdoc />
public bool IsUserActive(Guid userId)
{
if (_activeUsers.TryGetValue(userId, out var sessionsCounter))
{
return sessionsCounter > 0;
}
else
{
return false;
}
}
/// <summary>
/// Releases unmanaged and optionally managed resources.
/// </summary>
@@ -329,11 +352,11 @@ namespace Emby.Server.Implementations.SyncPlay
return;
}
_sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
_sessionManager.SessionControllerConnected -= OnSessionControllerConnected;
_disposed = true;
}
private void OnSessionManagerSessionStarted(object sender, SessionEventArgs e)
private void OnSessionControllerConnected(object sender, SessionEventArgs e)
{
var session = e.SessionInfo;
@@ -343,5 +366,26 @@ namespace Emby.Server.Implementations.SyncPlay
JoinGroup(session, request, CancellationToken.None);
}
}
private void UpdateSessionsCounter(Guid userId, int toAdd)
{
// Update sessions counter.
var newSessionsCounter = _activeUsers.AddOrUpdate(
userId,
1,
(key, sessionsCounter) => sessionsCounter + toAdd);
// Should never happen.
if (newSessionsCounter < 0)
{
throw new InvalidOperationException("Sessions counter is negative!");
}
// Clean record if user has no more active sessions.
if (newSessionsCounter == 0)
{
_activeUsers.TryRemove(new KeyValuePair<Guid, int>(userId, newSessionsCounter));
}
}
}
}

View File

@@ -75,8 +75,7 @@ namespace Emby.Server.Implementations.TV
{
parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
.Where(i => i is Folder)
.Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes)
.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
.Where(i => !user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes).Contains(i.Id))
.ToArray();
}

View File

@@ -1,4 +1,4 @@
#pragma warning disable CS1591
#nullable enable
using System;
using System.Collections.Concurrent;
@@ -12,7 +12,6 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
@@ -41,17 +40,15 @@ namespace Emby.Server.Implementations.Updates
private readonly IEventManager _eventManager;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
private readonly JsonSerializerOptions _jsonSerializerOptions;
private readonly IPluginManager _pluginManager;
/// <summary>
/// Gets the application host.
/// </summary>
/// <value>The application host.</value>
private readonly IServerApplicationHost _applicationHost;
private readonly IZipClient _zipClient;
private readonly object _currentInstallationsLock = new object();
/// <summary>
@@ -64,6 +61,17 @@ namespace Emby.Server.Implementations.Updates
/// </summary>
private readonly ConcurrentBag<InstallationInfo> _completedInstallationsInternal;
/// <summary>
/// Initializes a new instance of the <see cref="InstallationManager"/> class.
/// </summary>
/// <param name="logger">The <see cref="ILogger{InstallationManager}"/>.</param>
/// <param name="appHost">The <see cref="IServerApplicationHost"/>.</param>
/// <param name="appPaths">The <see cref="IApplicationPaths"/>.</param>
/// <param name="eventManager">The <see cref="IEventManager"/>.</param>
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param>
/// <param name="config">The <see cref="IServerConfigurationManager"/>.</param>
/// <param name="zipClient">The <see cref="IZipClient"/>.</param>
/// <param name="pluginManager">The <see cref="IPluginManager"/>.</param>
public InstallationManager(
ILogger<InstallationManager> logger,
IServerApplicationHost appHost,
@@ -71,8 +79,8 @@ namespace Emby.Server.Implementations.Updates
IEventManager eventManager,
IHttpClientFactory httpClientFactory,
IServerConfigurationManager config,
IFileSystem fileSystem,
IZipClient zipClient)
IZipClient zipClient,
IPluginManager pluginManager)
{
_currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>();
_completedInstallationsInternal = new ConcurrentBag<InstallationInfo>();
@@ -83,38 +91,65 @@ namespace Emby.Server.Implementations.Updates
_eventManager = eventManager;
_httpClientFactory = httpClientFactory;
_config = config;
_fileSystem = fileSystem;
_zipClient = zipClient;
_jsonSerializerOptions = JsonDefaults.GetOptions();
_pluginManager = pluginManager;
}
/// <inheritdoc />
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
/// <inheritdoc />
public async Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, CancellationToken cancellationToken = default)
public async Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default)
{
try
{
var packages = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetFromJsonAsync<List<PackageInfo>>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
List<PackageInfo>? packages = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetFromJsonAsync<List<PackageInfo>>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
if (packages == null)
{
return Array.Empty<PackageInfo>();
}
var minimumVersion = new Version(0, 0, 0, 1);
// Store the repository and repository url with each version, as they may be spread apart.
foreach (var entry in packages)
{
foreach (var ver in entry.versions)
for (int a = entry.Versions.Count - 1; a >= 0; a--)
{
ver.repositoryName = manifestName;
ver.repositoryUrl = manifest;
var ver = entry.Versions[a];
ver.RepositoryName = manifestName;
ver.RepositoryUrl = manifest;
if (!filterIncompatible)
{
continue;
}
if (!Version.TryParse(ver.TargetAbi, out var targetAbi))
{
targetAbi = minimumVersion;
}
// Only show plugins that are greater than or equal to targetAbi.
if (_applicationHost.ApplicationVersion >= targetAbi)
{
continue;
}
// Not compatible with this version so remove it.
entry.Versions.Remove(ver);
}
}
return packages;
}
catch (IOException ex)
{
_logger.LogError(ex, "Cannot locate the plugin manifest {Manifest}", manifest);
return Array.Empty<PackageInfo>();
}
catch (JsonException ex)
{
_logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest);
@@ -132,69 +167,58 @@ namespace Emby.Server.Implementations.Updates
}
}
private static void MergeSort(IList<VersionInfo> source, IList<VersionInfo> dest)
{
int sLength = source.Count - 1;
int dLength = dest.Count;
int s = 0, d = 0;
var sourceVersion = source[0].VersionNumber;
var destVersion = dest[0].VersionNumber;
while (d < dLength)
{
if (sourceVersion.CompareTo(destVersion) >= 0)
{
if (s < sLength)
{
sourceVersion = source[++s].VersionNumber;
}
else
{
// Append all of destination to the end of source.
while (d < dLength)
{
source.Add(dest[d++]);
}
break;
}
}
else
{
source.Insert(s++, dest[d++]);
if (d >= dLength)
{
break;
}
sLength++;
destVersion = dest[d].VersionNumber;
}
}
}
/// <inheritdoc />
public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default)
{
var result = new List<PackageInfo>();
foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories)
{
if (repository.Enabled)
if (repository.Enabled && repository.Url != null)
{
// Where repositories have the same content, the details of the first is taken.
foreach (var package in await GetPackages(repository.Name, repository.Url, cancellationToken).ConfigureAwait(true))
// Where repositories have the same content, the details from the first is taken.
foreach (var package in await GetPackages(repository.Name ?? "Unnamed Repo", repository.Url, true, cancellationToken).ConfigureAwait(true))
{
if (!Guid.TryParse(package.guid, out var packageGuid))
if (!Guid.TryParse(package.Id, out var packageGuid))
{
// Package doesn't have a valid GUID, skip.
continue;
}
var existing = FilterPackages(result, package.name, packageGuid).FirstOrDefault();
var existing = FilterPackages(result, package.Name, packageGuid).FirstOrDefault();
// Remove invalid versions from the valid package.
for (var i = package.Versions.Count - 1; i >= 0; i--)
{
var version = package.Versions[i];
var plugin = _pluginManager.GetPlugin(packageGuid, version.VersionNumber);
// Update the manifests, if anything changes.
if (plugin != null)
{
if (!string.Equals(plugin.Manifest.TargetAbi, version.TargetAbi, StringComparison.Ordinal))
{
plugin.Manifest.TargetAbi = version.TargetAbi ?? string.Empty;
_pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
}
}
// Remove versions with a target abi that is greater then the current application version.
if (Version.TryParse(version.TargetAbi, out var targetAbi) && _applicationHost.ApplicationVersion < targetAbi)
{
package.Versions.RemoveAt(i);
}
}
// Don't add a package that doesn't have any compatible versions.
if (package.Versions.Count == 0)
{
continue;
}
if (existing != null)
{
// Assumption is both lists are ordered, so slot these into the correct place.
MergeSort(existing.versions, package.versions);
MergeSortedList(existing.Versions, package.Versions);
}
else
{
@@ -210,23 +234,23 @@ namespace Emby.Server.Implementations.Updates
/// <inheritdoc />
public IEnumerable<PackageInfo> FilterPackages(
IEnumerable<PackageInfo> availablePackages,
string name = null,
Guid guid = default,
Version specificVersion = null)
string? name = null,
Guid? id = default,
Version? specificVersion = null)
{
if (name != null)
{
availablePackages = availablePackages.Where(x => x.name.Equals(name, StringComparison.OrdinalIgnoreCase));
availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
if (guid != Guid.Empty)
if (id != Guid.Empty)
{
availablePackages = availablePackages.Where(x => Guid.Parse(x.guid) == guid);
availablePackages = availablePackages.Where(x => Guid.Parse(x.Id) == id);
}
if (specificVersion != null)
{
availablePackages = availablePackages.Where(x => x.versions.Where(y => y.VersionNumber.Equals(specificVersion)).Any());
availablePackages = availablePackages.Where(x => x.Versions.Any(y => y.VersionNumber.Equals(specificVersion)));
}
return availablePackages;
@@ -235,12 +259,12 @@ namespace Emby.Server.Implementations.Updates
/// <inheritdoc />
public IEnumerable<InstallationInfo> GetCompatibleVersions(
IEnumerable<PackageInfo> availablePackages,
string name = null,
Guid guid = default,
Version minVersion = null,
Version specificVersion = null)
string? name = null,
Guid? id = default,
Version? minVersion = null,
Version? specificVersion = null)
{
var package = FilterPackages(availablePackages, name, guid, specificVersion).FirstOrDefault();
var package = FilterPackages(availablePackages, name, id, specificVersion).FirstOrDefault();
// Package not found in repository
if (package == null)
@@ -249,8 +273,8 @@ namespace Emby.Server.Implementations.Updates
}
var appVer = _applicationHost.ApplicationVersion;
var availableVersions = package.versions
.Where(x => Version.Parse(x.targetAbi) <= appVer);
var availableVersions = package.Versions
.Where(x => string.IsNullOrEmpty(x.TargetAbi) || Version.Parse(x.TargetAbi) <= appVer);
if (specificVersion != null)
{
@@ -265,12 +289,12 @@ namespace Emby.Server.Implementations.Updates
{
yield return new InstallationInfo
{
Changelog = v.changelog,
Guid = new Guid(package.guid),
Name = package.name,
Changelog = v.Changelog,
Id = new Guid(package.Id),
Name = package.Name,
Version = v.VersionNumber,
SourceUrl = v.sourceUrl,
Checksum = v.checksum
SourceUrl = v.SourceUrl,
Checksum = v.Checksum
};
}
}
@@ -282,20 +306,6 @@ namespace Emby.Server.Implementations.Updates
return GetAvailablePluginUpdates(catalog);
}
private IEnumerable<InstallationInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
{
var plugins = _applicationHost.GetLocalPlugins(_appPaths.PluginsPath);
foreach (var plugin in plugins)
{
var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version);
var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version);
if (version != null && CompletedInstallations.All(x => x.Guid != version.Guid))
{
yield return version;
}
}
}
/// <inheritdoc />
public async Task InstallPackage(InstallationInfo package, CancellationToken cancellationToken)
{
@@ -372,143 +382,29 @@ namespace Emby.Server.Implementations.Updates
}
}
/// <summary>
/// Installs the package internal.
/// </summary>
/// <param name="package">The package.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns><see cref="Task" />.</returns>
private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
{
// Set last update time if we were installed before
IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => p.Id == package.Guid)
?? _applicationHost.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase));
// Do the install
await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
// Do plugin-specific processing
_logger.LogInformation(plugin == null ? "New plugin installed: {0} {1}" : "Plugin updated: {0} {1}", package.Name, package.Version);
return plugin != null;
}
private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken)
{
var extension = Path.GetExtension(package.SourceUrl);
if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
{
_logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl);
return;
}
// Always override the passed-in target (which is a file) and figure it out again
string targetDir = Path.Combine(_appPaths.PluginsPath, package.Name);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(new Uri(package.SourceUrl), cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
// CA5351: Do Not Use Broken Cryptographic Algorithms
#pragma warning disable CA5351
using var md5 = MD5.Create();
cancellationToken.ThrowIfCancellationRequested();
var hash = Convert.ToHexString(md5.ComputeHash(stream));
if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase))
{
_logger.LogError(
"The checksums didn't match while installing {Package}, expected: {Expected}, got: {Received}",
package.Name,
package.Checksum,
hash);
throw new InvalidDataException("The checksum of the received data doesn't match.");
}
// Version folder as they cannot be overwritten in Windows.
targetDir += "_" + package.Version;
if (Directory.Exists(targetDir))
{
try
{
Directory.Delete(targetDir, true);
}
catch
{
// Ignore any exceptions.
}
}
stream.Position = 0;
_zipClient.ExtractAllFromZip(stream, targetDir, true);
#pragma warning restore CA5351
}
/// <summary>
/// Uninstalls a plugin.
/// </summary>
/// <param name="plugin">The plugin.</param>
public void UninstallPlugin(IPlugin plugin)
/// <param name="plugin">The <see cref="LocalPlugin"/> to uninstall.</param>
public void UninstallPlugin(LocalPlugin plugin)
{
if (!plugin.CanUninstall)
if (plugin == null)
{
_logger.LogWarning("Attempt to delete non removable plugin {0}, ignoring request", plugin.Name);
return;
}
plugin.OnUninstalling();
if (plugin.Instance?.CanUninstall == false)
{
_logger.LogWarning("Attempt to delete non removable plugin {PluginName}, ignoring request", plugin.Name);
return;
}
plugin.Instance?.OnUninstalling();
// Remove it the quick way for now
_applicationHost.RemovePlugin(plugin);
_pluginManager.RemovePlugin(plugin);
var path = plugin.AssemblyFilePath;
bool isDirectory = false;
// Check if we have a plugin directory we should remove too
if (Path.GetDirectoryName(plugin.AssemblyFilePath) != _appPaths.PluginsPath)
{
path = Path.GetDirectoryName(plugin.AssemblyFilePath);
isDirectory = true;
}
// Make this case-insensitive to account for possible incorrect assembly naming
var file = _fileSystem.GetFilePaths(Path.GetDirectoryName(path))
.FirstOrDefault(i => string.Equals(i, path, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(file))
{
path = file;
}
try
{
if (isDirectory)
{
_logger.LogInformation("Deleting plugin directory {0}", path);
Directory.Delete(path, true);
}
else
{
_logger.LogInformation("Deleting plugin file {0}", path);
_fileSystem.DeleteFile(path);
}
}
catch
{
// Ignore file errors.
}
var list = _config.Configuration.UninstalledPlugins.ToList();
var filename = Path.GetFileName(path);
if (!list.Contains(filename, StringComparer.OrdinalIgnoreCase))
{
list.Add(filename);
_config.Configuration.UninstalledPlugins = list.ToArray();
_config.SaveConfiguration();
}
_eventManager.Publish(new PluginUninstalledEventArgs(plugin));
_eventManager.Publish(new PluginUninstalledEventArgs(plugin.GetPluginInfo()));
_applicationHost.NotifyPendingRestart();
}
@@ -518,7 +414,7 @@ namespace Emby.Server.Implementations.Updates
{
lock (_currentInstallationsLock)
{
var install = _currentInstallations.Find(x => x.info.Guid == id);
var install = _currentInstallations.Find(x => x.info.Id == id);
if (install == default((InstallationInfo, CancellationTokenSource)))
{
return false;
@@ -547,14 +443,155 @@ namespace Emby.Server.Implementations.Updates
{
lock (_currentInstallationsLock)
{
foreach (var tuple in _currentInstallations)
foreach (var (info, token) in _currentInstallations)
{
tuple.token.Dispose();
token.Dispose();
}
_currentInstallations.Clear();
}
}
}
/// <summary>
/// Merges two sorted lists.
/// </summary>
/// <param name="source">The source <see cref="IList{VersionInfo}"/> instance to merge.</param>
/// <param name="dest">The destination <see cref="IList{VersionInfo}"/> instance to merge with.</param>
private static void MergeSortedList(IList<VersionInfo> source, IList<VersionInfo> dest)
{
int sLength = source.Count - 1;
int dLength = dest.Count;
int s = 0, d = 0;
var sourceVersion = source[0].VersionNumber;
var destVersion = dest[0].VersionNumber;
while (d < dLength)
{
if (sourceVersion.CompareTo(destVersion) >= 0)
{
if (s < sLength)
{
sourceVersion = source[++s].VersionNumber;
}
else
{
// Append all of destination to the end of source.
while (d < dLength)
{
source.Add(dest[d++]);
}
break;
}
}
else
{
source.Insert(s++, dest[d++]);
if (d >= dLength)
{
break;
}
sLength++;
destVersion = dest[d].VersionNumber;
}
}
}
private IEnumerable<InstallationInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
{
var plugins = _pluginManager.Plugins;
foreach (var plugin in plugins)
{
if (plugin.Manifest?.AutoUpdate == false)
{
continue;
}
var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version);
var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version);
if (version != null && CompletedInstallations.All(x => x.Id != version.Id))
{
yield return version;
}
}
}
private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken)
{
var extension = Path.GetExtension(package.SourceUrl);
if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
{
_logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl);
return;
}
// Always override the passed-in target (which is a file) and figure it out again
string targetDir = Path.Combine(_appPaths.PluginsPath, package.Name);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(new Uri(package.SourceUrl), cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
// CA5351: Do Not Use Broken Cryptographic Algorithms
#pragma warning disable CA5351
using var md5 = MD5.Create();
cancellationToken.ThrowIfCancellationRequested();
var hash = Convert.ToHexString(md5.ComputeHash(stream));
if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase))
{
_logger.LogError(
"The checksums didn't match while installing {Package}, expected: {Expected}, got: {Received}",
package.Name,
package.Checksum,
hash);
throw new InvalidDataException("The checksum of the received data doesn't match.");
}
// Version folder as they cannot be overwritten in Windows.
targetDir += "_" + package.Version;
if (Directory.Exists(targetDir))
{
try
{
Directory.Delete(targetDir, true);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch
#pragma warning restore CA1031 // Do not catch general exception types
{
// Ignore any exceptions.
}
}
stream.Position = 0;
_zipClient.ExtractAllFromZip(stream, targetDir, true);
_pluginManager.ImportPluginFrom(targetDir);
}
private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
{
// Set last update time if we were installed before
LocalPlugin? plugin = _pluginManager.Plugins.FirstOrDefault(p => p.Id.Equals(package.Id) && p.Version.Equals(package.Version))
?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase) && p.Version.Equals(package.Version));
if (plugin != null)
{
plugin.Manifest.Timestamp = DateTime.UtcNow;
_pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
}
// Do the install
await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
// Do plugin-specific processing
_logger.LogInformation(plugin == null ? "New plugin installed: {PluginName} {PluginVersion}" : "Plugin updated: {PluginName} {PluginVersion}", package.Name, package.Version);
return plugin != null;
}
}
}

View File

@@ -3,6 +3,7 @@ using Jellyfin.Api.Helpers;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.SyncPlay;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -13,20 +14,24 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
/// </summary>
public class SyncPlayAccessHandler : BaseAuthorizationHandler<SyncPlayAccessRequirement>
{
private readonly ISyncPlayManager _syncPlayManager;
private readonly IUserManager _userManager;
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayAccessHandler"/> class.
/// </summary>
/// <param name="syncPlayManager">Instance of the <see cref="ISyncPlayManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public SyncPlayAccessHandler(
ISyncPlayManager syncPlayManager,
IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
: base(userManager, networkManager, httpContextAccessor)
{
_syncPlayManager = syncPlayManager;
_userManager = userManager;
}
@@ -42,10 +47,52 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
var userId = ClaimHelpers.GetUserId(context.User);
var user = _userManager.GetUserById(userId!.Value);
if ((requirement.RequiredAccess.HasValue && user.SyncPlayAccess == requirement.RequiredAccess)
|| user.SyncPlayAccess == SyncPlayAccess.CreateAndJoinGroups)
if (requirement.RequiredAccess == SyncPlayAccessRequirementType.HasAccess)
{
context.Succeed(requirement);
if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups
|| user.SyncPlayAccess == SyncPlayUserAccessType.JoinGroups
|| _syncPlayManager.IsUserActive(userId!.Value))
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.CreateGroup)
{
if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.JoinGroup)
{
if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups
|| user.SyncPlayAccess == SyncPlayUserAccessType.JoinGroups)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.IsInGroup)
{
if (_syncPlayManager.IsUserActive(userId!.Value))
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
else
{

View File

@@ -11,23 +11,15 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class.
/// </summary>
/// <param name="requiredAccess">A value of <see cref="SyncPlayAccess"/>.</param>
public SyncPlayAccessRequirement(SyncPlayAccess requiredAccess)
/// <param name="requiredAccess">A value of <see cref="SyncPlayAccessRequirementType"/>.</param>
public SyncPlayAccessRequirement(SyncPlayAccessRequirementType requiredAccess)
{
RequiredAccess = requiredAccess;
}
/// <summary>
/// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class.
/// </summary>
public SyncPlayAccessRequirement()
{
RequiredAccess = null;
}
/// <summary>
/// Gets the required SyncPlay access.
/// </summary>
public SyncPlayAccess? RequiredAccess { get; }
public SyncPlayAccessRequirementType RequiredAccess { get; }
}
}

View File

@@ -51,13 +51,23 @@ namespace Jellyfin.Api.Constants
public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl";
/// <summary>
/// Policy name for requiring access to SyncPlay.
/// Policy name for accessing SyncPlay.
/// </summary>
public const string SyncPlayAccess = "SyncPlayAccess";
public const string SyncPlayHasAccess = "SyncPlayHasAccess";
/// <summary>
/// Policy name for requiring group creation access to SyncPlay.
/// Policy name for creating a SyncPlay group.
/// </summary>
public const string SyncPlayCreateGroupAccess = "SyncPlayCreateGroupAccess";
public const string SyncPlayCreateGroup = "SyncPlayCreateGroup";
/// <summary>
/// Policy name for joining a SyncPlay group.
/// </summary>
public const string SyncPlayJoinGroup = "SyncPlayJoinGroup";
/// <summary>
/// Policy name for accessing a SyncPlay group.
/// </summary>
public const string SyncPlayIsInGroup = "SyncPlayIsInGroup";
}
}

View File

@@ -29,18 +29,22 @@ namespace Jellyfin.Api.Controllers
{
private readonly ILogger<DashboardController> _logger;
private readonly IServerApplicationHost _appHost;
private readonly IPluginManager _pluginManager;
/// <summary>
/// Initializes a new instance of the <see cref="DashboardController"/> class.
/// </summary>
/// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
/// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
/// <param name="pluginManager">Instance of <see cref="IPluginManager"/> interface.</param>
public DashboardController(
ILogger<DashboardController> logger,
IServerApplicationHost appHost)
IServerApplicationHost appHost,
IPluginManager pluginManager)
{
_logger = logger;
_appHost = appHost;
_pluginManager = pluginManager;
}
/// <summary>
@@ -83,7 +87,7 @@ namespace Jellyfin.Api.Controllers
.Where(i => i != null)
.ToList();
configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages));
configPages.AddRange(_pluginManager.Plugins.SelectMany(GetConfigPages));
if (pageType.HasValue)
{
@@ -155,24 +159,24 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
private IEnumerable<ConfigurationPageInfo> GetConfigPages(IPlugin plugin)
private IEnumerable<ConfigurationPageInfo> GetConfigPages(LocalPlugin plugin)
{
return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1));
return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1));
}
private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(IPlugin plugin)
private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(LocalPlugin? plugin)
{
if (!(plugin is IHasWebPages hasWebPages))
if (plugin?.Instance is not IHasWebPages hasWebPages)
{
return new List<Tuple<PluginPageInfo, IPlugin>>();
}
return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin));
return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin.Instance));
}
private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages()
{
return _appHost.Plugins.SelectMany(GetPluginPages);
return _pluginManager.Plugins.SelectMany(GetPluginPages);
}
}
}

View File

@@ -12,6 +12,7 @@ using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Controllers
{
@@ -22,14 +23,17 @@ namespace Jellyfin.Api.Controllers
public class DisplayPreferencesController : BaseJellyfinApiController
{
private readonly IDisplayPreferencesManager _displayPreferencesManager;
private readonly ILogger<DisplayPreferencesController> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class.
/// </summary>
/// <param name="displayPreferencesManager">Instance of <see cref="IDisplayPreferencesManager"/> interface.</param>
public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager)
/// <param name="logger">Instance of <see cref="ILogger{DisplayPreferencesController}"/> interface.</param>
public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager, ILogger<DisplayPreferencesController> logger)
{
_displayPreferencesManager = displayPreferencesManager;
_logger = logger;
}
/// <summary>
@@ -61,7 +65,6 @@ namespace Jellyfin.Api.Controllers
{
Client = displayPreferences.Client,
Id = displayPreferences.ItemId.ToString(),
ViewType = itemPreferences.ViewType.ToString(),
SortBy = itemPreferences.SortBy,
SortOrder = itemPreferences.SortOrder,
IndexBy = displayPreferences.IndexBy?.ToString(),
@@ -77,16 +80,12 @@ namespace Jellyfin.Api.Controllers
dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant();
}
foreach (var itemDisplayPreferences in _displayPreferencesManager.ListItemDisplayPreferences(displayPreferences.UserId, displayPreferences.Client))
{
dto.CustomPrefs["landing-" + itemDisplayPreferences.ItemId] = itemDisplayPreferences.ViewType.ToString().ToLowerInvariant();
}
dto.CustomPrefs["chromecastVersion"] = displayPreferences.ChromecastVersion.ToString().ToLowerInvariant();
dto.CustomPrefs["skipForwardLength"] = displayPreferences.SkipForwardLength.ToString(CultureInfo.InvariantCulture);
dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString(CultureInfo.InvariantCulture);
dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture);
dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
dto.CustomPrefs["dashboardTheme"] = displayPreferences.DashboardTheme;
// Load all custom display preferences
var customDisplayPreferences = _displayPreferencesManager.ListCustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);
@@ -189,10 +188,9 @@ namespace Jellyfin.Api.Controllers
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase)))
{
if (Guid.TryParse(key.AsSpan().Slice("landing-".Length), out var preferenceId))
if (!Enum.TryParse<ViewType>(displayPreferences.CustomPrefs[key], true, out var type))
{
var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, preferenceId, existingDisplayPreferences.Client);
itemPreferences.ViewType = Enum.Parse<ViewType>(displayPreferences.ViewType);
_logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]);
displayPreferences.CustomPrefs.Remove(key);
}
}
@@ -204,11 +202,6 @@ namespace Jellyfin.Api.Controllers
itemPrefs.RememberSorting = displayPreferences.RememberSorting;
itemPrefs.ItemId = itemId;
if (Enum.TryParse<ViewType>(displayPreferences.ViewType, true, out var viewType))
{
itemPrefs.ViewType = viewType;
}
// Set all remaining custom preferences.
_displayPreferencesManager.SetCustomItemDisplayPreferences(userId, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs);
_displayPreferencesManager.SaveChanges();

View File

@@ -41,18 +41,25 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="serverId">Server UUID.</param>
/// <response code="200">Description xml returned.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>An <see cref="OkResult"/> containing the description xml.</returns>
[HttpGet("{serverId}/description")]
[HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult GetDescriptionXml([FromRoute, Required] string serverId)
{
var url = GetAbsoluteUri();
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress);
return Ok(xml);
if (DlnaEntryPoint.Enabled)
{
var url = GetAbsoluteUri();
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress);
return Ok(xml);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
/// <summary>
@@ -60,17 +67,24 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="serverId">Server UUID.</param>
/// <response code="200">Dlna content directory returned.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns>
[HttpGet("{serverId}/ContentDirectory")]
[HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")]
[HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetContentDirectory([FromRoute, Required] string serverId)
{
return Ok(_contentDirectory.GetServiceXml());
if (DlnaEntryPoint.Enabled)
{
return Ok(_contentDirectory.GetServiceXml());
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
/// <summary>
@@ -78,17 +92,24 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="serverId">Server UUID.</param>
/// <response code="200">Dlna media receiver registrar xml returned.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Dlna media receiver registrar xml.</returns>
[HttpGet("{serverId}/MediaReceiverRegistrar")]
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")]
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
{
return Ok(_mediaReceiverRegistrar.GetServiceXml());
if (DlnaEntryPoint.Enabled)
{
return Ok(_mediaReceiverRegistrar.GetServiceXml());
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
/// <summary>
@@ -96,17 +117,24 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="serverId">Server UUID.</param>
/// <response code="200">Dlna media receiver registrar xml returned.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Dlna media receiver registrar xml.</returns>
[HttpGet("{serverId}/ConnectionManager")]
[HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")]
[HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
public ActionResult GetConnectionManager([FromRoute, Required] string serverId)
{
return Ok(_connectionManager.GetServiceXml());
if (DlnaEntryPoint.Enabled)
{
return Ok(_connectionManager.GetServiceXml());
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
/// <summary>
@@ -114,14 +142,21 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Control response.</returns>
[HttpPost("{serverId}/ContentDirectory/Control")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId)
{
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
if (DlnaEntryPoint.Enabled)
{
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
/// <summary>
@@ -129,14 +164,21 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Control response.</returns>
[HttpPost("{serverId}/ConnectionManager/Control")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId)
{
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
if (DlnaEntryPoint.Enabled)
{
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
/// <summary>
@@ -144,14 +186,21 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Control response.</returns>
[HttpPost("{serverId}/MediaReceiverRegistrar/Control")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId)
{
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
if (DlnaEntryPoint.Enabled)
{
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
/// <summary>
@@ -159,17 +208,24 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Event subscription response.</returns>
[HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")]
[HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")]
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId)
{
return ProcessEventRequest(_mediaReceiverRegistrar);
if (DlnaEntryPoint.Enabled)
{
return ProcessEventRequest(_mediaReceiverRegistrar);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
/// <summary>
@@ -177,17 +233,24 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Event subscription response.</returns>
[HttpSubscribe("{serverId}/ContentDirectory/Events")]
[HttpUnsubscribe("{serverId}/ContentDirectory/Events")]
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId)
{
return ProcessEventRequest(_contentDirectory);
if (DlnaEntryPoint.Enabled)
{
return ProcessEventRequest(_contentDirectory);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
/// <summary>
@@ -195,17 +258,24 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Event subscription response.</returns>
[HttpSubscribe("{serverId}/ConnectionManager/Events")]
[HttpUnsubscribe("{serverId}/ConnectionManager/Events")]
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId)
{
return ProcessEventRequest(_connectionManager);
if (DlnaEntryPoint.Enabled)
{
return ProcessEventRequest(_connectionManager);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
/// <summary>
@@ -213,14 +283,24 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="serverId">Server UUID.</param>
/// <param name="fileName">The icon filename.</param>
/// <response code="200">Request processed.</response>
/// <response code="404">Not Found.</response>
/// <response code="503">DLNA is disabled.</response>
/// <returns>Icon stream.</returns>
[HttpGet("{serverId}/icons/{fileName}")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[ProducesImageFile]
public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName)
{
return GetIconInternal(fileName);
if (DlnaEntryPoint.Enabled)
{
return GetIconInternal(fileName);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
/// <summary>
@@ -228,11 +308,22 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="fileName">The icon filename.</param>
/// <returns>Icon stream.</returns>
/// <response code="200">Request processed.</response>
/// <response code="404">Not Found.</response>
/// <response code="503">DLNA is disabled.</response>
[HttpGet("icons/{fileName}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
[ProducesImageFile]
public ActionResult GetIcon([FromRoute, Required] string fileName)
{
return GetIconInternal(fileName);
if (DlnaEntryPoint.Enabled)
{
return GetIconInternal(fileName);
}
return StatusCode(StatusCodes.Status503ServiceUnavailable);
}
private ActionResult GetIconInternal(string fileName)

View File

@@ -98,7 +98,7 @@ namespace Jellyfin.Api.Controllers
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{
return Forbid("User is not allowed to update the image.");
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
}
var user = _userManager.GetUserById(userId);
@@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{
return Forbid("User is not allowed to update the image.");
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the image.");
}
var user = _userManager.GetUserById(userId);
@@ -190,7 +190,7 @@ namespace Jellyfin.Api.Controllers
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{
return Forbid("User is not allowed to delete the image.");
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
}
var user = _userManager.GetUserById(userId);
@@ -229,7 +229,7 @@ namespace Jellyfin.Api.Controllers
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{
return Forbid("User is not allowed to delete the image.");
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to delete the image.");
}
var user = _userManager.GetUserById(userId);
@@ -325,9 +325,11 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
await _providerManager.SaveImage(item, Request.Body, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return NoContent();
@@ -358,9 +360,11 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
// Handle image/png; charset=utf-8
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
await _providerManager.SaveImage(item, Request.Body, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return NoContent();

View File

@@ -254,18 +254,18 @@ namespace Jellyfin.Api.Controllers
includeItemTypes = new[] { "Playlist" };
}
bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id)
var enabledChannels = user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels);
bool isInEnabledFolder = Array.IndexOf(user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders), item.Id) != -1
// Assume all folders inside an EnabledChannel are enabled
|| user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id)
|| Array.IndexOf(enabledChannels, item.Id) != -1
// Assume all items inside an EnabledChannel are enabled
|| user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.ChannelId);
|| Array.IndexOf(enabledChannels, item.ChannelId) != -1;
var collectionFolders = _libraryManager.GetCollectionFolders(item);
foreach (var collectionFolder in collectionFolders)
{
if (user.GetPreference(PreferenceKind.EnabledFolders).Contains(
collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
StringComparer.OrdinalIgnoreCase))
if (user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(collectionFolder.Id))
{
isInEnabledFolder = true;
}
@@ -786,12 +786,12 @@ namespace Jellyfin.Api.Controllers
var ancestorIds = Array.Empty<Guid>();
var excludeFolderIds = user.GetPreference(PreferenceKind.LatestItemExcludes);
var excludeFolderIds = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes);
if (parentIdGuid.Equals(Guid.Empty) && excludeFolderIds.Length > 0)
{
ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true)
.Where(i => i is Folder)
.Where(i => !excludeFolderIds.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
.Where(i => !excludeFolderIds.Contains(i.Id))
.Select(i => i.Id)
.ToArray();
}

View File

@@ -667,7 +667,7 @@ namespace Jellyfin.Api.Controllers
}
// TODO determine non-ASCII validity.
return PhysicalFile(path, MimeTypes.GetMimeType(path));
return PhysicalFile(path, MimeTypes.GetMimeType(path), filename);
}
/// <summary>
@@ -742,8 +742,6 @@ namespace Jellyfin.Api.Controllers
{
Limit = limit,
IncludeItemTypes = includeItemTypes.ToArray(),
IsMovie = isMovie,
IsSeries = isSeries,
SimilarTo = item,
DtoOptions = dtoOptions,
EnableTotalRecordCount = !isMovie ?? true,

View File

@@ -1119,20 +1119,15 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Set channel mappings.
/// </summary>
/// <param name="providerId">Provider id.</param>
/// <param name="tunerChannelId">Tuner channel id.</param>
/// <param name="providerChannelId">Provider channel id.</param>
/// <param name="setChannelMappingDto">The set channel mapping dto.</param>
/// <response code="200">Created channel mapping returned.</response>
/// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
[HttpPost("ChannelMappings")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping(
[FromQuery] string? providerId,
[FromQuery] string? tunerChannelId,
[FromQuery] string? providerChannelId)
public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto)
{
return await _liveTvManager.SetChannelMapping(providerId, tunerChannelId, providerChannelId).ConfigureAwait(false);
return await _liveTvManager.SetChannelMapping(setChannelMappingDto.ProviderId, setChannelMappingDto.TunerChannelId, setChannelMappingDto.ProviderChannelId).ConfigureAwait(false);
}
/// <summary>

View File

@@ -17,6 +17,7 @@ using MediaBrowser.Model.MediaInfo;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Controllers
@@ -119,7 +120,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableTranscoding,
[FromQuery] bool? allowVideoStreamCopy,
[FromQuery] bool? allowAudioStreamCopy,
[FromBody] PlaybackInfoDto? playbackInfoDto)
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] PlaybackInfoDto? playbackInfoDto)
{
var authInfo = _authContext.GetAuthorizationInfo(Request);
@@ -258,24 +259,24 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? subtitleStreamIndex,
[FromQuery] int? maxAudioChannels,
[FromQuery] Guid? itemId,
[FromBody] OpenLiveStreamDto openLiveStreamDto,
[FromQuery] bool enableDirectPlay = true,
[FromQuery] bool enableDirectStream = true)
[FromBody] OpenLiveStreamDto? openLiveStreamDto,
[FromQuery] bool? enableDirectPlay,
[FromQuery] bool? enableDirectStream)
{
var request = new LiveStreamRequest
{
OpenToken = openToken,
UserId = userId ?? Guid.Empty,
PlaySessionId = playSessionId,
MaxStreamingBitrate = maxStreamingBitrate,
StartTimeTicks = startTimeTicks,
AudioStreamIndex = audioStreamIndex,
SubtitleStreamIndex = subtitleStreamIndex,
MaxAudioChannels = maxAudioChannels,
ItemId = itemId ?? Guid.Empty,
OpenToken = openToken ?? openLiveStreamDto?.OpenToken,
UserId = userId ?? openLiveStreamDto?.UserId ?? Guid.Empty,
PlaySessionId = playSessionId ?? openLiveStreamDto?.PlaySessionId,
MaxStreamingBitrate = maxStreamingBitrate ?? openLiveStreamDto?.MaxStreamingBitrate,
StartTimeTicks = startTimeTicks ?? openLiveStreamDto?.StartTimeTicks,
AudioStreamIndex = audioStreamIndex ?? openLiveStreamDto?.AudioStreamIndex,
SubtitleStreamIndex = subtitleStreamIndex ?? openLiveStreamDto?.SubtitleStreamIndex,
MaxAudioChannels = maxAudioChannels ?? openLiveStreamDto?.MaxAudioChannels,
ItemId = itemId ?? openLiveStreamDto?.ItemId ?? Guid.Empty,
DeviceProfile = openLiveStreamDto?.DeviceProfile,
EnableDirectPlay = enableDirectPlay,
EnableDirectStream = enableDirectStream,
EnableDirectPlay = enableDirectPlay ?? openLiveStreamDto?.EnableDirectPlay ?? true,
EnableDirectStream = enableDirectStream ?? openLiveStreamDto?.EnableDirectStream ?? true,
DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
};
return await _mediaInfoHelper.OpenMediaSource(Request, request).ConfigureAwait(false);

View File

@@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Updates;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Updates;
@@ -99,7 +100,7 @@ namespace Jellyfin.Api.Controllers
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
if (!string.IsNullOrEmpty(repositoryUrl))
{
packages = packages.Where(p => p.versions.Where(q => q.repositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase)).Any())
packages = packages.Where(p => p.Versions.Any(q => q.RepositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase)))
.ToList();
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
@@ -17,6 +18,7 @@ using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Jellyfin.Api.Controllers
{
@@ -53,6 +55,13 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Creates a new playlist.
/// </summary>
/// <remarks>
/// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
/// </remarks>
/// <param name="name">The playlist name.</param>
/// <param name="ids">The item ids.</param>
/// <param name="userId">The user id.</param>
/// <param name="mediaType">The media type.</param>
/// <param name="createPlaylistRequest">The create playlist payload.</param>
/// <returns>
/// A <see cref="Task" /> that represents the asynchronous operation to create a playlist.
@@ -61,14 +70,23 @@ namespace Jellyfin.Api.Controllers
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
[FromBody, Required] CreatePlaylistDto createPlaylistRequest)
[FromQuery] string? name,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] IReadOnlyList<Guid> ids,
[FromQuery] Guid? userId,
[FromQuery] string? mediaType,
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest)
{
if (ids.Count == 0)
{
ids = createPlaylistRequest?.Ids ?? Array.Empty<Guid>();
}
var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
{
Name = createPlaylistRequest.Name,
ItemIdList = createPlaylistRequest.Ids,
UserId = createPlaylistRequest.UserId,
MediaType = createPlaylistRequest.MediaType
Name = name ?? createPlaylistRequest?.Name,
ItemIdList = ids,
UserId = userId ?? createPlaylistRequest?.UserId ?? default,
MediaType = mediaType ?? createPlaylistRequest?.MediaType
}).ConfigureAwait(false);
return result;

View File

@@ -1,15 +1,21 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Mime;
using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.PluginDtos;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Plugins;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -23,112 +29,26 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
public class PluginsController : BaseJellyfinApiController
{
private readonly IApplicationHost _appHost;
private readonly IInstallationManager _installationManager;
private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions();
private readonly IPluginManager _pluginManager;
private readonly IConfigurationManager _config;
private readonly JsonSerializerOptions _serializerOptions;
/// <summary>
/// Initializes a new instance of the <see cref="PluginsController"/> class.
/// </summary>
/// <param name="appHost">Instance of the <see cref="IApplicationHost"/> interface.</param>
/// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
/// <param name="pluginManager">Instance of the <see cref="IPluginManager"/> interface.</param>
/// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param>
public PluginsController(
IApplicationHost appHost,
IInstallationManager installationManager)
IInstallationManager installationManager,
IPluginManager pluginManager,
IConfigurationManager config)
{
_appHost = appHost;
_installationManager = installationManager;
}
/// <summary>
/// Gets a list of currently installed plugins.
/// </summary>
/// <response code="200">Installed plugins returned.</response>
/// <returns>List of currently installed plugins.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<PluginInfo>> GetPlugins()
{
return Ok(_appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo()));
}
/// <summary>
/// Uninstalls a plugin.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <response code="204">Plugin uninstalled.</response>
/// <response code="404">Plugin not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
[HttpDelete("{pluginId}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId)
{
var plugin = _appHost.Plugins.FirstOrDefault(p => p.Id == pluginId);
if (plugin == null)
{
return NotFound();
}
_installationManager.UninstallPlugin(plugin);
return NoContent();
}
/// <summary>
/// Gets plugin configuration.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <response code="200">Plugin configuration returned.</response>
/// <response code="404">Plugin not found or plugin configuration not found.</response>
/// <returns>Plugin configuration.</returns>
[HttpGet("{pluginId}/Configuration")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute, Required] Guid pluginId)
{
if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin))
{
return NotFound();
}
return plugin.Configuration;
}
/// <summary>
/// Updates plugin configuration.
/// </summary>
/// <remarks>
/// Accepts plugin configuration as JSON body.
/// </remarks>
/// <param name="pluginId">Plugin id.</param>
/// <response code="204">Plugin configuration updated.</response>
/// <response code="404">Plugin not found or plugin does not have configuration.</response>
/// <returns>
/// A <see cref="Task" /> that represents the asynchronous operation to update plugin configuration.
/// The task result contains an <see cref="NoContentResult"/> indicating success, or <see cref="NotFoundResult"/>
/// when plugin not found or plugin doesn't have configuration.
/// </returns>
[HttpPost("{pluginId}/Configuration")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UpdatePluginConfiguration([FromRoute, Required] Guid pluginId)
{
if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin))
{
return NotFound();
}
var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType, _serializerOptions)
.ConfigureAwait(false);
if (configuration != null)
{
plugin.UpdateConfiguration(configuration);
}
return NoContent();
_pluginManager = pluginManager;
_serializerOptions = JsonDefaults.GetOptions();
_config = config;
}
/// <summary>
@@ -139,7 +59,7 @@ namespace Jellyfin.Api.Controllers
[Obsolete("This endpoint should not be used.")]
[HttpGet("SecurityInfo")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<PluginSecurityInfo> GetPluginSecurityInfo()
public static ActionResult<PluginSecurityInfo> GetPluginSecurityInfo()
{
return new PluginSecurityInfo
{
@@ -148,21 +68,6 @@ namespace Jellyfin.Api.Controllers
};
}
/// <summary>
/// Updates plugin security info.
/// </summary>
/// <param name="pluginSecurityInfo">Plugin security info.</param>
/// <response code="204">Plugin security info updated.</response>
/// <returns>An <see cref="NoContentResult"/>.</returns>
[Obsolete("This endpoint should not be used.")]
[HttpPost("SecurityInfo")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdatePluginSecurityInfo([FromBody, Required] PluginSecurityInfo pluginSecurityInfo)
{
return NoContent();
}
/// <summary>
/// Gets registration status for a feature.
/// </summary>
@@ -172,7 +77,7 @@ namespace Jellyfin.Api.Controllers
[Obsolete("This endpoint should not be used.")]
[HttpPost("RegistrationRecords/{name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name)
public static ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name)
{
return new MBRegistrationRecord
{
@@ -194,11 +99,253 @@ namespace Jellyfin.Api.Controllers
[Obsolete("Paid plugins are not supported")]
[HttpGet("Registrations/{name}")]
[ProducesResponseType(StatusCodes.Status501NotImplemented)]
public ActionResult GetRegistration([FromRoute, Required] string name)
public static ActionResult GetRegistration([FromRoute, Required] string name)
{
// TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins,
// delete all these registration endpoints. They are only kept for compatibility.
throw new NotImplementedException();
}
/// <summary>
/// Gets a list of currently installed plugins.
/// </summary>
/// <response code="200">Installed plugins returned.</response>
/// <returns>List of currently installed plugins.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<PluginInfo>> GetPlugins()
{
return Ok(_pluginManager.Plugins
.OrderBy(p => p.Name)
.Select(p => p.GetPluginInfo()));
}
/// <summary>
/// Enables a disabled plugin.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <param name="version">Plugin version.</param>
/// <response code="204">Plugin enabled.</response>
/// <response code="404">Plugin not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
[HttpPost("{pluginId}/{version}/Enable")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult EnablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
{
var plugin = _pluginManager.GetPlugin(pluginId, version);
if (plugin == null)
{
return NotFound();
}
_pluginManager.EnablePlugin(plugin);
return NoContent();
}
/// <summary>
/// Disable a plugin.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <param name="version">Plugin version.</param>
/// <response code="204">Plugin disabled.</response>
/// <response code="404">Plugin not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
[HttpPost("{pluginId}/{version}/Disable")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult DisablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
{
var plugin = _pluginManager.GetPlugin(pluginId, version);
if (plugin == null)
{
return NotFound();
}
_pluginManager.DisablePlugin(plugin);
return NoContent();
}
/// <summary>
/// Uninstalls a plugin by version.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <param name="version">Plugin version.</param>
/// <response code="204">Plugin uninstalled.</response>
/// <response code="404">Plugin not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
[HttpDelete("{pluginId}/{version}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UninstallPluginByVersion([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
{
var plugin = _pluginManager.GetPlugin(pluginId, version);
if (plugin == null)
{
return NotFound();
}
_installationManager.UninstallPlugin(plugin);
return NoContent();
}
/// <summary>
/// Uninstalls a plugin.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <response code="204">Plugin uninstalled.</response>
/// <response code="404">Plugin not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
[HttpDelete("{pluginId}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Please use the UninstallPluginByVersion API.")]
public ActionResult UninstallPlugin([FromRoute, Required] Guid pluginId)
{
// If no version is given, return the current instance.
var plugins = _pluginManager.Plugins.Where(p => p.Id.Equals(pluginId));
// Select the un-instanced one first.
var plugin = plugins.FirstOrDefault(p => p.Instance == null);
if (plugin == null)
{
// Then by the status.
plugin = plugins.OrderBy(p => p.Manifest.Status).FirstOrDefault();
}
if (plugin != null)
{
_installationManager.UninstallPlugin(plugin);
return NoContent();
}
return NotFound();
}
/// <summary>
/// Gets plugin configuration.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <response code="200">Plugin configuration returned.</response>
/// <response code="404">Plugin not found or plugin configuration not found.</response>
/// <returns>Plugin configuration.</returns>
[HttpGet("{pluginId}/Configuration")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute, Required] Guid pluginId)
{
var plugin = _pluginManager.GetPlugin(pluginId);
if (plugin?.Instance is IHasPluginConfiguration configPlugin)
{
return configPlugin.Configuration;
}
return NotFound();
}
/// <summary>
/// Updates plugin configuration.
/// </summary>
/// <remarks>
/// Accepts plugin configuration as JSON body.
/// </remarks>
/// <param name="pluginId">Plugin id.</param>
/// <response code="204">Plugin configuration updated.</response>
/// <response code="404">Plugin not found or plugin does not have configuration.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
[HttpPost("{pluginId}/Configuration")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UpdatePluginConfiguration([FromRoute, Required] Guid pluginId)
{
var plugin = _pluginManager.GetPlugin(pluginId);
if (plugin?.Instance is not IHasPluginConfiguration configPlugin)
{
return NotFound();
}
var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, configPlugin.ConfigurationType, _serializerOptions)
.ConfigureAwait(false);
if (configuration != null)
{
configPlugin.UpdateConfiguration(configuration);
}
return NoContent();
}
/// <summary>
/// Gets a plugin's image.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <param name="version">Plugin version.</param>
/// <response code="200">Plugin image returned.</response>
/// <returns>Plugin's image.</returns>
[HttpGet("{pluginId}/{version}/Image")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
[AllowAnonymous]
public ActionResult GetPluginImage([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
{
var plugin = _pluginManager.GetPlugin(pluginId, version);
if (plugin == null)
{
return NotFound();
}
var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath ?? string.Empty);
if (((ServerConfiguration)_config.CommonConfiguration).DisablePluginImages
|| plugin.Manifest.ImagePath == null
|| !System.IO.File.Exists(imagePath))
{
return NotFound();
}
imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath);
return PhysicalFile(imagePath, MimeTypes.GetMimeType(imagePath));
}
/// <summary>
/// Gets a plugin's manifest.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <response code="204">Plugin manifest returned.</response>
/// <response code="404">Plugin not found.</response>
/// <returns>A <see cref="PluginManifest"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
[HttpPost("{pluginId}/Manifest")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<PluginManifest> GetPluginManifest([FromRoute, Required] Guid pluginId)
{
var plugin = _pluginManager.GetPlugin(pluginId);
if (plugin != null)
{
return plugin.Manifest;
}
return NotFound();
}
/// <summary>
/// Updates plugin security info.
/// </summary>
/// <param name="pluginSecurityInfo">Plugin security info.</param>
/// <response code="204">Plugin security info updated.</response>
/// <returns>An <see cref="NoContentResult"/>.</returns>
[Obsolete("This endpoint should not be used.")]
[HttpPost("SecurityInfo")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdatePluginSecurityInfo([FromBody, Required] PluginSecurityInfo pluginSecurityInfo)
{
return NoContent();
}
}
}

View File

@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
{
if (_quickConnect.State == QuickConnectState.Unavailable)
{
return Forbid("Quick connect is unavailable");
return StatusCode(StatusCodes.Status403Forbidden, "Quick connect is unavailable");
}
_quickConnect.Activate();
@@ -126,7 +126,7 @@ namespace Jellyfin.Api.Controllers
var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
if (!userId.HasValue)
{
return Forbid("Unknown user id");
return StatusCode(StatusCodes.Status403Forbidden, "Unknown user id");
}
return _quickConnect.AuthorizeRequest(userId.Value, code);

View File

@@ -20,7 +20,7 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// The sync play controller.
/// </summary>
[Authorize(Policy = Policies.SyncPlayAccess)]
[Authorize(Policy = Policies.SyncPlayHasAccess)]
public class SyncPlayController : BaseJellyfinApiController
{
private readonly ISessionManager _sessionManager;
@@ -51,7 +51,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("New")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayCreateGroupAccess)]
[Authorize(Policy = Policies.SyncPlayCreateGroup)]
public ActionResult SyncPlayCreateGroup(
[FromBody, Required] NewGroupRequestDto requestData)
{
@@ -69,7 +69,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Join")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayAccess)]
[Authorize(Policy = Policies.SyncPlayJoinGroup)]
public ActionResult SyncPlayJoinGroup(
[FromBody, Required] JoinGroupRequestDto requestData)
{
@@ -86,6 +86,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Leave")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayLeaveGroup()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@@ -101,7 +102,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="IEnumerable{GroupInfoView}"/> containing the available SyncPlay groups.</returns>
[HttpGet("List")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.SyncPlayAccess)]
[Authorize(Policy = Policies.SyncPlayJoinGroup)]
public ActionResult<IEnumerable<GroupInfoDto>> SyncPlayGetGroups()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@@ -117,6 +118,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("SetNewQueue")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetNewQueue(
[FromBody, Required] PlayRequestDto requestData)
{
@@ -137,6 +139,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("SetPlaylistItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetPlaylistItem(
[FromBody, Required] SetPlaylistItemRequestDto requestData)
{
@@ -154,6 +157,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("RemoveFromPlaylist")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayRemoveFromPlaylist(
[FromBody, Required] RemoveFromPlaylistRequestDto requestData)
{
@@ -171,6 +175,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("MovePlaylistItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayMovePlaylistItem(
[FromBody, Required] MovePlaylistItemRequestDto requestData)
{
@@ -188,6 +193,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Queue")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayQueue(
[FromBody, Required] QueueRequestDto requestData)
{
@@ -204,6 +210,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Unpause")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayUnpause()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@@ -219,6 +226,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Pause")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayPause()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@@ -234,6 +242,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Stop")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayStop()
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
@@ -250,6 +259,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Seek")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySeek(
[FromBody, Required] SeekRequestDto requestData)
{
@@ -267,6 +277,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Buffering")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayBuffering(
[FromBody, Required] BufferRequestDto requestData)
{
@@ -288,6 +299,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Ready")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayReady(
[FromBody, Required] ReadyRequestDto requestData)
{
@@ -309,6 +321,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("SetIgnoreWait")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetIgnoreWait(
[FromBody, Required] IgnoreWaitRequestDto requestData)
{
@@ -326,6 +339,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("NextItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayNextItem(
[FromBody, Required] NextItemRequestDto requestData)
{
@@ -343,6 +357,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("PreviousItem")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlayPreviousItem(
[FromBody, Required] PreviousItemRequestDto requestData)
{
@@ -360,6 +375,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("SetRepeatMode")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetRepeatMode(
[FromBody, Required] SetRepeatModeRequestDto requestData)
{
@@ -377,6 +393,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("SetShuffleMode")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
public ActionResult SyncPlaySetShuffleMode(
[FromBody, Required] SetShuffleModeRequestDto requestData)
{

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
@@ -66,7 +67,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<SystemInfo> GetSystemInfo()
{
return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress);
return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
}
/// <summary>
@@ -78,7 +79,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
{
return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress);
return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
}
/// <summary>

View File

@@ -169,7 +169,7 @@ namespace Jellyfin.Api.Controllers
if (!string.IsNullOrEmpty(password) && string.IsNullOrEmpty(pw))
{
return Forbid("Only sha1 password is not allowed.");
return StatusCode(StatusCodes.Status403Forbidden, "Only sha1 password is not allowed.");
}
// Password should always be null
@@ -267,11 +267,11 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UpdateUserPassword(
[FromRoute, Required] Guid userId,
[FromBody] UpdateUserPassword request)
[FromBody, Required] UpdateUserPassword request)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{
return Forbid("User is not allowed to update the password.");
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the password.");
}
var user = _userManager.GetUserById(userId);
@@ -296,7 +296,7 @@ namespace Jellyfin.Api.Controllers
if (success == null)
{
return Forbid("Invalid user or password entered.");
return StatusCode(StatusCodes.Status403Forbidden, "Invalid user or password entered.");
}
await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
@@ -325,11 +325,11 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateUserEasyPassword(
[FromRoute, Required] Guid userId,
[FromBody] UpdateUserEasyPassword request)
[FromBody, Required] UpdateUserEasyPassword request)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
{
return Forbid("User is not allowed to update the easy password.");
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the easy password.");
}
var user = _userManager.GetUserById(userId);
@@ -367,16 +367,11 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> UpdateUser(
[FromRoute, Required] Guid userId,
[FromBody] UserDto updateUser)
[FromBody, Required] UserDto updateUser)
{
if (updateUser == null)
{
return BadRequest();
}
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
{
return Forbid("User update not allowed.");
return StatusCode(StatusCodes.Status403Forbidden, "User update not allowed.");
}
var user = _userManager.GetUserById(userId);
@@ -407,13 +402,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> UpdateUserPolicy(
[FromRoute, Required] Guid userId,
[FromBody] UserPolicy newPolicy)
[FromBody, Required] UserPolicy newPolicy)
{
if (newPolicy == null)
{
return BadRequest();
}
var user = _userManager.GetUserById(userId);
// If removing admin access
@@ -421,14 +411,14 @@ namespace Jellyfin.Api.Controllers
{
if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
{
return Forbid("There must be at least one user in the system with administrative access.");
return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one user in the system with administrative access.");
}
}
// If disabling
if (newPolicy.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator))
{
return Forbid("Administrators cannot be disabled.");
return StatusCode(StatusCodes.Status403Forbidden, "Administrators cannot be disabled.");
}
// If disabling
@@ -436,7 +426,7 @@ namespace Jellyfin.Api.Controllers
{
if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1)
{
return Forbid("There must be at least one enabled user in the system.");
return StatusCode(StatusCodes.Status403Forbidden, "There must be at least one enabled user in the system.");
}
var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
@@ -462,11 +452,11 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public async Task<ActionResult> UpdateUserConfiguration(
[FromRoute, Required] Guid userId,
[FromBody] UserConfiguration userConfig)
[FromBody, Required] UserConfiguration userConfig)
{
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
{
return Forbid("User configuration update not allowed");
return StatusCode(StatusCodes.Status403Forbidden, "User configuration update not allowed");
}
await _userManager.UpdateConfigurationAsync(userId, userConfig).ConfigureAwait(false);
@@ -483,7 +473,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("New")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<UserDto>> CreateUserByName([FromBody] CreateUserByName request)
public async Task<ActionResult<UserDto>> CreateUserByName([FromBody, Required] CreateUserByName request)
{
var newUser = await _userManager.CreateUserAsync(request.Name).ConfigureAwait(false);

View File

@@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Merges videos into a single record.
/// </summary>
/// <param name="itemIds">Item id list. This allows multiple, comma delimited.</param>
/// <param name="ids">Item id list. This allows multiple, comma delimited.</param>
/// <response code="204">Videos merged.</response>
/// <response code="400">Supply at least 2 video ids.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success, or a <see cref="BadRequestResult"/> if less than two ids were supplied.</returns>
@@ -204,9 +204,9 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds)
public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
{
var items = itemIds
var items = ids
.Select(i => _libraryManager.GetItemById(i))
.OfType<Video>()
.OrderBy(i => i.Id)

View File

@@ -16,7 +16,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />

View File

@@ -1,4 +1,5 @@
using MediaBrowser.Common.Plugins;
using System;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Model.Plugins;
@@ -22,8 +23,7 @@ namespace Jellyfin.Api.Models
if (page.Plugin != null)
{
DisplayName = page.Plugin.Name;
// Don't use "N" because it needs to match Plugin.Id
PluginId = page.Plugin.Id.ToString();
PluginId = page.Plugin.Id;
}
}
@@ -32,16 +32,14 @@ namespace Jellyfin.Api.Models
/// </summary>
/// <param name="plugin">Instance of <see cref="IPlugin"/> interface.</param>
/// <param name="page">Instance of <see cref="PluginPageInfo"/> interface.</param>
public ConfigurationPageInfo(IPlugin plugin, PluginPageInfo page)
public ConfigurationPageInfo(IPlugin? plugin, PluginPageInfo page)
{
Name = page.Name;
EnableInMainMenu = page.EnableInMainMenu;
MenuSection = page.MenuSection;
MenuIcon = page.MenuIcon;
DisplayName = string.IsNullOrWhiteSpace(page.DisplayName) ? plugin.Name : page.DisplayName;
// Don't use "N" because it needs to match Plugin.Id
PluginId = plugin.Id.ToString();
DisplayName = string.IsNullOrWhiteSpace(page.DisplayName) ? plugin?.Name : page.DisplayName;
PluginId = plugin?.Id;
}
/// <summary>
@@ -80,6 +78,6 @@ namespace Jellyfin.Api.Models
/// Gets or sets the plugin id.
/// </summary>
/// <value>The plugin id.</value>
public string? PluginId { get; set; }
public Guid? PluginId { get; set; }
}
}

View File

@@ -0,0 +1,28 @@
using System.ComponentModel.DataAnnotations;
namespace Jellyfin.Api.Models.LiveTvDtos
{
/// <summary>
/// Set channel mapping dto.
/// </summary>
public class SetChannelMappingDto
{
/// <summary>
/// Gets or sets the provider id.
/// </summary>
[Required]
public string ProviderId { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the tuner channel id.
/// </summary>
[Required]
public string TunerChannelId { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the provider channel id.
/// </summary>
[Required]
public string ProviderChannelId { get; set; } = string.Empty;
}
}

View File

@@ -10,6 +10,61 @@ namespace Jellyfin.Api.Models.MediaInfoDtos
/// </summary>
public class OpenLiveStreamDto
{
/// <summary>
/// Gets or sets the open token.
/// </summary>
public string? OpenToken { get; set; }
/// <summary>
/// Gets or sets the user id.
/// </summary>
public Guid? UserId { get; set; }
/// <summary>
/// Gets or sets the play session id.
/// </summary>
public string? PlaySessionId { get; set; }
/// <summary>
/// Gets or sets the max streaming bitrate.
/// </summary>
public int? MaxStreamingBitrate { get; set; }
/// <summary>
/// Gets or sets the start time in ticks.
/// </summary>
public long? StartTimeTicks { get; set; }
/// <summary>
/// Gets or sets the audio stream index.
/// </summary>
public int? AudioStreamIndex { get; set; }
/// <summary>
/// Gets or sets the subtitle stream index.
/// </summary>
public int? SubtitleStreamIndex { get; set; }
/// <summary>
/// Gets or sets the max audio channels.
/// </summary>
public int? MaxAudioChannels { get; set; }
/// <summary>
/// Gets or sets the item id.
/// </summary>
public Guid? ItemId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to enable direct play.
/// </summary>
public bool? EnableDirectPlay { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to enale direct stream.
/// </summary>
public bool? EnableDirectStream { get; set; }
/// <summary>
/// Gets or sets the device profile.
/// </summary>

View File

@@ -24,7 +24,7 @@ namespace Jellyfin.Api.Models.PlaylistDtos
/// <summary>
/// Gets or sets the user id.
/// </summary>
public Guid UserId { get; set; }
public Guid? UserId { get; set; }
/// <summary>
/// Gets or sets the media type.

View File

@@ -23,7 +23,6 @@ namespace Jellyfin.Data.Entities
Client = client;
SortBy = "SortName";
ViewType = ViewType.Poster;
SortOrder = SortOrder.Ascending;
RememberSorting = false;
RememberIndexing = false;

View File

@@ -2,9 +2,9 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
@@ -71,7 +71,7 @@ namespace Jellyfin.Data.Entities
EnableAutoLogin = false;
PlayDefaultAudioTrack = true;
SubtitleMode = SubtitlePlaybackMode.Default;
SyncPlayAccess = SyncPlayAccess.CreateAndJoinGroups;
SyncPlayAccess = SyncPlayUserAccessType.CreateAndJoinGroups;
AddDefaultPermissions();
AddDefaultPreferences();
@@ -326,7 +326,7 @@ namespace Jellyfin.Data.Entities
/// <summary>
/// Gets or sets the level of sync play permissions this user has.
/// </summary>
public SyncPlayAccess SyncPlayAccess { get; set; }
public SyncPlayUserAccessType SyncPlayAccess { get; set; }
/// <summary>
/// Gets or sets the row version.
@@ -413,6 +413,44 @@ namespace Jellyfin.Data.Entities
return Equals(val, string.Empty) ? Array.Empty<string>() : val.Split(Delimiter);
}
/// <summary>
/// Gets the user's preferences for the given preference kind.
/// </summary>
/// <param name="preference">The preference kind.</param>
/// <typeparam name="T">Type of preference.</typeparam>
/// <returns>A {T} array containing the user's preference.</returns>
public T[] GetPreferenceValues<T>(PreferenceKind preference)
{
var val = Preferences.First(p => p.Kind == preference).Value;
if (string.IsNullOrEmpty(val))
{
return Array.Empty<T>();
}
// Convert array of {string} to array of {T}
var converter = TypeDescriptor.GetConverter(typeof(T));
var stringValues = val.Split(Delimiter);
var convertedCount = 0;
var parsedValues = new T[stringValues.Length];
for (var i = 0; i < stringValues.Length; i++)
{
try
{
var parsedValue = converter.ConvertFromString(stringValues[i].Trim());
if (parsedValue != null)
{
parsedValues[convertedCount++] = (T)parsedValue;
}
}
catch (FormatException)
{
// Unable to convert value
}
}
return parsedValues[..convertedCount];
}
/// <summary>
/// Sets the specified preference to the given value.
/// </summary>
@@ -421,7 +459,19 @@ namespace Jellyfin.Data.Entities
public void SetPreference(PreferenceKind preference, string[] values)
{
Preferences.First(p => p.Kind == preference).Value
= string.Join(Delimiter.ToString(CultureInfo.InvariantCulture), values);
= string.Join(Delimiter, values);
}
/// <summary>
/// Sets the specified preference to the given value.
/// </summary>
/// <param name="preference">The preference kind.</param>
/// <param name="values">The values.</param>
/// <typeparam name="T">The type of value.</typeparam>
public void SetPreference<T>(PreferenceKind preference, T[] values)
{
Preferences.First(p => p.Kind == preference).Value
= string.Join(Delimiter, values);
}
/// <summary>
@@ -441,7 +491,7 @@ namespace Jellyfin.Data.Entities
/// <returns><c>True</c> if the folder is in the user's grouped folders.</returns>
public bool IsFolderGrouped(Guid id)
{
return GetPreference(PreferenceKind.GroupedFolders).Any(i => new Guid(i) == id);
return Array.IndexOf(GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders), id) != -1;
}
private static bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date)

View File

@@ -0,0 +1,28 @@
namespace Jellyfin.Data.Enums
{
/// <summary>
/// Enum SyncPlayAccessRequirementType.
/// </summary>
public enum SyncPlayAccessRequirementType
{
/// <summary>
/// User must have access to SyncPlay, in some form.
/// </summary>
HasAccess = 0,
/// <summary>
/// User must be able to create groups.
/// </summary>
CreateGroup = 1,
/// <summary>
/// User must be able to join groups.
/// </summary>
JoinGroup = 2,
/// <summary>
/// User must be in a group.
/// </summary>
IsInGroup = 3
}
}

View File

@@ -1,9 +1,9 @@
namespace Jellyfin.Data.Enums
{
/// <summary>
/// Enum SyncPlayAccess.
/// Enum SyncPlayUserAccessType.
/// </summary>
public enum SyncPlayAccess
public enum SyncPlayUserAccessType
{
/// <summary>
/// User can create groups and join them.

View File

@@ -1,4 +1,4 @@
namespace Jellyfin.Data.Enums
namespace Jellyfin.Data.Enums
{
/// <summary>
/// An enum representing the type of view for a library or collection.
@@ -6,33 +6,108 @@
public enum ViewType
{
/// <summary>
/// Shows banners.
/// Shows albums.
/// </summary>
Banner = 0,
Albums = 0,
/// <summary>
/// Shows a list of content.
/// Shows album artists.
/// </summary>
List = 1,
AlbumArtists = 1,
/// <summary>
/// Shows poster artwork.
/// Shows artists.
/// </summary>
Poster = 2,
Artists = 2,
/// <summary>
/// Shows poster artwork with a card containing the name and year.
/// Shows channels.
/// </summary>
PosterCard = 3,
Channels = 3,
/// <summary>
/// Shows a thumbnail.
/// Shows collections.
/// </summary>
Thumb = 4,
Collections = 4,
/// <summary>
/// Shows a thumbnail with a card containing the name and year.
/// Shows episodes.
/// </summary>
ThumbCard = 5
Episodes = 5,
/// <summary>
/// Shows favorites.
/// </summary>
Favorites = 6,
/// <summary>
/// Shows genres.
/// </summary>
Genres = 7,
/// <summary>
/// Shows guide.
/// </summary>
Guide = 8,
/// <summary>
/// Shows movies.
/// </summary>
Movies = 9,
/// <summary>
/// Shows networks.
/// </summary>
Networks = 10,
/// <summary>
/// Shows playlists.
/// </summary>
Playlists = 11,
/// <summary>
/// Shows programs.
/// </summary>
Programs = 12,
/// <summary>
/// Shows recordings.
/// </summary>
Recordings = 13,
/// <summary>
/// Shows schedule.
/// </summary>
Schedule = 14,
/// <summary>
/// Shows series.
/// </summary>
Series = 15,
/// <summary>
/// Shows shows.
/// </summary>
Shows = 16,
/// <summary>
/// Shows songs.
/// </summary>
Songs = 17,
/// <summary>
/// Shows songs.
/// </summary>
Suggestions = 18,
/// <summary>
/// Shows trailers.
/// </summary>
Trailers = 19,
/// <summary>
/// Shows upcoming.
/// </summary>
Upcoming = 20
}
}

View File

@@ -41,8 +41,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -435,7 +435,7 @@ namespace Jellyfin.Drawing.Skia
0f,
kernelOffset,
SKShaderTileMode.Clamp,
false);
true);
canvas.DrawBitmap(
source,

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using SkiaSharp;
namespace Jellyfin.Drawing.Skia
@@ -118,6 +119,16 @@ namespace Jellyfin.Drawing.Skia
};
canvas.DrawRect(0, 0, width, height, paintColor);
var typeFace = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
// use the system fallback to find a typeface for the given CJK character
var nonCjkPattern = @"[^\p{IsCJKUnifiedIdeographs}\p{IsCJKUnifiedIdeographsExtensionA}\p{IsKatakana}\p{IsHiragana}\p{IsHangulSyllables}\p{IsHangulJamo}]";
var filteredName = Regex.Replace(libraryName ?? string.Empty, nonCjkPattern, string.Empty);
if (!string.IsNullOrEmpty(filteredName))
{
typeFace = SKFontManager.Default.MatchCharacter(null, SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright, null, filteredName[0]);
}
// draw library name
var textPaint = new SKPaint
{
@@ -125,7 +136,7 @@ namespace Jellyfin.Drawing.Skia
Style = SKPaintStyle.Fill,
TextSize = 112,
TextAlign = SKTextAlign.Center,
Typeface = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright),
Typeface = typeFace,
IsAntialias = true
};

View File

@@ -27,6 +27,16 @@ namespace Jellyfin.Networking.Configuration
/// </summary>
public bool RequireHttps { get; set; }
/// <summary>
/// Gets or sets the filesystem path of an X.509 certificate to use for SSL.
/// </summary>
public string CertificatePath { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the password required to access the X.509 certificate data in the file specified by <see cref="CertificatePath"/>.
/// </summary>
public string CertificatePassword { get; set; } = string.Empty;
/// <summary>
/// Gets or sets a value used to specify the URL prefix that your Jellyfin instance can be accessed at.
/// </summary>
@@ -83,7 +93,7 @@ namespace Jellyfin.Networking.Configuration
/// </summary>
/// <remarks>
/// In order for HTTPS to be used, in addition to setting this to true, valid values must also be
/// provided for <see cref="ServerConfiguration.CertificatePath"/> and <see cref="ServerConfiguration.CertificatePassword"/>.
/// provided for <see cref="CertificatePath"/> and <see cref="CertificatePassword"/>.
/// </remarks>
public bool EnableHttps { get; set; }

View File

@@ -784,7 +784,7 @@ namespace Jellyfin.Networking.Manager
}
else
{
_logger.LogDebug("Invalid or unknown network {Token}.", token);
_logger.LogDebug("Invalid or unknown object {Token}.", token);
}
}
@@ -913,15 +913,6 @@ namespace Jellyfin.Networking.Manager
{
string[] lanAddresses = config.LocalNetworkAddresses;
// TODO: remove when bug fixed: https://github.com/jellyfin/jellyfin-web/issues/1334
if (lanAddresses.Length == 1 && lanAddresses[0].IndexOf(',', StringComparison.OrdinalIgnoreCase) != -1)
{
lanAddresses = lanAddresses[0].Split(',');
}
// TODO: end fix: https://github.com/jellyfin/jellyfin-web/issues/1334
// Add virtual machine interface names to the list of bind exclusions, so that they are auto-excluded.
if (config.IgnoreVirtualInterfaces)
{
@@ -1314,9 +1305,7 @@ namespace Jellyfin.Networking.Manager
return true;
}
// Have to return something, so return an internal address
_logger.LogWarning("{Source}: External request received, however, no WAN interface found.", source);
_logger.LogDebug("{Source}: External request received, but no WAN interface found. Need to route through internal network.", source);
return false;
}
}

View File

@@ -25,11 +25,11 @@
<ItemGroup>
<PackageReference Include="System.Linq.Async" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.0">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.0">
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -376,14 +376,14 @@ namespace Jellyfin.Server.Implementations.Users
EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing),
AccessSchedules = user.AccessSchedules.ToArray(),
BlockedTags = user.GetPreference(PreferenceKind.BlockedTags),
EnabledChannels = user.GetPreference(PreferenceKind.EnabledChannels)?.Select(Guid.Parse).ToArray(),
EnabledChannels = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels),
EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices),
EnabledFolders = user.GetPreference(PreferenceKind.EnabledFolders)?.Select(Guid.Parse).ToArray(),
EnabledFolders = user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders),
EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders),
SyncPlayAccess = user.SyncPlayAccess,
BlockedChannels = user.GetPreference(PreferenceKind.BlockedChannels)?.Select(Guid.Parse).ToArray(),
BlockedMediaFolders = user.GetPreference(PreferenceKind.BlockedMediaFolders)?.Select(Guid.Parse).ToArray(),
BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems).Select(Enum.Parse<UnratedItem>).ToArray()
BlockedChannels = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels),
BlockedMediaFolders = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedMediaFolders),
BlockUnratedItems = user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems)
}
};
}
@@ -704,13 +704,11 @@ namespace Jellyfin.Server.Implementations.Users
}
// TODO: fix this at some point
user.SetPreference(
PreferenceKind.BlockUnratedItems,
policy.BlockUnratedItems?.Select(i => i.ToString()).ToArray() ?? Array.Empty<string>());
user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<UnratedItem>());
user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
dbContext.Update(user);

View File

@@ -107,5 +107,28 @@ namespace Jellyfin.Server.Extensions
{
return appBuilder.UseMiddleware<WebSocketHandlerMiddleware>();
}
/// <summary>
/// Adds robots.txt redirection to the application pipeline.
/// </summary>
/// <param name="appBuilder">The application builder.</param>
/// <returns>The updated application builder.</returns>
public static IApplicationBuilder UseRobotsRedirection(this IApplicationBuilder appBuilder)
{
return appBuilder.UseMiddleware<RobotsRedirectionMiddleware>();
}
/// <summary>
/// Adds /emby and /mediabrowser route trimming to the application pipeline.
/// </summary>
/// <remarks>
/// This must be injected before any path related middleware.
/// </remarks>
/// <param name="appBuilder">The application builder.</param>
/// <returns>The updated application builder.</returns>
public static IApplicationBuilder UsePathTrim(this IApplicationBuilder appBuilder)
{
return appBuilder.UseMiddleware<LegacyEmbyRouteRewriteMiddleware>();
}
}
}

View File

@@ -24,6 +24,7 @@ using Jellyfin.Server.Configuration;
using Jellyfin.Server.Filters;
using Jellyfin.Server.Formatters;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
@@ -127,18 +128,32 @@ namespace Jellyfin.Server.Extensions
policy.AddRequirements(new RequiresElevationRequirement());
});
options.AddPolicy(
Policies.SyncPlayAccess,
Policies.SyncPlayHasAccess,
policy =>
{
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccess.JoinGroups));
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.HasAccess));
});
options.AddPolicy(
Policies.SyncPlayCreateGroupAccess,
Policies.SyncPlayCreateGroup,
policy =>
{
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccess.CreateAndJoinGroups));
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.CreateGroup));
});
options.AddPolicy(
Policies.SyncPlayJoinGroup,
policy =>
{
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup));
});
options.AddPolicy(
Policies.SyncPlayIsInGroup,
policy =>
{
policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication);
policy.AddRequirements(new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup));
});
});
}
@@ -169,11 +184,19 @@ namespace Jellyfin.Server.Extensions
.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
for (var i = 0; i < knownProxies.Count; i++)
if (knownProxies.Count == 0)
{
if (IPAddress.TryParse(knownProxies[i], out var address))
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
}
else
{
for (var i = 0; i < knownProxies.Count; i++)
{
options.KnownProxies.Add(address);
if (IPHost.TryParse(knownProxies[i], out var host))
{
options.KnownProxies.Add(host.Address);
}
}
}
})
@@ -204,6 +227,7 @@ namespace Jellyfin.Server.Extensions
options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented;
options.JsonSerializerOptions.DefaultIgnoreCondition = jsonOptions.DefaultIgnoreCondition;
options.JsonSerializerOptions.NumberHandling = jsonOptions.NumberHandling;
options.JsonSerializerOptions.PropertyNameCaseInsensitive = jsonOptions.PropertyNameCaseInsensitive;
options.JsonSerializerOptions.Converters.Clear();
foreach (var converter in jsonOptions.Converters)

View File

@@ -14,7 +14,8 @@ namespace Jellyfin.Server.Filters
{
Schema = new OpenApiSchema
{
Type = "file"
Type = "string",
Format = "binary"
}
};

View File

@@ -40,8 +40,8 @@
<PackageReference Include="CommandLineParser" Version="2.8.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="5.0.1" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="5.0.1" />
<PackageReference Include="prometheus-net" Version="4.0.0" />
<PackageReference Include="prometheus-net.AspNetCore" Version="4.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />

View File

@@ -0,0 +1,54 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Middleware
{
/// <summary>
/// Removes /emby and /mediabrowser from requested route.
/// </summary>
public class LegacyEmbyRouteRewriteMiddleware
{
private const string EmbyPath = "/emby";
private const string MediabrowserPath = "/mediabrowser";
private readonly RequestDelegate _next;
private readonly ILogger<LegacyEmbyRouteRewriteMiddleware> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="LegacyEmbyRouteRewriteMiddleware"/> class.
/// </summary>
/// <param name="next">The next delegate in the pipeline.</param>
/// <param name="logger">The logger.</param>
public LegacyEmbyRouteRewriteMiddleware(
RequestDelegate next,
ILogger<LegacyEmbyRouteRewriteMiddleware> logger)
{
_next = next;
_logger = logger;
}
/// <summary>
/// Executes the middleware action.
/// </summary>
/// <param name="httpContext">The current HTTP context.</param>
/// <returns>The async task.</returns>
public async Task Invoke(HttpContext httpContext)
{
var localPath = httpContext.Request.Path.ToString();
if (localPath.StartsWith(EmbyPath, StringComparison.OrdinalIgnoreCase))
{
httpContext.Request.Path = localPath[EmbyPath.Length..];
_logger.LogDebug("Removing {EmbyPath} from route.", EmbyPath);
}
else if (localPath.StartsWith(MediabrowserPath, StringComparison.OrdinalIgnoreCase))
{
httpContext.Request.Path = localPath[MediabrowserPath.Length..];
_logger.LogDebug("Removing {MediabrowserPath} from route.", MediabrowserPath);
}
await _next(httpContext).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,47 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Middleware
{
/// <summary>
/// Redirect requests to robots.txt to web/robots.txt.
/// </summary>
public class RobotsRedirectionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RobotsRedirectionMiddleware> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="RobotsRedirectionMiddleware"/> class.
/// </summary>
/// <param name="next">The next delegate in the pipeline.</param>
/// <param name="logger">The logger.</param>
public RobotsRedirectionMiddleware(
RequestDelegate next,
ILogger<RobotsRedirectionMiddleware> logger)
{
_next = next;
_logger = logger;
}
/// <summary>
/// Executes the middleware action.
/// </summary>
/// <param name="httpContext">The current HTTP context.</param>
/// <returns>The async task.</returns>
public async Task Invoke(HttpContext httpContext)
{
var localPath = httpContext.Request.Path.ToString();
if (string.Equals(localPath, "/robots.txt", StringComparison.OrdinalIgnoreCase))
{
_logger.LogDebug("Redirecting robots.txt request to web/robots.txt");
httpContext.Response.Redirect("web/robots.txt");
return;
}
await _next(httpContext).ConfigureAwait(false);
}
}
}

View File

@@ -8,7 +8,6 @@ using System.Text.Json.Serialization;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Server.Implementations;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
@@ -81,6 +80,8 @@ namespace Jellyfin.Server.Migrations.Routines
{ "unstable", ChromecastVersion.Unstable }
};
var displayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var customDisplayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null))
{
@@ -97,6 +98,15 @@ namespace Jellyfin.Server.Migrations.Routines
var itemId = new Guid(result[1].ToBlob());
var dtoUserId = new Guid(result[1].ToBlob());
var client = result[2].ToString();
var displayPreferencesKey = $"{dtoUserId}|{itemId}|{client}";
if (displayPrefs.Contains(displayPreferencesKey))
{
// Duplicate display preference.
continue;
}
displayPrefs.Add(displayPreferencesKey);
var existingUser = _userManager.GetUserById(dtoUserId);
if (existingUser == null)
{
@@ -109,7 +119,7 @@ namespace Jellyfin.Server.Migrations.Routines
: ChromecastVersion.Stable;
dto.CustomPrefs.Remove("chromecastVersion");
var displayPreferences = new DisplayPreferences(dtoUserId, itemId, result[2].ToString())
var displayPreferences = new DisplayPreferences(dtoUserId, itemId, client)
{
IndexBy = Enum.TryParse<IndexingKind>(dto.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null,
ShowBackdrop = dto.ShowBackdrop,
@@ -185,7 +195,13 @@ namespace Jellyfin.Server.Migrations.Routines
foreach (var (key, value) in dto.CustomPrefs)
{
dbContext.Add(new CustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client, key, value));
// Custom display preferences can have a key collision.
var indexKey = $"{displayPreferences.UserId}|{itemId}|{displayPreferences.Client}|{key}";
if (!customDisplayPrefs.Contains(indexKey))
{
dbContext.Add(new CustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client, key, value));
customDisplayPrefs.Add(indexKey);
}
}
dbContext.Add(displayPreferences);

View File

@@ -168,9 +168,9 @@ namespace Jellyfin.Server.Migrations.Routines
}
user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders?.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);

View File

@@ -128,6 +128,8 @@ namespace Jellyfin.Server
mainApp.UseHttpsRedirection();
}
// This must be injected before any path related middleware.
mainApp.UsePathTrim();
mainApp.UseStaticFiles();
if (appConfig.HostWebClient())
{
@@ -142,6 +144,8 @@ namespace Jellyfin.Server
RequestPath = "/web",
ContentTypeProvider = extensionProvider
});
mainApp.UseRobotsRedirection();
}
mainApp.UseAuthentication();

View File

@@ -2,11 +2,16 @@ using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using MediaBrowser.Common.Plugins;
using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common
{
/// <summary>
/// Delegate used with GetExports{T}.
/// </summary>
/// <param name="type">Type to create.</param>
/// <returns>New instance of type <param>type</param>.</returns>
public delegate object CreationDelegate(Type type);
/// <summary>
/// An interface to be implemented by the applications hosting a kernel.
/// </summary>
@@ -53,6 +58,11 @@ namespace MediaBrowser.Common
/// <value>The application version.</value>
Version ApplicationVersion { get; }
/// <summary>
/// Gets or sets the service provider.
/// </summary>
IServiceProvider ServiceProvider { get; set; }
/// <summary>
/// Gets the application version.
/// </summary>
@@ -71,12 +81,6 @@ namespace MediaBrowser.Common
/// </summary>
string ApplicationUserAgentAddress { get; }
/// <summary>
/// Gets the plugins.
/// </summary>
/// <value>The plugins.</value>
IReadOnlyList<IPlugin> Plugins { get; }
/// <summary>
/// Gets all plugin assemblies which implement a custom rest api.
/// </summary>
@@ -101,6 +105,22 @@ namespace MediaBrowser.Common
/// <returns><see cref="IReadOnlyCollection{T}" />.</returns>
IReadOnlyCollection<T> GetExports<T>(bool manageLifetime = true);
/// <summary>
/// Gets the exports.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="defaultFunc">Delegate function that gets called to create the object.</param>
/// <param name="manageLifetime">If set to <c>true</c> [manage lifetime].</param>
/// <returns><see cref="IReadOnlyCollection{T}" />.</returns>
IReadOnlyCollection<T> GetExports<T>(CreationDelegate defaultFunc, bool manageLifetime = true);
/// <summary>
/// Gets the export types.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <returns>IEnumerable{Type}.</returns>
IEnumerable<Type> GetExportTypes<T>();
/// <summary>
/// Resolves this instance.
/// </summary>
@@ -114,12 +134,6 @@ namespace MediaBrowser.Common
/// <returns>A task.</returns>
Task Shutdown();
/// <summary>
/// Removes the plugin.
/// </summary>
/// <param name="plugin">The plugin.</param>
void RemovePlugin(IPlugin plugin);
/// <summary>
/// Initializes this instance.
/// </summary>

View File

@@ -0,0 +1,30 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MediaBrowser.Common.Json.Converters
{
/// <summary>
/// Converts a number to a boolean.
/// This is needed for HDHomerun.
/// </summary>
public class JsonBoolNumberConverter : JsonConverter<bool>
{
/// <inheritdoc />
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Number)
{
return Convert.ToBoolean(reader.GetInt32());
}
return reader.GetBoolean();
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
{
writer.WriteBooleanValue(value);
}
}
}

View File

@@ -0,0 +1,34 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MediaBrowser.Common.Json.Converters
{
/// <summary>
/// Legacy DateTime converter.
/// Milliseconds aren't output if zero by default.
/// </summary>
public class JsonDateTimeConverter : JsonConverter<DateTime>
{
/// <inheritdoc />
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.GetDateTime();
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
if (value.Millisecond == 0)
{
// Remaining ticks value will be 0, manually format.
writer.WriteStringValue(value.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffZ", CultureInfo.InvariantCulture));
}
else
{
writer.WriteStringValue(value);
}
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -13,21 +14,13 @@ namespace MediaBrowser.Common.Json.Converters
public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var guidStr = reader.GetString();
return guidStr == null ? Guid.Empty : new Guid(guidStr);
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options)
{
if (value == Guid.Empty)
{
writer.WriteNullValue();
}
else
{
writer.WriteStringValue(value);
}
writer.WriteStringValue(value.ToString("N", CultureInfo.InvariantCulture));
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MediaBrowser.Common.Json.Converters
{
/// <summary>
/// Converts a GUID object or value to/from JSON.
/// </summary>
public class JsonNullableGuidConverter : JsonConverter<Guid?>
{
/// <inheritdoc />
public override Guid? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var guidStr = reader.GetString();
return guidStr == null ? null : new Guid(guidStr);
}
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, Guid? value, JsonSerializerOptions options)
{
if (value == null || value == Guid.Empty)
{
writer.WriteNullValue();
}
else
{
writer.WriteStringValue(value.Value.ToString("N", CultureInfo.InvariantCulture));
}
}
}
}

View File

@@ -19,54 +19,72 @@ namespace MediaBrowser.Common.Json
/// </summary>
public const string CamelCaseMediaType = "application/json; profile=\"CamelCase\"";
/// <summary>
/// When changing these options, update
/// Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
/// -> AddJellyfinApi
/// -> AddJsonOptions.
/// </summary>
private static readonly JsonSerializerOptions _jsonSerializerOptions = new ()
{
ReadCommentHandling = JsonCommentHandling.Disallow,
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
NumberHandling = JsonNumberHandling.AllowReadingFromString,
PropertyNameCaseInsensitive = true,
Converters =
{
new JsonGuidConverter(),
new JsonNullableGuidConverter(),
new JsonVersionConverter(),
new JsonStringEnumConverter(),
new JsonNullableStructConverterFactory(),
new JsonBoolNumberConverter(),
new JsonDateTimeConverter()
}
};
private static readonly JsonSerializerOptions _pascalCaseJsonSerializerOptions = new (_jsonSerializerOptions)
{
PropertyNamingPolicy = null
};
private static readonly JsonSerializerOptions _camelCaseJsonSerializerOptions = new (_jsonSerializerOptions)
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
/// <summary>
/// Gets the default <see cref="JsonSerializerOptions" /> options.
/// </summary>
/// <remarks>
/// When changing these options, update
/// Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
/// -> AddJellyfinApi
/// -> AddJsonOptions.
/// The return value must not be modified.
/// If the defaults must be modified the author must use the copy constructor.
/// </remarks>
/// <returns>The default <see cref="JsonSerializerOptions" /> options.</returns>
public static JsonSerializerOptions GetOptions()
{
var options = new JsonSerializerOptions
{
ReadCommentHandling = JsonCommentHandling.Disallow,
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
NumberHandling = JsonNumberHandling.AllowReadingFromString
};
options.Converters.Add(new JsonGuidConverter());
options.Converters.Add(new JsonVersionConverter());
options.Converters.Add(new JsonStringEnumConverter());
options.Converters.Add(new JsonNullableStructConverterFactory());
return options;
}
=> _jsonSerializerOptions;
/// <summary>
/// Gets camelCase json options.
/// </summary>
/// <remarks>
/// The return value must not be modified.
/// If the defaults must be modified the author must use the copy constructor.
/// </remarks>
/// <returns>The camelCase <see cref="JsonSerializerOptions" /> options.</returns>
public static JsonSerializerOptions GetCamelCaseOptions()
{
var options = GetOptions();
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
return options;
}
=> _camelCaseJsonSerializerOptions;
/// <summary>
/// Gets PascalCase json options.
/// </summary>
/// <remarks>
/// The return value must not be modified.
/// If the defaults must be modified the author must use the copy constructor.
/// </remarks>
/// <returns>The PascalCase <see cref="JsonSerializerOptions" /> options.</returns>
public static JsonSerializerOptions GetPascalCaseOptions()
{
var options = GetOptions();
options.PropertyNamingPolicy = null;
return options;
}
=> _pascalCaseJsonSerializerOptions;
}
}

View File

@@ -33,7 +33,7 @@ namespace MediaBrowser.Common.Net
/// <summary>
/// IP4Loopback address host.
/// </summary>
public static readonly IPNetAddress IP4Loopback = IPNetAddress.Parse("127.0.0.1/32");
public static readonly IPNetAddress IP4Loopback = IPNetAddress.Parse("127.0.0.1/8");
/// <summary>
/// IP6Loopback address host.

View File

@@ -1,5 +1,3 @@
#pragma warning disable SA1402
using System;
using System.IO;
using System.Reflection;
@@ -7,7 +5,6 @@ using System.Runtime.InteropServices;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common.Plugins
{
@@ -64,14 +61,12 @@ namespace MediaBrowser.Common.Plugins
/// <returns>PluginInfo.</returns>
public virtual PluginInfo GetPluginInfo()
{
var info = new PluginInfo
{
Name = Name,
Version = Version.ToString(),
Description = Description,
Id = Id.ToString(),
CanUninstall = CanUninstall
};
var info = new PluginInfo(
Name,
Version,
Description,
Id,
CanUninstall);
return info;
}
@@ -97,207 +92,4 @@ namespace MediaBrowser.Common.Plugins
Id = assemblyId;
}
}
/// <summary>
/// Provides a common base class for all plugins.
/// </summary>
/// <typeparam name="TConfigurationType">The type of the T configuration type.</typeparam>
public abstract class BasePlugin<TConfigurationType> : BasePlugin, IHasPluginConfiguration
where TConfigurationType : BasePluginConfiguration
{
/// <summary>
/// The configuration sync lock.
/// </summary>
private readonly object _configurationSyncLock = new object();
/// <summary>
/// The configuration save lock.
/// </summary>
private readonly object _configurationSaveLock = new object();
private Action<string> _directoryCreateFn;
/// <summary>
/// The configuration.
/// </summary>
private TConfigurationType _configuration;
/// <summary>
/// Initializes a new instance of the <see cref="BasePlugin{TConfigurationType}" /> class.
/// </summary>
/// <param name="applicationPaths">The application paths.</param>
/// <param name="xmlSerializer">The XML serializer.</param>
protected BasePlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
{
ApplicationPaths = applicationPaths;
XmlSerializer = xmlSerializer;
if (this is IPluginAssembly assemblyPlugin)
{
var assembly = GetType().Assembly;
var assemblyName = assembly.GetName();
var assemblyFilePath = assembly.Location;
var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath));
assemblyPlugin.SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version);
var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true);
if (idAttributes.Length > 0)
{
var attribute = (GuidAttribute)idAttributes[0];
var assemblyId = new Guid(attribute.Value);
assemblyPlugin.SetId(assemblyId);
}
}
if (this is IHasPluginConfiguration hasPluginConfiguration)
{
hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s));
}
}
/// <summary>
/// Gets the application paths.
/// </summary>
/// <value>The application paths.</value>
protected IApplicationPaths ApplicationPaths { get; private set; }
/// <summary>
/// Gets the XML serializer.
/// </summary>
/// <value>The XML serializer.</value>
protected IXmlSerializer XmlSerializer { get; private set; }
/// <summary>
/// Gets the type of configuration this plugin uses.
/// </summary>
/// <value>The type of the configuration.</value>
public Type ConfigurationType => typeof(TConfigurationType);
/// <summary>
/// Gets or sets the event handler that is triggered when this configuration changes.
/// </summary>
public EventHandler<BasePluginConfiguration> ConfigurationChanged { get; set; }
/// <summary>
/// Gets the name the assembly file.
/// </summary>
/// <value>The name of the assembly file.</value>
protected string AssemblyFileName => Path.GetFileName(AssemblyFilePath);
/// <summary>
/// Gets or sets the plugin configuration.
/// </summary>
/// <value>The configuration.</value>
public TConfigurationType Configuration
{
get
{
// Lazy load
if (_configuration == null)
{
lock (_configurationSyncLock)
{
if (_configuration == null)
{
_configuration = LoadConfiguration();
}
}
}
return _configuration;
}
protected set => _configuration = value;
}
/// <summary>
/// Gets the name of the configuration file. Subclasses should override.
/// </summary>
/// <value>The name of the configuration file.</value>
public virtual string ConfigurationFileName => Path.ChangeExtension(AssemblyFileName, ".xml");
/// <summary>
/// Gets the full path to the configuration file.
/// </summary>
/// <value>The configuration file path.</value>
public string ConfigurationFilePath => Path.Combine(ApplicationPaths.PluginConfigurationsPath, ConfigurationFileName);
/// <summary>
/// Gets the plugin configuration.
/// </summary>
/// <value>The configuration.</value>
BasePluginConfiguration IHasPluginConfiguration.Configuration => Configuration;
/// <inheritdoc />
public void SetStartupInfo(Action<string> directoryCreateFn)
{
// hack alert, until the .net core transition is complete
_directoryCreateFn = directoryCreateFn;
}
private TConfigurationType LoadConfiguration()
{
var path = ConfigurationFilePath;
try
{
return (TConfigurationType)XmlSerializer.DeserializeFromFile(typeof(TConfigurationType), path);
}
catch
{
var config = (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType));
SaveConfiguration(config);
return config;
}
}
/// <summary>
/// Saves the current configuration to the file system.
/// </summary>
/// <param name="config">Configuration to save.</param>
public virtual void SaveConfiguration(TConfigurationType config)
{
lock (_configurationSaveLock)
{
_directoryCreateFn(Path.GetDirectoryName(ConfigurationFilePath));
XmlSerializer.SerializeToFile(config, ConfigurationFilePath);
}
}
/// <summary>
/// Saves the current configuration to the file system.
/// </summary>
public virtual void SaveConfiguration()
{
SaveConfiguration(Configuration);
}
/// <inheritdoc />
public virtual void UpdateConfiguration(BasePluginConfiguration configuration)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
Configuration = (TConfigurationType)configuration;
SaveConfiguration(Configuration);
ConfigurationChanged?.Invoke(this, configuration);
}
/// <inheritdoc />
public override PluginInfo GetPluginInfo()
{
var info = base.GetPluginInfo();
info.ConfigurationFileName = ConfigurationFileName;
return info;
}
}
}

View File

@@ -0,0 +1,208 @@
#pragma warning disable SA1649 // File name should match first type name
using System;
using System.IO;
using System.Runtime.InteropServices;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
namespace MediaBrowser.Common.Plugins
{
/// <summary>
/// Provides a common base class for all plugins.
/// </summary>
/// <typeparam name="TConfigurationType">The type of the T configuration type.</typeparam>
public abstract class BasePlugin<TConfigurationType> : BasePlugin, IHasPluginConfiguration
where TConfigurationType : BasePluginConfiguration
{
/// <summary>
/// The configuration sync lock.
/// </summary>
private readonly object _configurationSyncLock = new object();
/// <summary>
/// The configuration save lock.
/// </summary>
private readonly object _configurationSaveLock = new object();
/// <summary>
/// The configuration.
/// </summary>
private TConfigurationType _configuration;
/// <summary>
/// Initializes a new instance of the <see cref="BasePlugin{TConfigurationType}" /> class.
/// </summary>
/// <param name="applicationPaths">The application paths.</param>
/// <param name="xmlSerializer">The XML serializer.</param>
protected BasePlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
{
ApplicationPaths = applicationPaths;
XmlSerializer = xmlSerializer;
if (this is IPluginAssembly assemblyPlugin)
{
var assembly = GetType().Assembly;
var assemblyName = assembly.GetName();
var assemblyFilePath = assembly.Location;
var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath));
if (!Directory.Exists(dataFolderPath) && Version != null)
{
// Try again with the version number appended to the folder name.
dataFolderPath = dataFolderPath + "_" + Version.ToString();
}
assemblyPlugin.SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version);
var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true);
if (idAttributes.Length > 0)
{
var attribute = (GuidAttribute)idAttributes[0];
var assemblyId = new Guid(attribute.Value);
assemblyPlugin.SetId(assemblyId);
}
}
}
/// <summary>
/// Gets the application paths.
/// </summary>
/// <value>The application paths.</value>
protected IApplicationPaths ApplicationPaths { get; private set; }
/// <summary>
/// Gets the XML serializer.
/// </summary>
/// <value>The XML serializer.</value>
protected IXmlSerializer XmlSerializer { get; private set; }
/// <summary>
/// Gets the type of configuration this plugin uses.
/// </summary>
/// <value>The type of the configuration.</value>
public Type ConfigurationType => typeof(TConfigurationType);
/// <summary>
/// Gets or sets the event handler that is triggered when this configuration changes.
/// </summary>
public EventHandler<BasePluginConfiguration> ConfigurationChanged { get; set; }
/// <summary>
/// Gets the name the assembly file.
/// </summary>
/// <value>The name of the assembly file.</value>
protected string AssemblyFileName => Path.GetFileName(AssemblyFilePath);
/// <summary>
/// Gets or sets the plugin configuration.
/// </summary>
/// <value>The configuration.</value>
public TConfigurationType Configuration
{
get
{
// Lazy load
if (_configuration == null)
{
lock (_configurationSyncLock)
{
if (_configuration == null)
{
_configuration = LoadConfiguration();
}
}
}
return _configuration;
}
protected set => _configuration = value;
}
/// <summary>
/// Gets the name of the configuration file. Subclasses should override.
/// </summary>
/// <value>The name of the configuration file.</value>
public virtual string ConfigurationFileName => Path.ChangeExtension(AssemblyFileName, ".xml");
/// <summary>
/// Gets the full path to the configuration file.
/// </summary>
/// <value>The configuration file path.</value>
public string ConfigurationFilePath => Path.Combine(ApplicationPaths.PluginConfigurationsPath, ConfigurationFileName);
/// <summary>
/// Gets the plugin configuration.
/// </summary>
/// <value>The configuration.</value>
BasePluginConfiguration IHasPluginConfiguration.Configuration => Configuration;
/// <summary>
/// Saves the current configuration to the file system.
/// </summary>
/// <param name="config">Configuration to save.</param>
public virtual void SaveConfiguration(TConfigurationType config)
{
lock (_configurationSaveLock)
{
var folder = Path.GetDirectoryName(ConfigurationFilePath);
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
}
XmlSerializer.SerializeToFile(config, ConfigurationFilePath);
}
}
/// <summary>
/// Saves the current configuration to the file system.
/// </summary>
public virtual void SaveConfiguration()
{
SaveConfiguration(Configuration);
}
/// <inheritdoc />
public virtual void UpdateConfiguration(BasePluginConfiguration configuration)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
Configuration = (TConfigurationType)configuration;
SaveConfiguration(Configuration);
ConfigurationChanged?.Invoke(this, configuration);
}
/// <inheritdoc />
public override PluginInfo GetPluginInfo()
{
var info = base.GetPluginInfo();
info.ConfigurationFileName = ConfigurationFileName;
return info;
}
private TConfigurationType LoadConfiguration()
{
var path = ConfigurationFilePath;
try
{
return (TConfigurationType)XmlSerializer.DeserializeFromFile(typeof(TConfigurationType), path);
}
catch
{
var config = (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType));
SaveConfiguration(config);
return config;
}
}
}
}

View File

@@ -0,0 +1,27 @@
using System;
using MediaBrowser.Model.Plugins;
namespace MediaBrowser.Common.Plugins
{
/// <summary>
/// Defines the <see cref="IHasPluginConfiguration" />.
/// </summary>
public interface IHasPluginConfiguration
{
/// <summary>
/// Gets the type of configuration this plugin uses.
/// </summary>
Type ConfigurationType { get; }
/// <summary>
/// Gets the plugin's configuration.
/// </summary>
BasePluginConfiguration Configuration { get; }
/// <summary>
/// Completely overwrites the current configuration with a new copy.
/// </summary>
/// <param name="configuration">The configuration.</param>
void UpdateConfiguration(BasePluginConfiguration configuration);
}
}

View File

@@ -1,44 +1,36 @@
#pragma warning disable CS1591
using System;
using MediaBrowser.Model.Plugins;
using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common.Plugins
{
/// <summary>
/// Interface IPlugin.
/// Defines the <see cref="IPlugin" />.
/// </summary>
public interface IPlugin
{
/// <summary>
/// Gets the name of the plugin.
/// </summary>
/// <value>The name.</value>
string Name { get; }
/// <summary>
/// Gets the description.
/// Gets the Description.
/// </summary>
/// <value>The description.</value>
string Description { get; }
/// <summary>
/// Gets the unique id.
/// </summary>
/// <value>The unique id.</value>
Guid Id { get; }
/// <summary>
/// Gets the plugin version.
/// </summary>
/// <value>The version.</value>
Version Version { get; }
/// <summary>
/// Gets the path to the assembly file.
/// </summary>
/// <value>The assembly file path.</value>
string AssemblyFilePath { get; }
/// <summary>
@@ -49,11 +41,10 @@ namespace MediaBrowser.Common.Plugins
/// <summary>
/// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed.
/// </summary>
/// <value>The data folder path.</value>
string DataFolderPath { get; }
/// <summary>
/// Gets the plugin info.
/// Gets the <see cref="PluginInfo"/>.
/// </summary>
/// <returns>PluginInfo.</returns>
PluginInfo GetPluginInfo();
@@ -63,29 +54,4 @@ namespace MediaBrowser.Common.Plugins
/// </summary>
void OnUninstalling();
}
public interface IHasPluginConfiguration
{
/// <summary>
/// Gets the type of configuration this plugin uses.
/// </summary>
/// <value>The type of the configuration.</value>
Type ConfigurationType { get; }
/// <summary>
/// Gets the plugin's configuration.
/// </summary>
/// <value>The configuration.</value>
BasePluginConfiguration Configuration { get; }
/// <summary>
/// Completely overwrites the current configuration with a new copy
/// Returns true or false indicating success or failure.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <exception cref="ArgumentNullException"><c>configuration</c> is <c>null</c>.</exception>
void UpdateConfiguration(BasePluginConfiguration configuration);
void SetStartupInfo(Action<string> directoryCreateFn);
}
}

View File

@@ -0,0 +1,86 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common.Plugins
{
/// <summary>
/// Defines the <see cref="IPluginManager" />.
/// </summary>
public interface IPluginManager
{
/// <summary>
/// Gets the Plugins.
/// </summary>
IList<LocalPlugin> Plugins { get; }
/// <summary>
/// Creates the plugins.
/// </summary>
void CreatePlugins();
/// <summary>
/// Returns all the assemblies.
/// </summary>
/// <returns>An IEnumerable{Assembly}.</returns>
IEnumerable<Assembly> LoadAssemblies();
/// <summary>
/// Registers the plugin's services with the DI.
/// Note: DI is not yet instantiated yet.
/// </summary>
/// <param name="serviceCollection">A <see cref="ServiceCollection"/> instance.</param>
void RegisterServices(IServiceCollection serviceCollection);
/// <summary>
/// Saves the manifest back to disk.
/// </summary>
/// <param name="manifest">The <see cref="PluginManifest"/> to save.</param>
/// <param name="path">The path where to save the manifest.</param>
/// <returns>True if successful.</returns>
bool SaveManifest(PluginManifest manifest, string path);
/// <summary>
/// Imports plugin details from a folder.
/// </summary>
/// <param name="folder">Folder of the plugin.</param>
void ImportPluginFrom(string folder);
/// <summary>
/// Disable the plugin.
/// </summary>
/// <param name="assembly">The <see cref="Assembly"/> of the plug to disable.</param>
void FailPlugin(Assembly assembly);
/// <summary>
/// Disable the plugin.
/// </summary>
/// <param name="plugin">The <see cref="LocalPlugin"/> of the plug to disable.</param>
void DisablePlugin(LocalPlugin plugin);
/// <summary>
/// Enables the plugin, disabling all other versions.
/// </summary>
/// <param name="plugin">The <see cref="LocalPlugin"/> of the plug to disable.</param>
void EnablePlugin(LocalPlugin plugin);
/// <summary>
/// Attempts to find the plugin with and id of <paramref name="id"/>.
/// </summary>
/// <param name="id">Id of plugin.</param>
/// <param name="version">The version of the plugin to locate.</param>
/// <returns>A <see cref="LocalPlugin"/> if located, or null if not.</returns>
LocalPlugin? GetPlugin(Guid id, Version? version = null);
/// <summary>
/// Removes the plugin.
/// </summary>
/// <param name="plugin">The plugin.</param>
/// <returns>Outcome of the operation.</returns>
bool RemovePlugin(LocalPlugin plugin);
}
}

View File

@@ -1,6 +1,7 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using MediaBrowser.Model.Plugins;
namespace MediaBrowser.Common.Plugins
{
@@ -9,36 +10,48 @@ namespace MediaBrowser.Common.Plugins
/// </summary>
public class LocalPlugin : IEquatable<LocalPlugin>
{
private readonly bool _supported;
private Version? _version;
/// <summary>
/// Initializes a new instance of the <see cref="LocalPlugin"/> class.
/// </summary>
/// <param name="id">The plugin id.</param>
/// <param name="name">The plugin name.</param>
/// <param name="version">The plugin version.</param>
/// <param name="path">The plugin path.</param>
public LocalPlugin(Guid id, string name, Version version, string path)
/// <param name="isSupported"><b>True</b> if Jellyfin supports this version of the plugin.</param>
/// <param name="manifest">The manifest record for this plugin, or null if one does not exist.</param>
public LocalPlugin(string path, bool isSupported, PluginManifest manifest)
{
Id = id;
Name = name;
Version = version;
Path = path;
DllFiles = new List<string>();
_supported = isSupported;
Manifest = manifest;
}
/// <summary>
/// Gets the plugin id.
/// </summary>
public Guid Id { get; }
public Guid Id => Manifest.Id;
/// <summary>
/// Gets the plugin name.
/// </summary>
public string Name { get; }
public string Name => Manifest.Name;
/// <summary>
/// Gets the plugin version.
/// </summary>
public Version Version { get; }
public Version Version
{
get
{
if (_version == null)
{
_version = Version.Parse(Manifest.Version);
}
return _version;
}
}
/// <summary>
/// Gets the plugin path.
@@ -51,26 +64,19 @@ namespace MediaBrowser.Common.Plugins
public List<string> DllFiles { get; }
/// <summary>
/// == operator.
/// Gets or sets the instance of this plugin.
/// </summary>
/// <param name="left">Left item.</param>
/// <param name="right">Right item.</param>
/// <returns>Comparison result.</returns>
public static bool operator ==(LocalPlugin left, LocalPlugin right)
{
return left.Equals(right);
}
public IPlugin? Instance { get; set; }
/// <summary>
/// != operator.
/// Gets a value indicating whether Jellyfin supports this version of the plugin, and it's enabled.
/// </summary>
/// <param name="left">Left item.</param>
/// <param name="right">Right item.</param>
/// <returns>Comparison result.</returns>
public static bool operator !=(LocalPlugin left, LocalPlugin right)
{
return !left.Equals(right);
}
public bool IsEnabledAndSupported => _supported && Manifest.Status >= PluginStatus.Active;
/// <summary>
/// Gets a value indicating whether the plugin has a manifest.
/// </summary>
public PluginManifest Manifest { get; }
/// <summary>
/// Compare two <see cref="LocalPlugin"/>.
@@ -80,10 +86,15 @@ namespace MediaBrowser.Common.Plugins
/// <returns>Comparison result.</returns>
public static int Compare(LocalPlugin a, LocalPlugin b)
{
var compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture);
if (a == null || b == null)
{
throw new ArgumentNullException(a == null ? nameof(a) : nameof(b));
}
var compare = string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
// Id is not equal but name is.
if (a.Id != b.Id && compare == 0)
if (!a.Id.Equals(b.Id) && compare == 0)
{
compare = a.Id.CompareTo(b.Id);
}
@@ -91,8 +102,20 @@ namespace MediaBrowser.Common.Plugins
return compare == 0 ? a.Version.CompareTo(b.Version) : compare;
}
/// <summary>
/// Returns the plugin information.
/// </summary>
/// <returns>A <see cref="PluginInfo"/> instance containing the information.</returns>
public PluginInfo GetPluginInfo()
{
var inst = Instance?.GetPluginInfo() ?? new PluginInfo(Manifest.Name, Version, Manifest.Description, Manifest.Id, true);
inst.Status = Manifest.Status;
inst.HasImage = !string.IsNullOrEmpty(Manifest.ImagePath);
return inst;
}
/// <inheritdoc />
public override bool Equals(object obj)
public override bool Equals(object? obj)
{
return obj is LocalPlugin other && this.Equals(other);
}
@@ -104,16 +127,14 @@ namespace MediaBrowser.Common.Plugins
}
/// <inheritdoc />
public bool Equals(LocalPlugin other)
public bool Equals(LocalPlugin? other)
{
// Do not use == or != for comparison as this class overrides the operators.
if (object.ReferenceEquals(other, null))
if (other == null)
{
return false;
}
return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase)
&& Id.Equals(other.Id);
return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase) && Id.Equals(other.Id) && Version.Equals(other.Version);
}
}
}

View File

@@ -0,0 +1,110 @@
#nullable enable
using System;
using System.Text.Json.Serialization;
using MediaBrowser.Model.Plugins;
namespace MediaBrowser.Common.Plugins
{
/// <summary>
/// Defines a Plugin manifest file.
/// </summary>
public class PluginManifest
{
/// <summary>
/// Initializes a new instance of the <see cref="PluginManifest"/> class.
/// </summary>
public PluginManifest()
{
Category = string.Empty;
Changelog = string.Empty;
Description = string.Empty;
Id = Guid.Empty;
Name = string.Empty;
Owner = string.Empty;
Overview = string.Empty;
TargetAbi = string.Empty;
Version = string.Empty;
}
/// <summary>
/// Gets or sets the category of the plugin.
/// </summary>
[JsonPropertyName("category")]
public string Category { get; set; }
/// <summary>
/// Gets or sets the changelog information.
/// </summary>
[JsonPropertyName("changelog")]
public string Changelog { get; set; }
/// <summary>
/// Gets or sets the description of the plugin.
/// </summary>
[JsonPropertyName("description")]
public string Description { get; set; }
/// <summary>
/// Gets or sets the Global Unique Identifier for the plugin.
/// </summary>
[JsonPropertyName("guid")]
public Guid Id { get; set; }
/// <summary>
/// Gets or sets the Name of the plugin.
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; }
/// <summary>
/// Gets or sets an overview of the plugin.
/// </summary>
[JsonPropertyName("overview")]
public string Overview { get; set; }
/// <summary>
/// Gets or sets the owner of the plugin.
/// </summary>
[JsonPropertyName("owner")]
public string Owner { get; set; }
/// <summary>
/// Gets or sets the compatibility version for the plugin.
/// </summary>
[JsonPropertyName("targetAbi")]
public string TargetAbi { get; set; }
/// <summary>
/// Gets or sets the timestamp of the plugin.
/// </summary>
[JsonPropertyName("timestamp")]
public DateTime Timestamp { get; set; }
/// <summary>
/// Gets or sets the Version number of the plugin.
/// </summary>
[JsonPropertyName("version")]
public string Version { get; set; }
/// <summary>
/// Gets or sets a value indicating the operational status of this plugin.
/// </summary>
[JsonPropertyName("status")]
public PluginStatus Status { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this plugin should automatically update.
/// </summary>
[JsonPropertyName("autoUpdate")]
public bool AutoUpdate { get; set; } = true; // DO NOT MOVE THIS INTO THE CONSTRUCTOR.
/// <summary>
/// Gets or sets the ImagePath
/// Gets or sets a value indicating whether this plugin has an image.
/// Image must be located in the local plugin folder.
/// </summary>
[JsonPropertyName("imagePath")]
public string? ImagePath { get; set; }
}
}

View File

@@ -1,4 +1,4 @@
#pragma warning disable CS1591
#nullable enable
using System;
using System.Collections.Generic;
@@ -9,6 +9,9 @@ using MediaBrowser.Model.Updates;
namespace MediaBrowser.Common.Updates
{
/// <summary>
/// Defines the <see cref="IInstallationManager" />.
/// </summary>
public interface IInstallationManager : IDisposable
{
/// <summary>
@@ -21,12 +24,13 @@ namespace MediaBrowser.Common.Updates
/// </summary>
/// <param name="manifestName">Name of the repository.</param>
/// <param name="manifest">The URL to query.</param>
/// <param name="filterIncompatible">Filter out incompatible plugins.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{IReadOnlyList{PackageInfo}}.</returns>
Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, CancellationToken cancellationToken = default);
Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default);
/// <summary>
/// Gets all available packages.
/// Gets all available packages that are supported by this version.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{IReadOnlyList{PackageInfo}}.</returns>
@@ -37,33 +41,33 @@ namespace MediaBrowser.Common.Updates
/// </summary>
/// <param name="availablePackages">The available packages.</param>
/// <param name="name">The name of the plugin.</param>
/// <param name="guid">The id of the plugin.</param>
/// <param name="id">The id of the plugin.</param>
/// <param name="specificVersion">The version of the plugin.</param>
/// <returns>All plugins matching the requirements.</returns>
IEnumerable<PackageInfo> FilterPackages(
IEnumerable<PackageInfo> availablePackages,
string name = null,
Guid guid = default,
Version specificVersion = null);
string? name = null,
Guid? id = default,
Version? specificVersion = null);
/// <summary>
/// Returns all compatible versions ordered from newest to oldest.
/// </summary>
/// <param name="availablePackages">The available packages.</param>
/// <param name="name">The name.</param>
/// <param name="guid">The guid of the plugin.</param>
/// <param name="id">The id of the plugin.</param>
/// <param name="minVersion">The minimum required version of the plugin.</param>
/// <param name="specificVersion">The specific version of the plugin to install.</param>
/// <returns>All compatible versions ordered from newest to oldest.</returns>
IEnumerable<InstallationInfo> GetCompatibleVersions(
IEnumerable<PackageInfo> availablePackages,
string name = null,
Guid guid = default,
Version minVersion = null,
Version specificVersion = null);
string? name = null,
Guid? id = default,
Version? minVersion = null,
Version? specificVersion = null);
/// <summary>
/// Returns the available plugin updates.
/// Returns the available compatible plugin updates.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The available plugin updates.</returns>
@@ -81,7 +85,7 @@ namespace MediaBrowser.Common.Updates
/// Uninstalls a plugin.
/// </summary>
/// <param name="plugin">The plugin.</param>
void UninstallPlugin(IPlugin plugin);
void UninstallPlugin(LocalPlugin plugin);
/// <summary>
/// Cancels the installation.

View File

@@ -1,14 +1,21 @@
#pragma warning disable CS1591
using System;
using MediaBrowser.Model.Updates;
namespace MediaBrowser.Common.Updates
{
/// <summary>
/// Defines the <see cref="InstallationEventArgs" />.
/// </summary>
public class InstallationEventArgs : EventArgs
{
/// <summary>
/// Gets or sets the <see cref="InstallationInfo"/>.
/// </summary>
public InstallationInfo InstallationInfo { get; set; }
/// <summary>
/// Gets or sets the <see cref="VersionInfo"/>.
/// </summary>
public VersionInfo VersionInfo { get; set; }
}
}

View File

@@ -48,10 +48,10 @@ namespace MediaBrowser.Controller.BaseItemManager
return !baseItem.EnableMediaSourceDisplay;
}
var typeOptions = libraryOptions.GetTypeOptions(GetType().Name);
var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name);
if (typeOptions != null)
{
return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
return typeOptions.MetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
}
if (!libraryOptions.EnableInternetProviders)
@@ -61,7 +61,7 @@ namespace MediaBrowser.Controller.BaseItemManager
var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, GetType().Name, StringComparison.OrdinalIgnoreCase));
return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);
}
/// <inheritdoc />
@@ -79,7 +79,7 @@ namespace MediaBrowser.Controller.BaseItemManager
return !baseItem.EnableMediaSourceDisplay;
}
var typeOptions = libraryOptions.GetTypeOptions(GetType().Name);
var typeOptions = libraryOptions.GetTypeOptions(baseItem.GetType().Name);
if (typeOptions != null)
{
return typeOptions.ImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase);

View File

@@ -17,9 +17,10 @@ namespace MediaBrowser.Controller.Channels
{
public override bool IsVisible(User user)
{
if (user.GetPreference(PreferenceKind.BlockedChannels) != null)
var blockedChannelsPreference = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels);
if (blockedChannelsPreference.Length != 0)
{
if (user.GetPreference(PreferenceKind.BlockedChannels).Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
if (blockedChannelsPreference.Contains(Id))
{
return false;
}
@@ -27,8 +28,7 @@ namespace MediaBrowser.Controller.Channels
else
{
if (!user.HasPermission(PermissionKind.EnableAllChannels)
&& !user.GetPreference(PreferenceKind.EnabledChannels)
.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
&& !user.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels).Contains(Id))
{
return false;
}

View File

@@ -120,7 +120,7 @@ namespace MediaBrowser.Controller.Entities.Audio
protected override bool GetBlockUnratedValue(User user)
{
return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music.ToString());
return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music);
}
public override UnratedItem GetBlockUnratedType()

View File

@@ -145,7 +145,7 @@ namespace MediaBrowser.Controller.Entities.Audio
protected override bool GetBlockUnratedValue(User user)
{
return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music.ToString());
return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music);
}
public override UnratedItem GetBlockUnratedType()

View File

@@ -480,11 +480,11 @@ namespace MediaBrowser.Controller.Entities
return true;
}
var allowed = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders);
var allowed = user.GetPreferenceValues<Guid>(PreferenceKind.EnableContentDeletionFromFolders);
if (SourceType == SourceType.Channel)
{
return allowed.Contains(ChannelId.ToString(""), StringComparer.OrdinalIgnoreCase);
return allowed.Contains(ChannelId);
}
else
{
@@ -492,7 +492,7 @@ namespace MediaBrowser.Controller.Entities
foreach (var folder in collectionFolders)
{
if (allowed.Contains(folder.Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
if (allowed.Contains(folder.Id))
{
return true;
}
@@ -1385,6 +1385,7 @@ namespace MediaBrowser.Controller.Entities
new List<FileSystemMetadata>();
var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false);
await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties in DB are fresh
if (ownedItemsChanged)
{
@@ -1908,7 +1909,7 @@ namespace MediaBrowser.Controller.Entities
return false;
}
return user.GetPreference(PreferenceKind.BlockUnratedItems).Contains(GetBlockUnratedType().ToString());
return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(GetBlockUnratedType());
}
/// <summary>

Some files were not shown because too many files have changed in this diff Show More