mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-12-13 20:43:03 +03:00
Compare commits
147 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a320b26b5 | ||
|
|
63868eca40 | ||
|
|
f6e8493d69 | ||
|
|
3c3b536e81 | ||
|
|
a10eea41ac | ||
|
|
42d0c1ac5f | ||
|
|
b01290013e | ||
|
|
132335a747 | ||
|
|
75d3d120d3 | ||
|
|
e8890cc682 | ||
|
|
e4bf57c739 | ||
|
|
046dd7fa60 | ||
|
|
5e18ab3604 | ||
|
|
7545b1286b | ||
|
|
b99db64f8f | ||
|
|
20810eedbe | ||
|
|
2d88b8346d | ||
|
|
eafaccae5d | ||
|
|
d2851979d4 | ||
|
|
4b6ff7ffa5 | ||
|
|
153123278b | ||
|
|
25c19f79d4 | ||
|
|
0f139e8857 | ||
|
|
e6cc8d5015 | ||
|
|
ef864e24b9 | ||
|
|
ab054d6239 | ||
|
|
19ff447e51 | ||
|
|
4220808b96 | ||
|
|
eb0621a354 | ||
|
|
621c0b9d15 | ||
|
|
fecab1d549 | ||
|
|
bd89cdf8d2 | ||
|
|
557a091865 | ||
|
|
a1773ce97b | ||
|
|
be7411dc58 | ||
|
|
b2a8fd82d8 | ||
|
|
d53120602c | ||
|
|
da09257d58 | ||
|
|
706ac0fafd | ||
|
|
ebd4328f02 | ||
|
|
39b0d69786 | ||
|
|
9f3cebf493 | ||
|
|
20e985a0d1 | ||
|
|
5dbd6f076c | ||
|
|
19a01ccdf3 | ||
|
|
d816995d27 | ||
|
|
a934477850 | ||
|
|
a7f65bd205 | ||
|
|
8138fc3003 | ||
|
|
46a6cd8d1f | ||
|
|
524df2e45d | ||
|
|
a486cd27a9 | ||
|
|
34053b7259 | ||
|
|
d5a7478600 | ||
|
|
ed333dec43 | ||
|
|
c17c32f9dc | ||
|
|
5cc8ed6516 | ||
|
|
cdba6b3d35 | ||
|
|
fa7a8752a9 | ||
|
|
d129afa74e | ||
|
|
bc8a1d2276 | ||
|
|
147f9e1edf | ||
|
|
7796486511 | ||
|
|
ab5ae34595 | ||
|
|
81a17b803d | ||
|
|
cc6afb0971 | ||
|
|
d9a9a23a3c | ||
|
|
dd1fddf79c | ||
|
|
4df7522629 | ||
|
|
9c83a6cef9 | ||
|
|
910819c71c | ||
|
|
a0e047d560 | ||
|
|
34322ba491 | ||
|
|
801dd74ff6 | ||
|
|
129453214f | ||
|
|
ac82fead82 | ||
|
|
9a59ff3c87 | ||
|
|
a16cf8ec0a | ||
|
|
4a2b143028 | ||
|
|
d737c2b84a | ||
|
|
1ad8e54035 | ||
|
|
83dd3e2201 | ||
|
|
d9634b7fc0 | ||
|
|
05b34b2710 | ||
|
|
2dab55a8f2 | ||
|
|
124ab090bc | ||
|
|
03c8216946 | ||
|
|
f72f27ff45 | ||
|
|
71188ad27a | ||
|
|
0d9e8b4f00 | ||
|
|
fbfb23abab | ||
|
|
622c71ce1c | ||
|
|
dbc9256945 | ||
|
|
6b20aaaa6a | ||
|
|
c2097ba5fe | ||
|
|
8884f4f288 | ||
|
|
ce741f541c | ||
|
|
783d6409af | ||
|
|
5bf25ce2cc | ||
|
|
6c2ddd9758 | ||
|
|
8760a298b1 | ||
|
|
c08933c9d9 | ||
|
|
c86c652006 | ||
|
|
c08ce82a04 | ||
|
|
b10178bd1e | ||
|
|
00a608dfab | ||
|
|
480ded0671 | ||
|
|
98c081f0ce | ||
|
|
e77d0ab5fd | ||
|
|
28ef1bc8b2 | ||
|
|
7ebf7014e0 | ||
|
|
89a649cc23 | ||
|
|
24b2991def | ||
|
|
87dde66e92 | ||
|
|
1b82ef905e | ||
|
|
02cc83b807 | ||
|
|
0d8fa795a0 | ||
|
|
b32f15ab1e | ||
|
|
26993e39f7 | ||
|
|
ddedb2d7f1 | ||
|
|
5c0d930dc3 | ||
|
|
13d62c5977 | ||
|
|
9799b6ae81 | ||
|
|
c1dd8f2050 | ||
|
|
853c328763 | ||
|
|
126753a1fe | ||
|
|
24e4fcc3b7 | ||
|
|
aae90a8480 | ||
|
|
11a37884f0 | ||
|
|
49f3579c1b | ||
|
|
259d811b95 | ||
|
|
7e01cce884 | ||
|
|
2e5333c1d4 | ||
|
|
e8e1bbffd9 | ||
|
|
723fe43d2e | ||
|
|
e70a6d41f4 | ||
|
|
9d4417eee3 | ||
|
|
67f41386ba | ||
|
|
b1af8db423 | ||
|
|
91656acabb | ||
|
|
5fa8c83ba4 | ||
|
|
683bc27b27 | ||
|
|
0b6a05cf82 | ||
|
|
2647935b96 | ||
|
|
2a4023c6c7 | ||
|
|
2a2630098b | ||
|
|
79472dce70 |
@@ -7,7 +7,7 @@ parameters:
|
|||||||
default: "ubuntu-latest"
|
default: "ubuntu-latest"
|
||||||
- name: DotNetSdkVersion
|
- name: DotNetSdkVersion
|
||||||
type: string
|
type: string
|
||||||
default: 5.0.100
|
default: 5.0.103
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: CompatibilityCheck
|
- job: CompatibilityCheck
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
default: "ubuntu-latest"
|
default: "ubuntu-latest"
|
||||||
- name: GeneratorVersion
|
- name: GeneratorVersion
|
||||||
type: string
|
type: string
|
||||||
default: "5.0.0-beta2"
|
default: "5.0.1"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: GenerateApiClients
|
- job: GenerateApiClients
|
||||||
@@ -52,7 +52,8 @@ jobs:
|
|||||||
- task: Npm@1
|
- task: Npm@1
|
||||||
displayName: 'Publish stable typescript axios client'
|
displayName: 'Publish stable typescript axios client'
|
||||||
inputs:
|
inputs:
|
||||||
command: publish
|
command: custom
|
||||||
|
customCommand: publish --access public
|
||||||
publishRegistry: useExternalRegistry
|
publishRegistry: useExternalRegistry
|
||||||
publishEndpoint: 'jellyfin-bot for NPM'
|
publishEndpoint: 'jellyfin-bot for NPM'
|
||||||
workingDir: ./apiclient/generated/typescript/axios
|
workingDir: ./apiclient/generated/typescript/axios
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
parameters:
|
parameters:
|
||||||
LinuxImage: 'ubuntu-latest'
|
LinuxImage: 'ubuntu-latest'
|
||||||
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
||||||
DotNetSdkVersion: 5.0.100
|
DotNetSdkVersion: 5.0.103
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: Build
|
- job: Build
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ jobs:
|
|||||||
BuildConfiguration: ubuntu.armhf
|
BuildConfiguration: ubuntu.armhf
|
||||||
Linux.amd64:
|
Linux.amd64:
|
||||||
BuildConfiguration: linux.amd64
|
BuildConfiguration: linux.amd64
|
||||||
|
Linux.amd64-musl:
|
||||||
|
BuildConfiguration: linux.amd64-musl
|
||||||
|
Linux.arm64:
|
||||||
|
BuildConfiguration: linux.arm64
|
||||||
|
Linux.armhf:
|
||||||
|
BuildConfiguration: linux.armhf
|
||||||
Windows.amd64:
|
Windows.amd64:
|
||||||
BuildConfiguration: windows.amd64
|
BuildConfiguration: windows.amd64
|
||||||
MacOS:
|
MacOS:
|
||||||
@@ -187,6 +193,10 @@ jobs:
|
|||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-latest'
|
vmImage: 'ubuntu-latest'
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- name: JellyfinVersion
|
||||||
|
value: $[replace(variables['Build.SourceBranch'],'refs/tags/v','')]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
displayName: 'Use .NET 5.0 sdk'
|
displayName: 'Use .NET 5.0 sdk'
|
||||||
@@ -198,12 +208,19 @@ jobs:
|
|||||||
displayName: 'Build Stable Nuget packages'
|
displayName: 'Build Stable Nuget packages'
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||||
inputs:
|
inputs:
|
||||||
command: 'pack'
|
command: 'custom'
|
||||||
packagesToPack: 'Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj'
|
projects: |
|
||||||
versioningScheme: 'off'
|
Jellyfin.Data/Jellyfin.Data.csproj
|
||||||
|
MediaBrowser.Common/MediaBrowser.Common.csproj
|
||||||
|
MediaBrowser.Controller/MediaBrowser.Controller.csproj
|
||||||
|
MediaBrowser.Model/MediaBrowser.Model.csproj
|
||||||
|
Emby.Naming/Emby.Naming.csproj
|
||||||
|
custom: 'pack'
|
||||||
|
arguments: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion)
|
||||||
|
|
||||||
- task: DotNetCoreCLI@2
|
- task: DotNetCoreCLI@2
|
||||||
displayName: 'Build Unstable Nuget packages'
|
displayName: 'Build Unstable Nuget packages'
|
||||||
|
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||||
inputs:
|
inputs:
|
||||||
command: 'custom'
|
command: 'custom'
|
||||||
projects: |
|
projects: |
|
||||||
@@ -221,18 +238,12 @@ jobs:
|
|||||||
pathToPublish: $(Build.ArtifactStagingDirectory)
|
pathToPublish: $(Build.ArtifactStagingDirectory)
|
||||||
artifactName: Jellyfin Nuget Packages
|
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
|
- task: NuGetCommand@2
|
||||||
displayName: 'Push Nuget packages to stable feed'
|
displayName: 'Push Nuget packages to stable feed'
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||||
inputs:
|
inputs:
|
||||||
command: 'push'
|
command: 'push'
|
||||||
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;$(Build.ArtifactStagingDirectory)/**/*.snupkg'
|
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
|
||||||
nuGetFeedType: 'external'
|
nuGetFeedType: 'external'
|
||||||
publishFeedCredentials: 'NugetOrg'
|
publishFeedCredentials: 'NugetOrg'
|
||||||
allowPackageConflicts: true # This ignores an error if the version already exists
|
allowPackageConflicts: true # This ignores an error if the version already exists
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ parameters:
|
|||||||
default: "tests/**/*Tests.csproj"
|
default: "tests/**/*Tests.csproj"
|
||||||
- name: DotNetSdkVersion
|
- name: DotNetSdkVersion
|
||||||
type: string
|
type: string
|
||||||
default: 5.0.100
|
default: 5.0.103
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: Test
|
- job: Test
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ variables:
|
|||||||
- name: RestoreBuildProjects
|
- name: RestoreBuildProjects
|
||||||
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
||||||
- name: DotNetSdkVersion
|
- name: DotNetSdkVersion
|
||||||
value: 5.0.100
|
value: 5.0.103
|
||||||
|
|
||||||
pr:
|
pr:
|
||||||
autoCancel: true
|
autoCancel: true
|
||||||
|
|||||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
- name: Setup .NET Core
|
- name: Setup .NET Core
|
||||||
uses: actions/setup-dotnet@v1
|
uses: actions/setup-dotnet@v1
|
||||||
with:
|
with:
|
||||||
dotnet-version: '5.0.100'
|
dotnet-version: '5.0.x'
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
- [h1nk](https://github.com/h1nk)
|
- [h1nk](https://github.com/h1nk)
|
||||||
- [hawken93](https://github.com/hawken93)
|
- [hawken93](https://github.com/hawken93)
|
||||||
- [HelloWorld017](https://github.com/HelloWorld017)
|
- [HelloWorld017](https://github.com/HelloWorld017)
|
||||||
|
- [ikomhoog](https://github.com/ikomhoog)
|
||||||
- [jftuga](https://github.com/jftuga)
|
- [jftuga](https://github.com/jftuga)
|
||||||
- [joern-h](https://github.com/joern-h)
|
- [joern-h](https://github.com/joern-h)
|
||||||
- [joshuaboniface](https://github.com/joshuaboniface)
|
- [joshuaboniface](https://github.com/joshuaboniface)
|
||||||
@@ -141,6 +142,7 @@
|
|||||||
- [Pusta](https://github.com/pusta)
|
- [Pusta](https://github.com/pusta)
|
||||||
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
|
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
|
||||||
- [skyfrk](https://github.com/skyfrk)
|
- [skyfrk](https://github.com/skyfrk)
|
||||||
|
- [ianjazz246](https://github.com/ianjazz246)
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
|
using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
|
||||||
{
|
{
|
||||||
|
// If this using are changed to single lines, then write.Flush needs to be appended before the return.
|
||||||
using (var writer = XmlWriter.Create(builder, settings))
|
using (var writer = XmlWriter.Create(builder, settings))
|
||||||
{
|
{
|
||||||
// writer.WriteStartDocument();
|
// writer.WriteStartDocument();
|
||||||
|
|||||||
@@ -395,7 +395,8 @@ namespace Emby.Dlna
|
|||||||
{
|
{
|
||||||
Directory.CreateDirectory(systemProfilesPath);
|
Directory.CreateDirectory(systemProfilesPath);
|
||||||
|
|
||||||
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
|
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||||
|
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
{
|
{
|
||||||
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Dlna.PlayTo;
|
using Emby.Dlna.PlayTo;
|
||||||
using Emby.Dlna.Ssdp;
|
using Emby.Dlna.Ssdp;
|
||||||
|
using Jellyfin.Networking.Configuration;
|
||||||
using Jellyfin.Networking.Manager;
|
using Jellyfin.Networking.Manager;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
@@ -52,6 +53,8 @@ namespace Emby.Dlna.Main
|
|||||||
private readonly ISocketFactory _socketFactory;
|
private readonly ISocketFactory _socketFactory;
|
||||||
private readonly INetworkManager _networkManager;
|
private readonly INetworkManager _networkManager;
|
||||||
private readonly object _syncLock = new object();
|
private readonly object _syncLock = new object();
|
||||||
|
private readonly NetworkConfiguration _netConfig;
|
||||||
|
private readonly bool _disabled;
|
||||||
|
|
||||||
private PlayToManager _manager;
|
private PlayToManager _manager;
|
||||||
private SsdpDevicePublisher _publisher;
|
private SsdpDevicePublisher _publisher;
|
||||||
@@ -122,10 +125,22 @@ namespace Emby.Dlna.Main
|
|||||||
httpClientFactory,
|
httpClientFactory,
|
||||||
config);
|
config);
|
||||||
Current = this;
|
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; }
|
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 IContentDirectory ContentDirectory { get; private set; }
|
||||||
|
|
||||||
public IConnectionManager ConnectionManager { get; private set; }
|
public IConnectionManager ConnectionManager { get; private set; }
|
||||||
@@ -136,6 +151,12 @@ namespace Emby.Dlna.Main
|
|||||||
{
|
{
|
||||||
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
|
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();
|
ReloadComponents();
|
||||||
|
|
||||||
_config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
|
_config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
|
||||||
@@ -152,6 +173,7 @@ namespace Emby.Dlna.Main
|
|||||||
private void ReloadComponents()
|
private void ReloadComponents()
|
||||||
{
|
{
|
||||||
var options = _config.GetDlnaConfiguration();
|
var options = _config.GetDlnaConfiguration();
|
||||||
|
Enabled = options.EnableServer;
|
||||||
|
|
||||||
StartSsdpHandler();
|
StartSsdpHandler();
|
||||||
|
|
||||||
@@ -206,7 +228,10 @@ namespace Emby.Dlna.Main
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
|
if (communicationsServer != null)
|
||||||
|
{
|
||||||
|
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -290,12 +315,18 @@ namespace Emby.Dlna.Main
|
|||||||
|
|
||||||
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
|
_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);
|
||||||
|
if (!string.IsNullOrEmpty(_appHost.PublishedServerUrl))
|
||||||
|
{
|
||||||
|
// DLNA will only work over http, so we must reset to http:// : {port}.
|
||||||
|
uri.Scheme = "http";
|
||||||
|
uri.Port = _netConfig.HttpServerPortNumber;
|
||||||
|
}
|
||||||
|
|
||||||
var device = new SsdpRootDevice
|
var device = new SsdpRootDevice
|
||||||
{
|
{
|
||||||
CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
|
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,
|
Address = address.Address,
|
||||||
PrefixLength = address.PrefixLength,
|
PrefixLength = address.PrefixLength,
|
||||||
FriendlyName = "Jellyfin",
|
FriendlyName = "Jellyfin",
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
|
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -340,10 +340,19 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
|
|
||||||
var playlist = new PlaylistItem[len];
|
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++)
|
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);
|
_logger.LogDebug("{0} - Playlist created", _session.DeviceName);
|
||||||
@@ -817,7 +826,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return SendPlayCommand(data as PlayRequest, cancellationToken);
|
return SendPlayCommand(data as PlayRequest, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name == SessionMessageType.PlayState)
|
if (name == SessionMessageType.Playstate)
|
||||||
{
|
{
|
||||||
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
|
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
|
||||||
}
|
}
|
||||||
@@ -887,16 +896,16 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
var parts = url.Split('/');
|
var parts = url.Split('/');
|
||||||
|
|
||||||
for (var i = 0; i < parts.Length; i++)
|
for (var i = 0; i < parts.Length - 1; i++)
|
||||||
{
|
{
|
||||||
var part = parts[i];
|
var part = parts[i];
|
||||||
|
|
||||||
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
|
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
|
string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
if (parts.Length > i + 1)
|
if (Guid.TryParse(parts[i + 1], out var result))
|
||||||
{
|
{
|
||||||
return Guid.Parse(parts[i + 1]);
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ namespace Emby.Dlna.Profiles
|
|||||||
Container = "ts,mpegts",
|
Container = "ts,mpegts",
|
||||||
Type = DlnaProfileType.Video,
|
Type = DlnaProfileType.Video,
|
||||||
VideoCodec = "mpeg1video,mpeg2video,h264",
|
VideoCodec = "mpeg1video,mpeg2video,h264",
|
||||||
AudioCodec = "ac3,mp2,mp3,aac"
|
AudioCodec = "aac,ac3,mp2"
|
||||||
},
|
},
|
||||||
new DirectPlayProfile
|
new DirectPlayProfile
|
||||||
{
|
{
|
||||||
@@ -92,7 +92,7 @@ namespace Emby.Dlna.Profiles
|
|||||||
{
|
{
|
||||||
Container = "ts",
|
Container = "ts",
|
||||||
VideoCodec = "h264",
|
VideoCodec = "h264",
|
||||||
AudioCodec = "ac3,aac,mp3",
|
AudioCodec = "aac,ac3,mp2",
|
||||||
Type = DlnaProfileType.Video
|
Type = DlnaProfileType.Video
|
||||||
},
|
},
|
||||||
new TranscodingProfile
|
new TranscodingProfile
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ namespace Emby.Dlna.Profiles
|
|||||||
Container = "ts,mpegts",
|
Container = "ts,mpegts",
|
||||||
Type = DlnaProfileType.Video,
|
Type = DlnaProfileType.Video,
|
||||||
VideoCodec = "mpeg1video,mpeg2video,h264",
|
VideoCodec = "mpeg1video,mpeg2video,h264",
|
||||||
AudioCodec = "ac3,mp2,mp3,aac"
|
AudioCodec = "aac,ac3,mp2"
|
||||||
},
|
},
|
||||||
new DirectPlayProfile
|
new DirectPlayProfile
|
||||||
{
|
{
|
||||||
@@ -94,7 +94,7 @@ namespace Emby.Dlna.Profiles
|
|||||||
{
|
{
|
||||||
Container = "ts",
|
Container = "ts",
|
||||||
VideoCodec = "h264",
|
VideoCodec = "h264",
|
||||||
AudioCodec = "mp3",
|
AudioCodec = "aac,ac3,mp2",
|
||||||
Type = DlnaProfileType.Video
|
Type = DlnaProfileType.Video
|
||||||
},
|
},
|
||||||
new TranscodingProfile
|
new TranscodingProfile
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
<XmlRootAttributes />
|
<XmlRootAttributes />
|
||||||
<DirectPlayProfiles>
|
<DirectPlayProfiles>
|
||||||
<DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" />
|
<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="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" />
|
||||||
<DirectPlayProfile container="mp4" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" />
|
<DirectPlayProfile container="mp4" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" />
|
||||||
<DirectPlayProfile container="aac,mp3,wav" type="Audio" />
|
<DirectPlayProfile container="aac,mp3,wav" type="Audio" />
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
</DirectPlayProfiles>
|
</DirectPlayProfiles>
|
||||||
<TranscodingProfiles>
|
<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="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" />
|
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||||
</TranscodingProfiles>
|
</TranscodingProfiles>
|
||||||
<ContainerProfiles>
|
<ContainerProfiles>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
<XmlRootAttributes />
|
<XmlRootAttributes />
|
||||||
<DirectPlayProfiles>
|
<DirectPlayProfiles>
|
||||||
<DirectPlayProfile container="avi" audioCodec="mp2,mp3" videoCodec="mpeg4" type="Video" />
|
<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="mpeg" audioCodec="mp2" videoCodec="mpeg1video,mpeg2video" type="Video" />
|
||||||
<DirectPlayProfile container="mp4,mkv,m4v" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" />
|
<DirectPlayProfile container="mp4,mkv,m4v" audioCodec="aac,ac3" videoCodec="h264,mpeg4" type="Video" />
|
||||||
<DirectPlayProfile container="aac,mp3,wav" type="Audio" />
|
<DirectPlayProfile container="aac,mp3,wav" type="Audio" />
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
</DirectPlayProfiles>
|
</DirectPlayProfiles>
|
||||||
<TranscodingProfiles>
|
<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="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" />
|
<TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" minSegments="0" segmentLength="0" breakOnNonKeyFrames="false" />
|
||||||
</TranscodingProfiles>
|
</TranscodingProfiles>
|
||||||
<ContainerProfiles>
|
<ContainerProfiles>
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ namespace Emby.Naming.Video
|
|||||||
|
|
||||||
private static bool TryClean(string name, Regex expression, out ReadOnlySpan<char> newName)
|
private static bool TryClean(string name, Regex expression, out ReadOnlySpan<char> newName)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
|
newName = ReadOnlySpan<char>.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var match = expression.Match(name);
|
var match = expression.Match(name);
|
||||||
int index = match.Index;
|
int index = match.Index;
|
||||||
if (match.Success && index != 0)
|
if (match.Success && index != 0)
|
||||||
@@ -41,7 +47,7 @@ namespace Emby.Naming.Video
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
newName = string.Empty;
|
newName = ReadOnlySpan<char>.Empty;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,10 +75,6 @@ namespace Emby.Notifications
|
|||||||
Type = NotificationType.VideoPlaybackStopped.ToString()
|
Type = NotificationType.VideoPlaybackStopped.ToString()
|
||||||
},
|
},
|
||||||
new NotificationTypeInfo
|
new NotificationTypeInfo
|
||||||
{
|
|
||||||
Type = NotificationType.CameraImageUploaded.ToString()
|
|
||||||
},
|
|
||||||
new NotificationTypeInfo
|
|
||||||
{
|
{
|
||||||
Type = NotificationType.UserLockedOut.ToString()
|
Type = NotificationType.UserLockedOut.ToString()
|
||||||
},
|
},
|
||||||
@@ -114,10 +110,6 @@ namespace Emby.Notifications
|
|||||||
{
|
{
|
||||||
note.Category = _localization.GetLocalizedString("Plugin");
|
note.Category = _localization.GetLocalizedString("Plugin");
|
||||||
}
|
}
|
||||||
else if (note.Type.IndexOf("CameraImageUploaded", StringComparison.OrdinalIgnoreCase) != -1)
|
|
||||||
{
|
|
||||||
note.Category = _localization.GetLocalizedString("Sync");
|
|
||||||
}
|
|
||||||
else if (note.Type.IndexOf("UserLockedOut", StringComparison.OrdinalIgnoreCase) != -1)
|
else if (note.Type.IndexOf("UserLockedOut", StringComparison.OrdinalIgnoreCase) != -1)
|
||||||
{
|
{
|
||||||
note.Category = _localization.GetLocalizedString("User");
|
note.Category = _localization.GetLocalizedString("User");
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
|
|
||||||
Directory.CreateDirectory(directory);
|
Directory.CreateDirectory(directory);
|
||||||
// Save it after load in case we got new items
|
// Save it after load in case we got new items
|
||||||
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
|
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||||
|
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
{
|
{
|
||||||
fs.Write(newBytes, 0, newBytesLen);
|
fs.Write(newBytes, 0, newBytesLen);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
@@ -42,6 +43,7 @@ using Emby.Server.Implementations.Serialization;
|
|||||||
using Emby.Server.Implementations.Session;
|
using Emby.Server.Implementations.Session;
|
||||||
using Emby.Server.Implementations.SyncPlay;
|
using Emby.Server.Implementations.SyncPlay;
|
||||||
using Emby.Server.Implementations.TV;
|
using Emby.Server.Implementations.TV;
|
||||||
|
using Emby.Server.Implementations.Udp;
|
||||||
using Emby.Server.Implementations.Updates;
|
using Emby.Server.Implementations.Updates;
|
||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
using Jellyfin.Networking.Configuration;
|
using Jellyfin.Networking.Configuration;
|
||||||
@@ -97,6 +99,7 @@ using MediaBrowser.Providers.Subtitles;
|
|||||||
using MediaBrowser.XbmcMetadata.Providers;
|
using MediaBrowser.XbmcMetadata.Providers;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Prometheus.DotNetRuntime;
|
using Prometheus.DotNetRuntime;
|
||||||
@@ -116,10 +119,13 @@ namespace Emby.Server.Implementations
|
|||||||
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
|
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
|
||||||
|
|
||||||
private readonly IFileSystem _fileSystemManager;
|
private readonly IFileSystem _fileSystemManager;
|
||||||
|
private readonly IConfiguration _startupConfig;
|
||||||
private readonly IXmlSerializer _xmlSerializer;
|
private readonly IXmlSerializer _xmlSerializer;
|
||||||
private readonly IJsonSerializer _jsonSerializer;
|
private readonly IJsonSerializer _jsonSerializer;
|
||||||
private readonly IStartupOptions _startupOptions;
|
private readonly IStartupOptions _startupOptions;
|
||||||
|
private readonly IPluginManager _pluginManager;
|
||||||
|
|
||||||
|
private List<Type> _creatingInstances;
|
||||||
private IMediaEncoder _mediaEncoder;
|
private IMediaEncoder _mediaEncoder;
|
||||||
private ISessionManager _sessionManager;
|
private ISessionManager _sessionManager;
|
||||||
private string[] _urlPrefixes;
|
private string[] _urlPrefixes;
|
||||||
@@ -181,16 +187,6 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
protected IServiceCollection ServiceCollection { get; }
|
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>
|
/// <summary>
|
||||||
/// Gets the logger factory.
|
/// Gets the logger factory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -234,6 +230,11 @@ namespace Emby.Server.Implementations
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int HttpsPort { get; private set; }
|
public int HttpsPort { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the value of the PublishedServerUrl setting.
|
||||||
|
/// </summary>
|
||||||
|
public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the server configuration manager.
|
/// Gets the server configuration manager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -246,12 +247,14 @@ namespace Emby.Server.Implementations
|
|||||||
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
|
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
|
||||||
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
||||||
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
|
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
|
||||||
|
/// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param>
|
||||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||||
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
|
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
|
||||||
public ApplicationHost(
|
public ApplicationHost(
|
||||||
IServerApplicationPaths applicationPaths,
|
IServerApplicationPaths applicationPaths,
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
IStartupOptions options,
|
IStartupOptions options,
|
||||||
|
IConfiguration startupConfig,
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
IServiceCollection serviceCollection)
|
IServiceCollection serviceCollection)
|
||||||
{
|
{
|
||||||
@@ -275,6 +278,7 @@ namespace Emby.Server.Implementations
|
|||||||
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
|
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
|
||||||
|
|
||||||
_startupOptions = options;
|
_startupOptions = options;
|
||||||
|
_startupConfig = startupConfig;
|
||||||
|
|
||||||
// Initialize runtime stat collection
|
// Initialize runtime stat collection
|
||||||
if (ServerConfigurationManager.Configuration.EnableMetrics)
|
if (ServerConfigurationManager.Configuration.EnableMetrics)
|
||||||
@@ -284,16 +288,16 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
|
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;
|
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
|
||||||
ApplicationVersionString = ApplicationVersion.ToString(3);
|
ApplicationVersionString = ApplicationVersion.ToString(3);
|
||||||
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
|
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
|
||||||
|
|
||||||
|
_pluginManager = new PluginManager(
|
||||||
|
LoggerFactory.CreateLogger<PluginManager>(),
|
||||||
|
this,
|
||||||
|
ServerConfigurationManager.Configuration,
|
||||||
|
ApplicationPaths.PluginsPath,
|
||||||
|
ApplicationVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -393,16 +397,41 @@ namespace Emby.Server.Implementations
|
|||||||
/// <returns>System.Object.</returns>
|
/// <returns>System.Object.</returns>
|
||||||
protected object CreateInstanceSafe(Type type)
|
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
|
try
|
||||||
{
|
{
|
||||||
|
_creatingInstances.Add(type);
|
||||||
Logger.LogDebug("Creating instance of {Type}", type);
|
Logger.LogDebug("Creating instance of {Type}", type);
|
||||||
return ActivatorUtilities.CreateInstance(ServiceProvider, type);
|
return ActivatorUtilities.CreateInstance(ServiceProvider, type);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Error creating {Type}", type);
|
Logger.LogError(ex, "Error creating {Type}", type);
|
||||||
|
// If this is a plugin fail it.
|
||||||
|
_pluginManager.FailPlugin(type.Assembly);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_creatingInstances.Remove(type);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -412,11 +441,7 @@ namespace Emby.Server.Implementations
|
|||||||
/// <returns>``0.</returns>
|
/// <returns>``0.</returns>
|
||||||
public T Resolve<T>() => ServiceProvider.GetService<T>();
|
public T Resolve<T>() => ServiceProvider.GetService<T>();
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// Gets the export types.
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">The type.</typeparam>
|
|
||||||
/// <returns>IEnumerable{Type}.</returns>
|
|
||||||
public IEnumerable<Type> GetExportTypes<T>()
|
public IEnumerable<Type> GetExportTypes<T>()
|
||||||
{
|
{
|
||||||
var currentType = typeof(T);
|
var currentType = typeof(T);
|
||||||
@@ -445,6 +470,27 @@ namespace Emby.Server.Implementations
|
|||||||
return parts;
|
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>
|
/// <summary>
|
||||||
/// Runs the startup tasks.
|
/// Runs the startup tasks.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -456,6 +502,7 @@ namespace Emby.Server.Implementations
|
|||||||
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
|
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
|
||||||
|
|
||||||
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
|
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
|
||||||
|
ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated;
|
||||||
|
|
||||||
_mediaEncoder.SetFFmpegPath();
|
_mediaEncoder.SetFFmpegPath();
|
||||||
|
|
||||||
@@ -505,11 +552,18 @@ namespace Emby.Server.Implementations
|
|||||||
HttpsPort = NetworkConfiguration.DefaultHttpsPort;
|
HttpsPort = NetworkConfiguration.DefaultHttpsPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CertificateInfo = new CertificateInfo
|
||||||
|
{
|
||||||
|
Path = networkConfiguration.CertificatePath,
|
||||||
|
Password = networkConfiguration.CertificatePassword
|
||||||
|
};
|
||||||
|
Certificate = GetCertificate(CertificateInfo);
|
||||||
|
|
||||||
DiscoverTypes();
|
DiscoverTypes();
|
||||||
|
|
||||||
RegisterServices();
|
RegisterServices();
|
||||||
|
|
||||||
RegisterPluginServices();
|
_pluginManager.RegisterServices(ServiceCollection);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -523,7 +577,7 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
ServiceCollection.AddSingleton(ConfigurationManager);
|
ServiceCollection.AddSingleton(ConfigurationManager);
|
||||||
ServiceCollection.AddSingleton<IApplicationHost>(this);
|
ServiceCollection.AddSingleton<IApplicationHost>(this);
|
||||||
|
ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
|
||||||
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
|
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
|
||||||
|
|
||||||
ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
|
ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
|
||||||
@@ -714,7 +768,7 @@ namespace Emby.Server.Implementations
|
|||||||
// Don't use an empty string password
|
// Don't use an empty string password
|
||||||
var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.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;
|
// localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA;
|
||||||
if (!localCert.HasPrivateKey)
|
if (!localCert.HasPrivateKey)
|
||||||
{
|
{
|
||||||
@@ -768,34 +822,7 @@ namespace Emby.Server.Implementations
|
|||||||
}
|
}
|
||||||
|
|
||||||
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
|
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
|
||||||
_plugins = GetExports<IPlugin>()
|
_pluginManager.CreatePlugins();
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_urlPrefixes = GetUrlPrefixes().ToArray();
|
_urlPrefixes = GetUrlPrefixes().ToArray();
|
||||||
|
|
||||||
@@ -834,22 +861,6 @@ namespace Emby.Server.Implementations
|
|||||||
_allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray();
|
_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)
|
private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies)
|
||||||
{
|
{
|
||||||
foreach (var ass in assemblies)
|
foreach (var ass in assemblies)
|
||||||
@@ -862,11 +873,13 @@ namespace Emby.Server.Implementations
|
|||||||
catch (FileNotFoundException ex)
|
catch (FileNotFoundException ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
|
Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
|
||||||
|
_pluginManager.FailPlugin(ass);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
catch (TypeLoadException ex)
|
catch (TypeLoadException ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName);
|
Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName);
|
||||||
|
_pluginManager.FailPlugin(ass);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -912,11 +925,11 @@ namespace Emby.Server.Implementations
|
|||||||
protected void OnConfigurationUpdated(object sender, EventArgs e)
|
protected void OnConfigurationUpdated(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
var requiresRestart = false;
|
var requiresRestart = false;
|
||||||
|
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
|
||||||
|
|
||||||
// Don't do anything if these haven't been set yet
|
// Don't do anything if these haven't been set yet
|
||||||
if (HttpPort != 0 && HttpsPort != 0)
|
if (HttpPort != 0 && HttpsPort != 0)
|
||||||
{
|
{
|
||||||
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
|
|
||||||
// Need to restart if ports have changed
|
// Need to restart if ports have changed
|
||||||
if (networkConfiguration.HttpServerPortNumber != HttpPort ||
|
if (networkConfiguration.HttpServerPortNumber != HttpPort ||
|
||||||
networkConfiguration.HttpsPortNumber != HttpsPort)
|
networkConfiguration.HttpsPortNumber != HttpsPort)
|
||||||
@@ -936,10 +949,7 @@ namespace Emby.Server.Implementations
|
|||||||
requiresRestart = true;
|
requiresRestart = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentCertPath = CertificateInfo?.Path;
|
if (ValidateSslCertificate(networkConfiguration))
|
||||||
var newCertPath = ServerConfigurationManager.Configuration.CertificatePath;
|
|
||||||
|
|
||||||
if (!string.Equals(currentCertPath, newCertPath, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
requiresRestart = true;
|
requiresRestart = true;
|
||||||
}
|
}
|
||||||
@@ -952,6 +962,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>
|
/// <summary>
|
||||||
/// Notifies that the kernel that a change has been made that requires a restart.
|
/// Notifies that the kernel that a change has been made that requires a restart.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1005,129 +1042,15 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
protected abstract void RestartInternal();
|
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>
|
/// <summary>
|
||||||
/// Gets the composable part assemblies.
|
/// Gets the composable part assemblies.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>IEnumerable{Assembly}.</returns>
|
/// <returns>IEnumerable{Assembly}.</returns>
|
||||||
protected IEnumerable<Assembly> GetComposablePartAssemblies()
|
protected IEnumerable<Assembly> GetComposablePartAssemblies()
|
||||||
{
|
{
|
||||||
if (Directory.Exists(ApplicationPaths.PluginsPath))
|
foreach (var p in _pluginManager.LoadAssemblies())
|
||||||
{
|
{
|
||||||
_pluginsManifests = GetLocalPlugins(ApplicationPaths.PluginsPath).ToList();
|
yield return p;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include composable parts in the Model assembly
|
// Include composable parts in the Model assembly
|
||||||
@@ -1236,10 +1159,10 @@ namespace Emby.Server.Implementations
|
|||||||
public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
|
public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
|
||||||
{
|
{
|
||||||
// Published server ends with a /
|
// Published server ends with a /
|
||||||
if (_startupOptions.PublishedServerUrl != null)
|
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
||||||
{
|
{
|
||||||
// Published server ends with a '/', so we need to remove it.
|
// Published server ends with a '/', so we need to remove it.
|
||||||
return _startupOptions.PublishedServerUrl.ToString().Trim('/');
|
return PublishedServerUrl.Trim('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
string smart = NetManager.GetBindInterface(ipAddress, out port);
|
string smart = NetManager.GetBindInterface(ipAddress, out port);
|
||||||
@@ -1256,10 +1179,10 @@ namespace Emby.Server.Implementations
|
|||||||
public string GetSmartApiUrl(HttpRequest request, int? port = null)
|
public string GetSmartApiUrl(HttpRequest request, int? port = null)
|
||||||
{
|
{
|
||||||
// Published server ends with a /
|
// Published server ends with a /
|
||||||
if (_startupOptions.PublishedServerUrl != null)
|
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
||||||
{
|
{
|
||||||
// Published server ends with a '/', so we need to remove it.
|
// Published server ends with a '/', so we need to remove it.
|
||||||
return _startupOptions.PublishedServerUrl.ToString().Trim('/');
|
return PublishedServerUrl.Trim('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
string smart = NetManager.GetBindInterface(request, out port);
|
string smart = NetManager.GetBindInterface(request, out port);
|
||||||
@@ -1276,10 +1199,10 @@ namespace Emby.Server.Implementations
|
|||||||
public string GetSmartApiUrl(string hostname, int? port = null)
|
public string GetSmartApiUrl(string hostname, int? port = null)
|
||||||
{
|
{
|
||||||
// Published server ends with a /
|
// Published server ends with a /
|
||||||
if (_startupOptions.PublishedServerUrl != null)
|
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
||||||
{
|
{
|
||||||
// Published server ends with a '/', so we need to remove it.
|
// Published server ends with a '/', so we need to remove it.
|
||||||
return _startupOptions.PublishedServerUrl.ToString().Trim('/');
|
return PublishedServerUrl.Trim('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
string smart = NetManager.GetBindInterface(hostname, out port);
|
string smart = NetManager.GetBindInterface(hostname, out port);
|
||||||
@@ -1369,17 +1292,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()
|
public IEnumerable<Assembly> GetApiPluginAssemblies()
|
||||||
{
|
{
|
||||||
var assemblies = _allConcreteTypes
|
var assemblies = _allConcreteTypes
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ namespace Emby.Server.Implementations.Collections
|
|||||||
|
|
||||||
var name = _localizationManager.GetLocalizedString("Collections");
|
var name = _localizationManager.GetLocalizedString("Collections");
|
||||||
|
|
||||||
await _libraryManager.AddVirtualFolder(name, CollectionType.BoxSets, libraryOptions, true).ConfigureAwait(false);
|
await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.BoxSets, libraryOptions, true).ConfigureAwait(false);
|
||||||
|
|
||||||
return FindFolders(path).First();
|
return FindFolders(path).First();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,38 +88,12 @@ namespace Emby.Server.Implementations.Configuration
|
|||||||
var newConfig = (ServerConfiguration)newConfiguration;
|
var newConfig = (ServerConfiguration)newConfiguration;
|
||||||
|
|
||||||
ValidateMetadataPath(newConfig);
|
ValidateMetadataPath(newConfig);
|
||||||
ValidateSslCertificate(newConfig);
|
|
||||||
|
|
||||||
ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration>(newConfig));
|
ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration>(newConfig));
|
||||||
|
|
||||||
base.ReplaceConfiguration(newConfiguration);
|
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>
|
/// <summary>
|
||||||
/// Validates the metadata path.
|
/// Validates the metadata path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -582,7 +582,26 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
{
|
{
|
||||||
baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary);
|
baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary);
|
||||||
baseItemPerson.Id = entity.Id.ToString("N", CultureInfo.InvariantCulture);
|
baseItemPerson.Id = entity.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||||
baseItemPerson.ImageBlurHashes = dto.ImageBlurHashes;
|
if (dto.ImageBlurHashes != null)
|
||||||
|
{
|
||||||
|
// Only add BlurHash for the person's image.
|
||||||
|
baseItemPerson.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
|
||||||
|
foreach (var (imageType, blurHash) in dto.ImageBlurHashes)
|
||||||
|
{
|
||||||
|
if (blurHash != null)
|
||||||
|
{
|
||||||
|
baseItemPerson.ImageBlurHashes[imageType] = new Dictionary<string, string>();
|
||||||
|
foreach (var (imageId, blurHashValue) in blurHash)
|
||||||
|
{
|
||||||
|
if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
baseItemPerson.ImageBlurHashes[imageType][imageId] = blurHashValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
list.Add(baseItemPerson);
|
list.Add(baseItemPerson);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1138,7 +1157,10 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
if (episodeSeries != null)
|
if (episodeSeries != null)
|
||||||
{
|
{
|
||||||
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
|
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
|
||||||
AttachPrimaryImageAspectRatio(dto, episodeSeries);
|
if (dto.ImageTags == null || !dto.ImageTags.ContainsKey(ImageType.Primary))
|
||||||
|
{
|
||||||
|
AttachPrimaryImageAspectRatio(dto, episodeSeries);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1185,7 +1207,10 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
if (series != null)
|
if (series != null)
|
||||||
{
|
{
|
||||||
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
|
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
|
||||||
AttachPrimaryImageAspectRatio(dto, series);
|
if (dto.ImageTags == null || !dto.ImageTags.ContainsKey(ImageType.Primary))
|
||||||
|
{
|
||||||
|
AttachPrimaryImageAspectRatio(dto, series);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
|
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" 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.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.Caching.Memory" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.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\Core\*.json" />
|
||||||
<EmbeddedResource Include="Localization\Ratings\*.csv" />
|
<EmbeddedResource Include="Localization\Ratings\*.csv" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -29,7 +31,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The UDP server.
|
/// The UDP server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private UdpServer _udpServer;
|
private UdpServer? _udpServer;
|
||||||
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||||
private bool _disposed = false;
|
private bool _disposed = false;
|
||||||
|
|
||||||
@@ -71,9 +73,8 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
_cancellationTokenSource.Cancel();
|
_cancellationTokenSource.Cancel();
|
||||||
_udpServer.Dispose();
|
|
||||||
_cancellationTokenSource.Dispose();
|
_cancellationTokenSource.Dispose();
|
||||||
_cancellationTokenSource = null;
|
_udpServer?.Dispose();
|
||||||
_udpServer = null;
|
_udpServer = null;
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
|
|||||||
@@ -185,11 +185,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
updateToken = true;
|
updateToken = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
authInfo.IsApiKey = true;
|
authInfo.IsApiKey = false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
authInfo.IsApiKey = false;
|
authInfo.IsApiKey = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateToken)
|
if (updateToken)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||||||
|
|
||||||
public User GetUser(object requestContext)
|
public User GetUser(object requestContext)
|
||||||
{
|
{
|
||||||
return GetUser((HttpContext)requestContext);
|
return GetUser(((HttpRequest)requestContext).HttpContext);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -582,9 +582,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
|
|
||||||
public virtual IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false)
|
public virtual IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false)
|
||||||
{
|
{
|
||||||
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", GetEnumerationOptions(recursive)));
|
||||||
|
|
||||||
return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", searchOption));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
|
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
|
||||||
@@ -594,16 +592,16 @@ namespace Emby.Server.Implementations.IO
|
|||||||
|
|
||||||
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string> extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
|
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string> extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
|
||||||
{
|
{
|
||||||
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
var enumerationOptions = GetEnumerationOptions(recursive);
|
||||||
|
|
||||||
// On linux and osx the search pattern is case sensitive
|
// On linux and osx the search pattern is case sensitive
|
||||||
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
|
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
|
||||||
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Count == 1)
|
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Count == 1)
|
||||||
{
|
{
|
||||||
return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], searchOption));
|
return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], enumerationOptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
var files = new DirectoryInfo(path).EnumerateFiles("*", searchOption);
|
var files = new DirectoryInfo(path).EnumerateFiles("*", enumerationOptions);
|
||||||
|
|
||||||
if (extensions != null && extensions.Count > 0)
|
if (extensions != null && extensions.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -625,10 +623,10 @@ namespace Emby.Server.Implementations.IO
|
|||||||
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
|
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
|
||||||
{
|
{
|
||||||
var directoryInfo = new DirectoryInfo(path);
|
var directoryInfo = new DirectoryInfo(path);
|
||||||
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
var enumerationOptions = GetEnumerationOptions(recursive);
|
||||||
|
|
||||||
return ToMetadata(directoryInfo.EnumerateDirectories("*", searchOption))
|
return ToMetadata(directoryInfo.EnumerateDirectories("*", enumerationOptions))
|
||||||
.Concat(ToMetadata(directoryInfo.EnumerateFiles("*", searchOption)));
|
.Concat(ToMetadata(directoryInfo.EnumerateFiles("*", enumerationOptions)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<FileSystemMetadata> ToMetadata(IEnumerable<FileSystemInfo> infos)
|
private IEnumerable<FileSystemMetadata> ToMetadata(IEnumerable<FileSystemInfo> infos)
|
||||||
@@ -638,8 +636,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
|
|
||||||
public virtual IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false)
|
public virtual IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false)
|
||||||
{
|
{
|
||||||
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
return Directory.EnumerateDirectories(path, "*", GetEnumerationOptions(recursive));
|
||||||
return Directory.EnumerateDirectories(path, "*", searchOption);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual IEnumerable<string> GetFilePaths(string path, bool recursive = false)
|
public virtual IEnumerable<string> GetFilePaths(string path, bool recursive = false)
|
||||||
@@ -649,16 +646,16 @@ namespace Emby.Server.Implementations.IO
|
|||||||
|
|
||||||
public virtual IEnumerable<string> GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
|
public virtual IEnumerable<string> GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
|
||||||
{
|
{
|
||||||
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
var enumerationOptions = GetEnumerationOptions(recursive);
|
||||||
|
|
||||||
// On linux and osx the search pattern is case sensitive
|
// On linux and osx the search pattern is case sensitive
|
||||||
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
|
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
|
||||||
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Length == 1)
|
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Length == 1)
|
||||||
{
|
{
|
||||||
return Directory.EnumerateFiles(path, "*" + extensions[0], searchOption);
|
return Directory.EnumerateFiles(path, "*" + extensions[0], enumerationOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
var files = Directory.EnumerateFiles(path, "*", searchOption);
|
var files = Directory.EnumerateFiles(path, "*", enumerationOptions);
|
||||||
|
|
||||||
if (extensions != null && extensions.Length > 0)
|
if (extensions != null && extensions.Length > 0)
|
||||||
{
|
{
|
||||||
@@ -679,8 +676,18 @@ namespace Emby.Server.Implementations.IO
|
|||||||
|
|
||||||
public virtual IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false)
|
public virtual IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false)
|
||||||
{
|
{
|
||||||
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive));
|
||||||
return Directory.EnumerateFileSystemEntries(path, "*", searchOption);
|
}
|
||||||
|
|
||||||
|
private EnumerationOptions GetEnumerationOptions(bool recursive)
|
||||||
|
{
|
||||||
|
return new EnumerationOptions
|
||||||
|
{
|
||||||
|
RecurseSubdirectories = recursive,
|
||||||
|
IgnoreInaccessible = true,
|
||||||
|
// Don't skip any files.
|
||||||
|
AttributesToSkip = 0
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RunProcess(string path, string args, string workingDirectory)
|
private static void RunProcess(string path, string args, string workingDirectory)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#nullable enable
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations
|
namespace Emby.Server.Implementations
|
||||||
@@ -9,7 +9,7 @@ namespace Emby.Server.Implementations
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the value of the --ffmpeg command line option.
|
/// Gets the value of the --ffmpeg command line option.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string FFmpegPath { get; }
|
string? FFmpegPath { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the value of the --service command line option.
|
/// Gets the value of the --service command line option.
|
||||||
@@ -19,21 +19,21 @@ namespace Emby.Server.Implementations
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the value of the --package-name command line option.
|
/// Gets the value of the --package-name command line option.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string PackageName { get; }
|
string? PackageName { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the value of the --restartpath command line option.
|
/// Gets the value of the --restartpath command line option.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string RestartPath { get; }
|
string? RestartPath { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the value of the --restartargs command line option.
|
/// Gets the value of the --restartargs command line option.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string RestartArgs { get; }
|
string? RestartArgs { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the value of the --published-server-url command line option.
|
/// Gets the value of the --published-server-url command line option.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Uri PublishedServerUrl { get; }
|
string? PublishedServerUrl { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -42,7 +42,6 @@ using MediaBrowser.Model.Dto;
|
|||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.Library;
|
using MediaBrowser.Model.Library;
|
||||||
using MediaBrowser.Model.Net;
|
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
using MediaBrowser.Providers.MediaInfo;
|
using MediaBrowser.Providers.MediaInfo;
|
||||||
@@ -1241,11 +1240,20 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetCollectionType(string path)
|
private CollectionTypeOptions? GetCollectionType(string path)
|
||||||
{
|
{
|
||||||
return _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false)
|
var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false);
|
||||||
.Select(Path.GetFileNameWithoutExtension)
|
foreach (var file in files)
|
||||||
.FirstOrDefault(i => !string.IsNullOrEmpty(i));
|
{
|
||||||
|
// TODO: @bond use a ReadOnlySpan<char> here when Enum.TryParse supports it
|
||||||
|
// https://github.com/dotnet/runtime/issues/20008
|
||||||
|
if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
|
||||||
|
{
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1955,9 +1963,12 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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);
|
_itemRepository.SaveItems(items, cancellationToken);
|
||||||
|
|
||||||
@@ -1988,25 +1999,22 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||||
=> UpdateItemsAsync(new[] { item }, parent, updateReason, 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);
|
||||||
{
|
|
||||||
ProviderManager.SaveMetadata(item, updateReason);
|
|
||||||
}
|
|
||||||
|
|
||||||
item.DateLastSaved = DateTime.UtcNow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item.DateLastSaved = DateTime.UtcNow;
|
||||||
|
|
||||||
|
return UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -2768,6 +2776,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
|
public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
|
||||||
{
|
{
|
||||||
|
string newPath;
|
||||||
if (ownerItem != null)
|
if (ownerItem != null)
|
||||||
{
|
{
|
||||||
var libraryOptions = GetLibraryOptions(ownerItem);
|
var libraryOptions = GetLibraryOptions(ownerItem);
|
||||||
@@ -2775,15 +2784,9 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
foreach (var pathInfo in libraryOptions.PathInfos)
|
foreach (var pathInfo in libraryOptions.PathInfos)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(pathInfo.Path) || string.IsNullOrWhiteSpace(pathInfo.NetworkPath))
|
if (path.TryReplaceSubPath(pathInfo.Path, pathInfo.NetworkPath, out newPath))
|
||||||
{
|
{
|
||||||
continue;
|
return newPath;
|
||||||
}
|
|
||||||
|
|
||||||
var substitutionResult = SubstitutePathInternal(path, pathInfo.Path, pathInfo.NetworkPath);
|
|
||||||
if (substitutionResult.Item2)
|
|
||||||
{
|
|
||||||
return substitutionResult.Item1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2792,24 +2795,16 @@ namespace Emby.Server.Implementations.Library
|
|||||||
var metadataPath = _configurationManager.Configuration.MetadataPath;
|
var metadataPath = _configurationManager.Configuration.MetadataPath;
|
||||||
var metadataNetworkPath = _configurationManager.Configuration.MetadataNetworkPath;
|
var metadataNetworkPath = _configurationManager.Configuration.MetadataNetworkPath;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(metadataPath) && !string.IsNullOrWhiteSpace(metadataNetworkPath))
|
if (path.TryReplaceSubPath(metadataPath, metadataNetworkPath, out newPath))
|
||||||
{
|
{
|
||||||
var metadataSubstitutionResult = SubstitutePathInternal(path, metadataPath, metadataNetworkPath);
|
return newPath;
|
||||||
if (metadataSubstitutionResult.Item2)
|
|
||||||
{
|
|
||||||
return metadataSubstitutionResult.Item1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var map in _configurationManager.Configuration.PathSubstitutions)
|
foreach (var map in _configurationManager.Configuration.PathSubstitutions)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(map.From))
|
if (path.TryReplaceSubPath(map.From, map.To, out newPath))
|
||||||
{
|
{
|
||||||
var substitutionResult = SubstitutePathInternal(path, map.From, map.To);
|
return newPath;
|
||||||
if (substitutionResult.Item2)
|
|
||||||
{
|
|
||||||
return substitutionResult.Item1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2818,47 +2813,12 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
public string SubstitutePath(string path, string from, string to)
|
public string SubstitutePath(string path, string from, string to)
|
||||||
{
|
{
|
||||||
return SubstitutePathInternal(path, from, to).Item1;
|
if (path.TryReplaceSubPath(from, to, out var newPath))
|
||||||
}
|
|
||||||
|
|
||||||
private Tuple<string, bool> SubstitutePathInternal(string path, string from, string to)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(path));
|
return newPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(from))
|
return path;
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(from));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(to))
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(to));
|
|
||||||
}
|
|
||||||
|
|
||||||
from = from.Trim();
|
|
||||||
to = to.Trim();
|
|
||||||
|
|
||||||
var newPath = path.Replace(from, to, StringComparison.OrdinalIgnoreCase);
|
|
||||||
var changed = false;
|
|
||||||
|
|
||||||
if (!string.Equals(newPath, path, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
if (to.IndexOf('/', StringComparison.Ordinal) != -1)
|
|
||||||
{
|
|
||||||
newPath = newPath.Replace('\\', '/');
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
newPath = newPath.Replace('/', '\\');
|
|
||||||
}
|
|
||||||
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Tuple<string, bool>(newPath, changed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetExtraTypeFromFilename(Video item)
|
private void SetExtraTypeFromFilename(Video item)
|
||||||
@@ -2957,7 +2917,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
throw new InvalidOperationException();
|
throw new InvalidOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary)
|
public async Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, bool refreshLibrary)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
{
|
{
|
||||||
@@ -2991,9 +2951,9 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
Directory.CreateDirectory(virtualFolderPath);
|
Directory.CreateDirectory(virtualFolderPath);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(collectionType))
|
if (collectionType != null)
|
||||||
{
|
{
|
||||||
var path = Path.Combine(virtualFolderPath, collectionType + ".collection");
|
var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection");
|
||||||
|
|
||||||
File.WriteAllBytes(path, Array.Empty<byte>());
|
File.WriteAllBytes(path, Array.Empty<byte>());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library
|
namespace Emby.Server.Implementations.Library
|
||||||
@@ -47,5 +49,59 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replaces a sub path with another sub path and normalizes the final path.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The original path.</param>
|
||||||
|
/// <param name="subPath">The original sub path.</param>
|
||||||
|
/// <param name="newSubPath">The new sub path.</param>
|
||||||
|
/// <param name="newPath">The result of the sub path replacement</param>
|
||||||
|
/// <returns>The path after replacing the sub path.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException"><paramref name="path" />, <paramref name="newSubPath" /> or <paramref name="newSubPath" /> is empty.</exception>
|
||||||
|
public static bool TryReplaceSubPath(this string path, string subPath, string newSubPath, [NotNullWhen(true)] out string? newPath)
|
||||||
|
{
|
||||||
|
newPath = null;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(subPath) || string.IsNullOrEmpty(newSubPath) || subPath.Length > path.Length)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
char oldDirectorySeparatorChar;
|
||||||
|
char newDirectorySeparatorChar;
|
||||||
|
// True normalization is still not possible https://github.com/dotnet/runtime/issues/2162
|
||||||
|
// The reasoning behind this is that a forward slash likely means it's a Linux path and
|
||||||
|
// so the whole path should be normalized to use / and vice versa for Windows (although Windows doesn't care much).
|
||||||
|
if (newSubPath.Contains('/', StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
oldDirectorySeparatorChar = '\\';
|
||||||
|
newDirectorySeparatorChar = '/';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
oldDirectorySeparatorChar = '/';
|
||||||
|
newDirectorySeparatorChar = '\\';
|
||||||
|
}
|
||||||
|
|
||||||
|
path = path.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
|
||||||
|
subPath = subPath.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
|
||||||
|
|
||||||
|
// We have to ensure that the sub path ends with a directory separator otherwise we'll get weird results
|
||||||
|
// when the sub path matches a similar but in-complete subpath
|
||||||
|
var oldSubPathEndsWithSeparator = subPath[^1] == newDirectorySeparatorChar;
|
||||||
|
if (!path.StartsWith(subPath, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| (!oldSubPathEndsWithSeparator && path[subPath.Length] != newDirectorySeparatorChar))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newSubPathTrimmed = newSubPath.AsSpan().TrimEnd(newDirectorySeparatorChar);
|
||||||
|
// Ensure that the path with the old subpath removed starts with a leading dir separator
|
||||||
|
int idx = oldSubPathEndsWithSeparator ? subPath.Length - 1 : subPath.Length;
|
||||||
|
newPath = string.Concat(newSubPathTrimmed, path.AsSpan(idx));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
/// Gets the priority.
|
/// Gets the priority.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The priority.</value>
|
/// <value>The priority.</value>
|
||||||
public override ResolverPriority Priority => ResolverPriority.Fourth;
|
public override ResolverPriority Priority => ResolverPriority.Fifth;
|
||||||
|
|
||||||
public MultiItemResolverResult ResolveMultiple(
|
public MultiItemResolverResult ResolveMultiple(
|
||||||
Folder parent,
|
Folder parent,
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
/// Gets the priority.
|
/// Gets the priority.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The priority.</value>
|
/// <value>The priority.</value>
|
||||||
public override ResolverPriority Priority => ResolverPriority.Second;
|
public override ResolverPriority Priority => ResolverPriority.Third;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves the specified args.
|
/// Resolves the specified args.
|
||||||
|
|||||||
@@ -79,11 +79,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
return new MusicArtist();
|
return new MusicArtist();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_config.Configuration.EnableSimpleArtistDetection)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Avoid mis-identifying top folders
|
// Avoid mis-identifying top folders
|
||||||
if (args.Parent.IsRoot)
|
if (args.Parent.IsRoot)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
|||||||
{
|
{
|
||||||
public class BookResolver : MediaBrowser.Controller.Resolvers.ItemResolver<Book>
|
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)
|
protected override Book Resolve(ItemResolveArgs args)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
/// Gets the priority.
|
/// Gets the priority.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The priority.</value>
|
/// <value>The priority.</value>
|
||||||
public override ResolverPriority Priority => ResolverPriority.Third;
|
public override ResolverPriority Priority => ResolverPriority.Fourth;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public MultiItemResolverResult ResolveMultiple(
|
public MultiItemResolverResult ResolveMultiple(
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||||||
}
|
}
|
||||||
|
|
||||||
// It's a directory-based playlist if the directory contains a playlist file
|
// It's a directory-based playlist if the directory contains a playlist file
|
||||||
var filePaths = Directory.EnumerateFiles(args.Path);
|
var filePaths = Directory.EnumerateFiles(args.Path, "*", new EnumerationOptions { IgnoreInaccessible = true });
|
||||||
if (filePaths.Any(f => f.EndsWith(PlaylistXmlSaver.DefaultPlaylistFilename, StringComparison.OrdinalIgnoreCase)))
|
if (filePaths.Any(f => f.EndsWith(PlaylistXmlSaver.DefaultPlaylistFilename, StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
return new Playlist
|
return new Playlist
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
if (query.Limit.HasValue)
|
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>
|
return new QueryResult<SearchHintInfo>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ using MediaBrowser.Controller.Persistence;
|
|||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using Book = MediaBrowser.Controller.Entities.Book;
|
using Book = MediaBrowser.Controller.Entities.Book;
|
||||||
|
using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library
|
namespace Emby.Server.Implementations.Library
|
||||||
{
|
{
|
||||||
@@ -219,7 +220,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
var hasRuntime = runtimeTicks > 0;
|
var hasRuntime = runtimeTicks > 0;
|
||||||
|
|
||||||
// If a position has been reported, and if we know the duration
|
// 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;
|
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)
|
else if (!hasRuntime)
|
||||||
{
|
{
|
||||||
// If we don't know the runtime we'll just have to assume it was fully played
|
// If we don't know the runtime we'll just have to assume it was fully played
|
||||||
|
|||||||
@@ -129,23 +129,23 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
if (!query.IncludeHidden)
|
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 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
|
return list
|
||||||
.OrderBy(i =>
|
.OrderBy(i =>
|
||||||
{
|
{
|
||||||
var index = orders.IndexOf(i.Id.ToString("N", CultureInfo.InvariantCulture));
|
var index = Array.IndexOf(orders, i.Id);
|
||||||
|
|
||||||
if (index == -1
|
if (index == -1
|
||||||
&& i is UserView view
|
&& i is UserView view
|
||||||
&& view.DisplayParentId != Guid.Empty)
|
&& 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;
|
return index == -1 ? int.MaxValue : index;
|
||||||
@@ -280,8 +280,8 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
|
parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
|
||||||
.Where(i => i is Folder)
|
.Where(i => i is Folder)
|
||||||
.Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes)
|
.Where(i => !user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes)
|
||||||
.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
|
.Contains(i.Id))
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||||||
{
|
{
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
|
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
|
||||||
|
|
||||||
using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read))
|
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||||
|
using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
{
|
{
|
||||||
onStarted();
|
onStarted();
|
||||||
|
|
||||||
@@ -70,7 +71,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
|
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
|
||||||
|
|
||||||
await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read);
|
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||||
|
await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||||
|
|
||||||
onStarted();
|
onStarted();
|
||||||
|
|
||||||
|
|||||||
@@ -1860,7 +1860,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
|
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||||
|
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
{
|
{
|
||||||
var settings = new XmlWriterSettings
|
var settings = new XmlWriterSettings
|
||||||
{
|
{
|
||||||
@@ -1924,7 +1925,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
|
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||||
|
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
{
|
{
|
||||||
var settings = new XmlWriterSettings
|
var settings = new XmlWriterSettings
|
||||||
{
|
{
|
||||||
@@ -2608,7 +2610,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||||||
{
|
{
|
||||||
Locations = new string[] { customPath },
|
Locations = new string[] { customPath },
|
||||||
Name = "Recorded Movies",
|
Name = "Recorded Movies",
|
||||||
CollectionType = CollectionType.Movies
|
CollectionType = CollectionTypeOptions.Movies
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2619,7 +2621,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||||||
{
|
{
|
||||||
Locations = new string[] { customPath },
|
Locations = new string[] { customPath },
|
||||||
Name = "Recorded Shows",
|
Name = "Recorded Shows",
|
||||||
CollectionType = CollectionType.TvShows
|
CollectionType = CollectionTypeOptions.TvShows
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||||||
Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
|
Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
|
||||||
|
|
||||||
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
|
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
|
||||||
_logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
|
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||||
|
_logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
|
||||||
|
|
||||||
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
|
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
|
||||||
_logFileStream.Write(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length);
|
_logFileStream.Write(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length);
|
||||||
|
|||||||
@@ -611,25 +611,25 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
|
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);
|
return response;
|
||||||
}
|
|
||||||
catch (HttpRequestException ex)
|
|
||||||
{
|
|
||||||
_tokens.Clear();
|
|
||||||
|
|
||||||
if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500)
|
|
||||||
{
|
|
||||||
enableRetry = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!enableRetry)
|
|
||||||
{
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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));
|
options.Headers.TryAddWithoutValidation("token", await GetToken(providerInfo, cancellationToken).ConfigureAwait(false));
|
||||||
return await Send(options, false, 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);
|
options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
|
||||||
|
|
||||||
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
|
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(stream).ConfigureAwait(false);
|
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(stream).ConfigureAwait(false);
|
||||||
if (string.Equals(root.message, "OK", StringComparison.Ordinal))
|
if (string.Equals(root.message, "OK", StringComparison.Ordinal))
|
||||||
@@ -701,6 +702,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
|
using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
|
||||||
|
httpResponse.EnsureSuccessStatusCode();
|
||||||
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
using var response = httpResponse.Content;
|
using var response = httpResponse.Content;
|
||||||
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(stream).ConfigureAwait(false);
|
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(stream).ConfigureAwait(false);
|
||||||
@@ -709,7 +711,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
}
|
}
|
||||||
catch (HttpRequestException ex)
|
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)
|
if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.BadRequest)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -782,18 +784,17 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
var allStations = root.stations ?? new List<ScheduleDirect.Station>();
|
var allStations = root.stations ?? new List<ScheduleDirect.Station>();
|
||||||
|
|
||||||
var map = root.map;
|
var map = root.map;
|
||||||
int len = map.Count;
|
var list = new List<ChannelInfo>(map.Count);
|
||||||
var array = new List<ChannelInfo>(len);
|
foreach (var channel in map)
|
||||||
for (int i = 0; i < len; i++)
|
|
||||||
{
|
{
|
||||||
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)
|
if (station == null)
|
||||||
{
|
{
|
||||||
station = new ScheduleDirect.Station
|
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;
|
channelInfo.ImageUrl = station.logo.URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
array[i] = channelInfo;
|
list.Add(channelInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
return array;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string NormalizeName(string value)
|
private static string NormalizeName(string value)
|
||||||
|
|||||||
@@ -1928,7 +1928,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||||||
|
|
||||||
foreach (var programDto in currentProgramDtos)
|
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;
|
channelDto.CurrentProgram = programDto;
|
||||||
}
|
}
|
||||||
@@ -2018,7 +2018,7 @@ namespace Emby.Server.Implementations.LiveTv
|
|||||||
info.DayPattern = _tvDtoService.GetDayPattern(info.Days);
|
info.DayPattern = _tvDtoService.GetDayPattern(info.Days);
|
||||||
|
|
||||||
info.Name = program.Name;
|
info.Name = program.Name;
|
||||||
info.ChannelId = programDto.ChannelId;
|
info.ChannelId = programDto.ChannelId ?? Guid.Empty;
|
||||||
info.ChannelName = programDto.ChannelName;
|
info.ChannelName = programDto.ChannelName;
|
||||||
info.StartDate = program.StartDate;
|
info.StartDate = program.StartDate;
|
||||||
info.Name = program.Name;
|
info.Name = program.Name;
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,10 +8,12 @@ using System.Linq;
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
|
using MediaBrowser.Common.Json;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
@@ -37,6 +39,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
private readonly INetworkManager _networkManager;
|
private readonly INetworkManager _networkManager;
|
||||||
private readonly IStreamHelper _streamHelper;
|
private readonly IStreamHelper _streamHelper;
|
||||||
|
|
||||||
|
private readonly JsonSerializerOptions _jsonOptions;
|
||||||
|
|
||||||
private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
|
private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>();
|
||||||
|
|
||||||
public HdHomerunHost(
|
public HdHomerunHost(
|
||||||
@@ -56,6 +60,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
_socketFactory = socketFactory;
|
_socketFactory = socketFactory;
|
||||||
_networkManager = networkManager;
|
_networkManager = networkManager;
|
||||||
_streamHelper = streamHelper;
|
_streamHelper = streamHelper;
|
||||||
|
|
||||||
|
_jsonOptions = JsonDefaults.GetOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Name => "HD Homerun";
|
public string Name => "HD Homerun";
|
||||||
@@ -67,13 +73,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
private string GetChannelId(TunerHostInfo info, Channels i)
|
private string GetChannelId(TunerHostInfo info, Channels i)
|
||||||
=> ChannelIdPrefix + i.GuideNumber;
|
=> 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);
|
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);
|
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);
|
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>();
|
.ConfigureAwait(false) ?? new List<Channels>();
|
||||||
|
|
||||||
if (info.ImportFavoritesOnly)
|
if (info.ImportFavoritesOnly)
|
||||||
@@ -100,7 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
Id = GetChannelId(info, i),
|
Id = GetChannelId(info, i),
|
||||||
IsFavorite = i.Favorite,
|
IsFavorite = i.Favorite,
|
||||||
TunerHostId = info.Id,
|
TunerHostId = info.Id,
|
||||||
IsHD = i.HD == 1,
|
IsHD = i.HD,
|
||||||
AudioCodec = i.AudioCodec,
|
AudioCodec = i.AudioCodec,
|
||||||
VideoCodec = i.VideoCodec,
|
VideoCodec = i.VideoCodec,
|
||||||
ChannelType = ChannelType.TV,
|
ChannelType = ChannelType.TV,
|
||||||
@@ -109,7 +115,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
}).Cast<ChannelInfo>().ToList();
|
}).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;
|
var cacheKey = info.Id;
|
||||||
|
|
||||||
@@ -127,10 +133,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
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);
|
.ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
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);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(cacheKey))
|
if (!string.IsNullOrEmpty(cacheKey))
|
||||||
@@ -328,25 +335,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
return new Uri(url).AbsoluteUri.TrimEnd('/');
|
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()
|
protected EncodingOptions GetEncodingOptions()
|
||||||
{
|
{
|
||||||
return Config.GetConfiguration<EncodingOptions>("encoding");
|
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)
|
public async Task<List<TunerHostInfo>> DiscoverDevices(int discoveryDurationMs, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
lock (_modelCache)
|
lock (_modelCache)
|
||||||
@@ -762,7 +714,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<TunerHostInfo> TryGetTunerHostInfo(string url, CancellationToken cancellationToken)
|
internal async Task<TunerHostInfo> TryGetTunerHostInfo(string url, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var hostInfo = new TunerHostInfo
|
var hostInfo = new TunerHostInfo
|
||||||
{
|
{
|
||||||
@@ -774,6 +726,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
|
|
||||||
hostInfo.DeviceId = modelInfo.DeviceID;
|
hostInfo.DeviceId = modelInfo.DeviceID;
|
||||||
hostInfo.FriendlyName = modelInfo.FriendlyName;
|
hostInfo.FriendlyName = modelInfo.FriendlyName;
|
||||||
|
hostInfo.TunerCount = modelInfo.TunerCount;
|
||||||
|
|
||||||
return hostInfo;
|
return hostInfo;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,7 +193,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
{
|
{
|
||||||
var resolved = false;
|
var resolved = false;
|
||||||
|
|
||||||
using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read))
|
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||||
|
using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -136,7 +136,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||||||
Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath);
|
Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath);
|
||||||
using var message = response;
|
using var message = response;
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read);
|
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||||
|
await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||||
await StreamHelper.CopyToAsync(
|
await StreamHelper.CopyToAsync(
|
||||||
stream,
|
stream,
|
||||||
fileStream,
|
fileStream,
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ chb|||Chibcha|chibcha
|
|||||||
che||ce|Chechen|tchétchène
|
che||ce|Chechen|tchétchène
|
||||||
chg|||Chagatai|djaghataï
|
chg|||Chagatai|djaghataï
|
||||||
chi|zho|zh|Chinese|chinois
|
chi|zho|zh|Chinese|chinois
|
||||||
|
chi|zho|zh-tw|Chinese; Traditional|chinois
|
||||||
|
chi|zho|zh-hk|Chinese; Hong Kong|chinois
|
||||||
chk|||Chuukese|chuuk
|
chk|||Chuukese|chuuk
|
||||||
chm|||Mari|mari
|
chm|||Mari|mari
|
||||||
chn|||Chinook jargon|chinook, jargon
|
chn|||Chinook jargon|chinook, jargon
|
||||||
|
|||||||
763
Emby.Server.Implementations/Plugins/PluginManager.cs
Normal file
763
Emby.Server.Implementations/Plugins/PluginManager.cs
Normal file
@@ -0,0 +1,763 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
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.Net;
|
||||||
|
using MediaBrowser.Common.Plugins;
|
||||||
|
using MediaBrowser.Model.Configuration;
|
||||||
|
using MediaBrowser.Model.Plugins;
|
||||||
|
using MediaBrowser.Model.Updates;
|
||||||
|
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;
|
||||||
|
|
||||||
|
private IHttpClientFactory? _httpClientFactory;
|
||||||
|
|
||||||
|
private IHttpClientFactory HttpClientFactory
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_httpClientFactory == null)
|
||||||
|
{
|
||||||
|
_httpClientFactory = _appHost.Resolve<IHttpClientFactory>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _httpClientFactory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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);
|
||||||
|
|
||||||
|
assembly.GetExportedTypes();
|
||||||
|
}
|
||||||
|
catch (FileLoadException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to load assembly {Path}. Disabling plugin.", file);
|
||||||
|
ChangePluginState(plugin, PluginStatus.Malfunctioned);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
catch (TypeLoadException ex) // Undocumented exception
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to load assembly {Path}. This error occurs when a plugin references an incompatible version of one of the shared libraries. Disabling plugin.", file);
|
||||||
|
ChangePluginState(plugin, PluginStatus.NotSupported);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
#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, "Failed to load assembly {Path}. Unknown exception was thrown. 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool SaveManifest(PluginManifest manifest, string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = JsonSerializer.Serialize(manifest, _jsonOptions);
|
||||||
|
File.WriteAllText(Path.Combine(path, "meta.json"), data);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (ArgumentException e)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(e, "Unable to save plugin manifest due to invalid value. {Path}", path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path)
|
||||||
|
{
|
||||||
|
if (packageInfo == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var versionInfo = packageInfo.Versions.First(v => v.Version == version.ToString());
|
||||||
|
var imagePath = string.Empty;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(packageInfo.ImageUrl))
|
||||||
|
{
|
||||||
|
var url = new Uri(packageInfo.ImageUrl);
|
||||||
|
imagePath = Path.Join(path, url.Segments[^1]);
|
||||||
|
|
||||||
|
await using var fileStream = File.OpenWrite(imagePath);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var downloadStream = await HttpClientFactory
|
||||||
|
.CreateClient(NamedClient.Default)
|
||||||
|
.GetStreamAsync(url)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
await downloadStream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath);
|
||||||
|
imagePath = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var manifest = new PluginManifest
|
||||||
|
{
|
||||||
|
Category = packageInfo.Category,
|
||||||
|
Changelog = versionInfo.Changelog ?? string.Empty,
|
||||||
|
Description = packageInfo.Description,
|
||||||
|
Id = new Guid(packageInfo.Id),
|
||||||
|
Name = packageInfo.Name,
|
||||||
|
Overview = packageInfo.Overview,
|
||||||
|
Owner = packageInfo.Owner,
|
||||||
|
TargetAbi = versionInfo.TargetAbi ?? string.Empty,
|
||||||
|
Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp),
|
||||||
|
Version = versionInfo.Version,
|
||||||
|
Status = PluginStatus.Active,
|
||||||
|
AutoUpdate = true,
|
||||||
|
ImagePath = imagePath
|
||||||
|
};
|
||||||
|
|
||||||
|
return SaveManifest(manifest, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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 => p.DllFiles.Contains(assembly.Location, StringComparer.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 once 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 = instance.Version.ToString();
|
||||||
|
bool changed = false;
|
||||||
|
if (string.Equals(manifest.Version, pluginStr, StringComparison.Ordinal)
|
||||||
|
|| manifest.Id != instance.Id)
|
||||||
|
{
|
||||||
|
// 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;
|
||||||
|
manifest.Id = plugin.Instance.Id;
|
||||||
|
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.Active,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Resources;
|
using System.Resources;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
// General Information about an assembly is controlled through the following
|
// General Information about an assembly is controlled through the following
|
||||||
@@ -14,6 +15,7 @@ using System.Runtime.InteropServices;
|
|||||||
[assembly: AssemblyTrademark("")]
|
[assembly: AssemblyTrademark("")]
|
||||||
[assembly: AssemblyCulture("")]
|
[assembly: AssemblyCulture("")]
|
||||||
[assembly: NeutralResourcesLanguage("en")]
|
[assembly: NeutralResourcesLanguage("en")]
|
||||||
|
[assembly: InternalsVisibleTo("Jellyfin.Server.Implementations.Tests")]
|
||||||
|
|
||||||
// Setting ComVisible to false makes the types in this assembly not visible
|
// 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
|
// to COM components. If you need to access a type in this assembly from
|
||||||
|
|||||||
@@ -80,10 +80,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
|
|||||||
// Delete log files more than n days old
|
// Delete log files more than n days old
|
||||||
var minDateModified = DateTime.UtcNow.AddDays(-_configurationManager.CommonConfiguration.LogFileRetentionDays);
|
var minDateModified = DateTime.UtcNow.AddDays(-_configurationManager.CommonConfiguration.LogFileRetentionDays);
|
||||||
|
|
||||||
// Only delete the .txt log files, the *.log files created by serilog get managed by itself
|
// Only delete files that serilog doesn't manage (anything that doesn't start with 'log_'
|
||||||
var filesToDelete = _fileSystem.GetFiles(_configurationManager.CommonApplicationPaths.LogDirectoryPath, new[] { ".txt" }, true, true)
|
var filesToDelete = _fileSystem.GetFiles(_configurationManager.CommonApplicationPaths.LogDirectoryPath, true)
|
||||||
.Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
|
.Where(f => !f.Name.StartsWith("log_", StringComparison.Ordinal)
|
||||||
.ToList();
|
&& _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
var index = 0;
|
var index = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ using System.Net.Http;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MediaBrowser.Common.Updates;
|
using MediaBrowser.Common.Updates;
|
||||||
|
using MediaBrowser.Model.Globalization;
|
||||||
using MediaBrowser.Model.Net;
|
using MediaBrowser.Model.Net;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using MediaBrowser.Model.Globalization;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.ScheduledTasks
|
namespace Emby.Server.Implementations.ScheduledTasks
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -128,6 +128,9 @@ namespace Emby.Server.Implementations.Session
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event EventHandler<SessionEventArgs> SessionActivity;
|
public event EventHandler<SessionEventArgs> SessionActivity;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public event EventHandler<SessionEventArgs> SessionControllerConnected;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all connections.
|
/// Gets all connections.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -312,6 +315,19 @@ namespace Emby.Server.Implementations.Session
|
|||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void OnSessionControllerConnected(SessionInfo info)
|
||||||
|
{
|
||||||
|
EventHelper.QueueEventIfNotNull(
|
||||||
|
SessionControllerConnected,
|
||||||
|
this,
|
||||||
|
new SessionEventArgs
|
||||||
|
{
|
||||||
|
SessionInfo = info
|
||||||
|
},
|
||||||
|
_logger);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void CloseIfNeeded(SessionInfo session)
|
public void CloseIfNeeded(SessionInfo session)
|
||||||
{
|
{
|
||||||
@@ -1294,7 +1310,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return SendMessageToSession(session, SessionMessageType.PlayState, command, cancellationToken);
|
return SendMessageToSession(session, SessionMessageType.Playstate, command, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
|
private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
|
||||||
@@ -1440,7 +1456,12 @@ namespace Emby.Server.Implementations.Session
|
|||||||
throw new SecurityException("Unknown quick connect token");
|
throw new SecurityException("Unknown quick connect token");
|
||||||
}
|
}
|
||||||
|
|
||||||
request.UserId = result.Items[0].UserId;
|
var info = result.Items[0];
|
||||||
|
request.UserId = info.UserId;
|
||||||
|
|
||||||
|
// There's no need to keep the quick connect token in the database, as AuthenticateNewSessionInternal() issues a long lived token.
|
||||||
|
_authRepo.Delete(info);
|
||||||
|
|
||||||
return AuthenticateNewSessionInternal(request, false);
|
return AuthenticateNewSessionInternal(request, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,8 @@ namespace Emby.Server.Implementations.Session
|
|||||||
|
|
||||||
var controller = (WebSocketController)controllerInfo.Item1;
|
var controller = (WebSocketController)controllerInfo.Item1;
|
||||||
controller.AddWebSocket(connection);
|
controller.AddWebSocket(connection);
|
||||||
|
|
||||||
|
_sessionManager.OnSessionControllerConnected(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly ILibraryManager _libraryManager;
|
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>
|
/// <summary>
|
||||||
/// The map between sessions and groups.
|
/// The map between sessions and groups.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -81,7 +87,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
_sessionManager = sessionManager;
|
_sessionManager = sessionManager;
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
_logger = loggerFactory.CreateLogger<SyncPlayManager>();
|
_logger = loggerFactory.CreateLogger<SyncPlayManager>();
|
||||||
_sessionManager.SessionStarted += OnSessionManagerSessionStarted;
|
_sessionManager.SessionControllerConnected += OnSessionControllerConnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -122,6 +128,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
throw new InvalidOperationException("Could not add session to group!");
|
throw new InvalidOperationException("Could not add session to group!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateSessionsCounter(session.UserId, 1);
|
||||||
group.CreateGroup(session, request, cancellationToken);
|
group.CreateGroup(session, request, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,6 +179,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
if (existingGroup.GroupId.Equals(request.GroupId))
|
if (existingGroup.GroupId.Equals(request.GroupId))
|
||||||
{
|
{
|
||||||
// Restore session.
|
// Restore session.
|
||||||
|
UpdateSessionsCounter(session.UserId, 1);
|
||||||
group.SessionJoin(session, request, cancellationToken);
|
group.SessionJoin(session, request, cancellationToken);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -185,6 +193,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
throw new InvalidOperationException("Could not add session to group!");
|
throw new InvalidOperationException("Could not add session to group!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateSessionsCounter(session.UserId, 1);
|
||||||
group.SessionJoin(session, request, cancellationToken);
|
group.SessionJoin(session, request, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,6 +232,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
throw new InvalidOperationException("Could not remove session from group!");
|
throw new InvalidOperationException("Could not remove session from group!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateSessionsCounter(session.UserId, -1);
|
||||||
group.SessionLeave(session, request, cancellationToken);
|
group.SessionLeave(session, request, cancellationToken);
|
||||||
|
|
||||||
if (group.IsGroupEmpty())
|
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>
|
/// <summary>
|
||||||
/// Releases unmanaged and optionally managed resources.
|
/// Releases unmanaged and optionally managed resources.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -329,11 +352,11 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_sessionManager.SessionStarted -= OnSessionManagerSessionStarted;
|
_sessionManager.SessionControllerConnected -= OnSessionControllerConnected;
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSessionManagerSessionStarted(object sender, SessionEventArgs e)
|
private void OnSessionControllerConnected(object sender, SessionEventArgs e)
|
||||||
{
|
{
|
||||||
var session = e.SessionInfo;
|
var session = e.SessionInfo;
|
||||||
|
|
||||||
@@ -343,5 +366,26 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
JoinGroup(session, request, CancellationToken.None);
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,8 +75,7 @@ namespace Emby.Server.Implementations.TV
|
|||||||
{
|
{
|
||||||
parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
|
parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
|
||||||
.Where(i => i is Folder)
|
.Where(i => i is Folder)
|
||||||
.Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes)
|
.Where(i => !user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes).Contains(i.Id))
|
||||||
.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
|
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,10 +143,31 @@ namespace Emby.Server.Implementations.TV
|
|||||||
var allNextUp = seriesKeys
|
var allNextUp = seriesKeys
|
||||||
.Select(i => GetNextUp(i, currentUser, dtoOptions));
|
.Select(i => GetNextUp(i, currentUser, dtoOptions));
|
||||||
|
|
||||||
|
// If viewing all next up for all series, remove first episodes
|
||||||
|
// But if that returns empty, keep those first episodes (avoid completely empty view)
|
||||||
|
var alwaysEnableFirstEpisode = !string.IsNullOrEmpty(request.SeriesId);
|
||||||
|
var anyFound = false;
|
||||||
|
|
||||||
return allNextUp
|
return allNextUp
|
||||||
.Where(i =>
|
.Where(i =>
|
||||||
{
|
{
|
||||||
return i.Item1 != DateTime.MinValue;
|
if (request.DisableFirstEpisode)
|
||||||
|
{
|
||||||
|
return i.Item1 != DateTime.MinValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alwaysEnableFirstEpisode || i.Item1 != DateTime.MinValue)
|
||||||
|
{
|
||||||
|
anyFound = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!anyFound && i.Item1 == DateTime.MinValue)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
})
|
})
|
||||||
.Select(i => i.Item2())
|
.Select(i => i.Item2())
|
||||||
.Where(i => i != null);
|
.Where(i => i != null);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#pragma warning disable CS1591
|
#nullable enable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
@@ -12,7 +12,6 @@ using System.Text.Json;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Events;
|
using Jellyfin.Data.Events;
|
||||||
using MediaBrowser.Common;
|
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Json;
|
using MediaBrowser.Common.Json;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
@@ -41,17 +40,15 @@ namespace Emby.Server.Implementations.Updates
|
|||||||
private readonly IEventManager _eventManager;
|
private readonly IEventManager _eventManager;
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
private readonly IServerConfigurationManager _config;
|
private readonly IServerConfigurationManager _config;
|
||||||
private readonly IFileSystem _fileSystem;
|
|
||||||
private readonly JsonSerializerOptions _jsonSerializerOptions;
|
private readonly JsonSerializerOptions _jsonSerializerOptions;
|
||||||
|
private readonly IPluginManager _pluginManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the application host.
|
/// Gets the application host.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The application host.</value>
|
/// <value>The application host.</value>
|
||||||
private readonly IServerApplicationHost _applicationHost;
|
private readonly IServerApplicationHost _applicationHost;
|
||||||
|
|
||||||
private readonly IZipClient _zipClient;
|
private readonly IZipClient _zipClient;
|
||||||
|
|
||||||
private readonly object _currentInstallationsLock = new object();
|
private readonly object _currentInstallationsLock = new object();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -64,6 +61,17 @@ namespace Emby.Server.Implementations.Updates
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly ConcurrentBag<InstallationInfo> _completedInstallationsInternal;
|
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(
|
public InstallationManager(
|
||||||
ILogger<InstallationManager> logger,
|
ILogger<InstallationManager> logger,
|
||||||
IServerApplicationHost appHost,
|
IServerApplicationHost appHost,
|
||||||
@@ -71,8 +79,8 @@ namespace Emby.Server.Implementations.Updates
|
|||||||
IEventManager eventManager,
|
IEventManager eventManager,
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
IServerConfigurationManager config,
|
IServerConfigurationManager config,
|
||||||
IFileSystem fileSystem,
|
IZipClient zipClient,
|
||||||
IZipClient zipClient)
|
IPluginManager pluginManager)
|
||||||
{
|
{
|
||||||
_currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>();
|
_currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>();
|
||||||
_completedInstallationsInternal = new ConcurrentBag<InstallationInfo>();
|
_completedInstallationsInternal = new ConcurrentBag<InstallationInfo>();
|
||||||
@@ -83,38 +91,65 @@ namespace Emby.Server.Implementations.Updates
|
|||||||
_eventManager = eventManager;
|
_eventManager = eventManager;
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
_config = config;
|
_config = config;
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_zipClient = zipClient;
|
_zipClient = zipClient;
|
||||||
_jsonSerializerOptions = JsonDefaults.GetOptions();
|
_jsonSerializerOptions = JsonDefaults.GetOptions();
|
||||||
|
_pluginManager = pluginManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
|
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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
|
try
|
||||||
{
|
{
|
||||||
var packages = await _httpClientFactory.CreateClient(NamedClient.Default)
|
List<PackageInfo>? packages = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||||
.GetFromJsonAsync<List<PackageInfo>>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
|
.GetFromJsonAsync<List<PackageInfo>>(new Uri(manifest), _jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (packages == null)
|
if (packages == null)
|
||||||
{
|
{
|
||||||
return Array.Empty<PackageInfo>();
|
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.
|
// Store the repository and repository url with each version, as they may be spread apart.
|
||||||
foreach (var entry in packages)
|
foreach (var entry in packages)
|
||||||
{
|
{
|
||||||
foreach (var ver in entry.versions)
|
for (int a = entry.Versions.Count - 1; a >= 0; a--)
|
||||||
{
|
{
|
||||||
ver.repositoryName = manifestName;
|
var ver = entry.Versions[a];
|
||||||
ver.repositoryUrl = manifest;
|
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;
|
return packages;
|
||||||
}
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Cannot locate the plugin manifest {Manifest}", manifest);
|
||||||
|
return Array.Empty<PackageInfo>();
|
||||||
|
}
|
||||||
catch (JsonException ex)
|
catch (JsonException ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest);
|
_logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest);
|
||||||
@@ -132,69 +167,53 @@ 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 />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var result = new List<PackageInfo>();
|
var result = new List<PackageInfo>();
|
||||||
foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories)
|
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.
|
// Where repositories have the same content, the details from the first is taken.
|
||||||
foreach (var package in await GetPackages(repository.Name, repository.Url, cancellationToken).ConfigureAwait(true))
|
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.
|
// Package doesn't have a valid GUID, skip.
|
||||||
continue;
|
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);
|
||||||
|
if (plugin != null)
|
||||||
|
{
|
||||||
|
await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove versions with a target ABI 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)
|
if (existing != null)
|
||||||
{
|
{
|
||||||
// Assumption is both lists are ordered, so slot these into the correct place.
|
// Assumption is both lists are ordered, so slot these into the correct place.
|
||||||
MergeSort(existing.versions, package.versions);
|
MergeSortedList(existing.Versions, package.Versions);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -210,23 +229,23 @@ namespace Emby.Server.Implementations.Updates
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IEnumerable<PackageInfo> FilterPackages(
|
public IEnumerable<PackageInfo> FilterPackages(
|
||||||
IEnumerable<PackageInfo> availablePackages,
|
IEnumerable<PackageInfo> availablePackages,
|
||||||
string name = null,
|
string? name = null,
|
||||||
Guid guid = default,
|
Guid? id = default,
|
||||||
Version specificVersion = null)
|
Version? specificVersion = null)
|
||||||
{
|
{
|
||||||
if (name != 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)
|
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;
|
return availablePackages;
|
||||||
@@ -235,12 +254,12 @@ namespace Emby.Server.Implementations.Updates
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IEnumerable<InstallationInfo> GetCompatibleVersions(
|
public IEnumerable<InstallationInfo> GetCompatibleVersions(
|
||||||
IEnumerable<PackageInfo> availablePackages,
|
IEnumerable<PackageInfo> availablePackages,
|
||||||
string name = null,
|
string? name = null,
|
||||||
Guid guid = default,
|
Guid? id = default,
|
||||||
Version minVersion = null,
|
Version? minVersion = null,
|
||||||
Version specificVersion = 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
|
// Package not found in repository
|
||||||
if (package == null)
|
if (package == null)
|
||||||
@@ -249,8 +268,8 @@ namespace Emby.Server.Implementations.Updates
|
|||||||
}
|
}
|
||||||
|
|
||||||
var appVer = _applicationHost.ApplicationVersion;
|
var appVer = _applicationHost.ApplicationVersion;
|
||||||
var availableVersions = package.versions
|
var availableVersions = package.Versions
|
||||||
.Where(x => Version.Parse(x.targetAbi) <= appVer);
|
.Where(x => string.IsNullOrEmpty(x.TargetAbi) || Version.Parse(x.TargetAbi) <= appVer);
|
||||||
|
|
||||||
if (specificVersion != null)
|
if (specificVersion != null)
|
||||||
{
|
{
|
||||||
@@ -265,12 +284,13 @@ namespace Emby.Server.Implementations.Updates
|
|||||||
{
|
{
|
||||||
yield return new InstallationInfo
|
yield return new InstallationInfo
|
||||||
{
|
{
|
||||||
Changelog = v.changelog,
|
Changelog = v.Changelog,
|
||||||
Guid = new Guid(package.guid),
|
Id = new Guid(package.Id),
|
||||||
Name = package.name,
|
Name = package.Name,
|
||||||
Version = v.VersionNumber,
|
Version = v.VersionNumber,
|
||||||
SourceUrl = v.sourceUrl,
|
SourceUrl = v.SourceUrl,
|
||||||
Checksum = v.checksum
|
Checksum = v.Checksum,
|
||||||
|
PackageInfo = package
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -282,20 +302,6 @@ namespace Emby.Server.Implementations.Updates
|
|||||||
return GetAvailablePluginUpdates(catalog);
|
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 />
|
/// <inheritdoc />
|
||||||
public async Task InstallPackage(InstallationInfo package, CancellationToken cancellationToken)
|
public async Task InstallPackage(InstallationInfo package, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -372,143 +378,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>
|
/// <summary>
|
||||||
/// Uninstalls a plugin.
|
/// Uninstalls a plugin.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="plugin">The plugin.</param>
|
/// <param name="plugin">The <see cref="LocalPlugin"/> to uninstall.</param>
|
||||||
public void UninstallPlugin(IPlugin plugin)
|
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;
|
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
|
// Remove it the quick way for now
|
||||||
_applicationHost.RemovePlugin(plugin);
|
_pluginManager.RemovePlugin(plugin);
|
||||||
|
|
||||||
var path = plugin.AssemblyFilePath;
|
_eventManager.Publish(new PluginUninstalledEventArgs(plugin.GetPluginInfo()));
|
||||||
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));
|
|
||||||
|
|
||||||
_applicationHost.NotifyPendingRestart();
|
_applicationHost.NotifyPendingRestart();
|
||||||
}
|
}
|
||||||
@@ -518,7 +410,7 @@ namespace Emby.Server.Implementations.Updates
|
|||||||
{
|
{
|
||||||
lock (_currentInstallationsLock)
|
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)))
|
if (install == default((InstallationInfo, CancellationTokenSource)))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -547,14 +439,147 @@ namespace Emby.Server.Implementations.Updates
|
|||||||
{
|
{
|
||||||
lock (_currentInstallationsLock)
|
lock (_currentInstallationsLock)
|
||||||
{
|
{
|
||||||
foreach (var tuple in _currentInstallations)
|
foreach (var (info, token) in _currentInstallations)
|
||||||
{
|
{
|
||||||
tuple.token.Dispose();
|
token.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
_currentInstallations.Clear();
|
_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);
|
||||||
|
await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir);
|
||||||
|
_pluginManager.ImportPluginFrom(targetDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
|
||||||
|
await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
|
||||||
|
_logger.LogInformation(plugin == null ? "New plugin installed: {PluginName} {PluginVersion}" : "Plugin updated: {PluginName} {PluginVersion}", package.Name, package.Version);
|
||||||
|
|
||||||
|
return plugin != null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
28
Jellyfin.Api/Attributes/AcceptsFileAttribute.cs
Normal file
28
Jellyfin.Api/Attributes/AcceptsFileAttribute.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Attributes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Internal produces image attribute.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Method)]
|
||||||
|
public class AcceptsFileAttribute : Attribute
|
||||||
|
{
|
||||||
|
private readonly string[] _contentTypes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AcceptsFileAttribute"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="contentTypes">Content types this endpoint produces.</param>
|
||||||
|
public AcceptsFileAttribute(params string[] contentTypes)
|
||||||
|
{
|
||||||
|
_contentTypes = contentTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the configured content types.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>the configured content types.</returns>
|
||||||
|
public string[] GetContentTypes() => _contentTypes;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs
Normal file
18
Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace Jellyfin.Api.Attributes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Produces file attribute of "image/*".
|
||||||
|
/// </summary>
|
||||||
|
public class AcceptsImageFileAttribute : AcceptsFileAttribute
|
||||||
|
{
|
||||||
|
private const string ContentType = "image/*";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AcceptsImageFileAttribute"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public AcceptsImageFileAttribute()
|
||||||
|
: base(ContentType)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs
Normal file
12
Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Attributes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute to mark a parameter as obsolete.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Parameter)]
|
||||||
|
public class ParameterObsoleteAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ using Jellyfin.Api.Helpers;
|
|||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Controller.SyncPlay;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
@@ -13,20 +14,24 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class SyncPlayAccessHandler : BaseAuthorizationHandler<SyncPlayAccessRequirement>
|
public class SyncPlayAccessHandler : BaseAuthorizationHandler<SyncPlayAccessRequirement>
|
||||||
{
|
{
|
||||||
|
private readonly ISyncPlayManager _syncPlayManager;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SyncPlayAccessHandler"/> class.
|
/// Initializes a new instance of the <see cref="SyncPlayAccessHandler"/> class.
|
||||||
/// </summary>
|
/// </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="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||||
public SyncPlayAccessHandler(
|
public SyncPlayAccessHandler(
|
||||||
|
ISyncPlayManager syncPlayManager,
|
||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
INetworkManager networkManager,
|
INetworkManager networkManager,
|
||||||
IHttpContextAccessor httpContextAccessor)
|
IHttpContextAccessor httpContextAccessor)
|
||||||
: base(userManager, networkManager, httpContextAccessor)
|
: base(userManager, networkManager, httpContextAccessor)
|
||||||
{
|
{
|
||||||
|
_syncPlayManager = syncPlayManager;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,10 +47,52 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
|
|||||||
var userId = ClaimHelpers.GetUserId(context.User);
|
var userId = ClaimHelpers.GetUserId(context.User);
|
||||||
var user = _userManager.GetUserById(userId!.Value);
|
var user = _userManager.GetUserById(userId!.Value);
|
||||||
|
|
||||||
if ((requirement.RequiredAccess.HasValue && user.SyncPlayAccess == requirement.RequiredAccess)
|
if (requirement.RequiredAccess == SyncPlayAccessRequirementType.HasAccess)
|
||||||
|| user.SyncPlayAccess == SyncPlayAccess.CreateAndJoinGroups)
|
|
||||||
{
|
{
|
||||||
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
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,23 +11,15 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class.
|
/// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="requiredAccess">A value of <see cref="SyncPlayAccess"/>.</param>
|
/// <param name="requiredAccess">A value of <see cref="SyncPlayAccessRequirementType"/>.</param>
|
||||||
public SyncPlayAccessRequirement(SyncPlayAccess requiredAccess)
|
public SyncPlayAccessRequirement(SyncPlayAccessRequirementType requiredAccess)
|
||||||
{
|
{
|
||||||
RequiredAccess = requiredAccess;
|
RequiredAccess = requiredAccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class.
|
|
||||||
/// </summary>
|
|
||||||
public SyncPlayAccessRequirement()
|
|
||||||
{
|
|
||||||
RequiredAccess = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the required SyncPlay access.
|
/// Gets the required SyncPlay access.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SyncPlayAccess? RequiredAccess { get; }
|
public SyncPlayAccessRequirementType RequiredAccess { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,13 +51,23 @@ namespace Jellyfin.Api.Constants
|
|||||||
public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl";
|
public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for requiring access to SyncPlay.
|
/// Policy name for accessing SyncPlay.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string SyncPlayAccess = "SyncPlayAccess";
|
public const string SyncPlayHasAccess = "SyncPlayHasAccess";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for requiring group creation access to SyncPlay.
|
/// Policy name for creating a SyncPlay group.
|
||||||
/// </summary>
|
/// </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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? videoBitRate,
|
[FromQuery] int? videoBitRate,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
[FromQuery] int? maxRefFrames,
|
[FromQuery] int? maxRefFrames,
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
[FromQuery] bool? requireAvc,
|
[FromQuery] bool? requireAvc,
|
||||||
@@ -174,7 +174,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
Height = height,
|
Height = height,
|
||||||
VideoBitRate = videoBitRate,
|
VideoBitRate = videoBitRate,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
SubtitleMethod = subtitleMethod,
|
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||||
MaxRefFrames = maxRefFrames,
|
MaxRefFrames = maxRefFrames,
|
||||||
MaxVideoBitDepth = maxVideoBitDepth,
|
MaxVideoBitDepth = maxVideoBitDepth,
|
||||||
RequireAvc = requireAvc ?? true,
|
RequireAvc = requireAvc ?? true,
|
||||||
@@ -287,7 +287,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? videoBitRate,
|
[FromQuery] int? videoBitRate,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
[FromQuery] int? maxRefFrames,
|
[FromQuery] int? maxRefFrames,
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
[FromQuery] bool? requireAvc,
|
[FromQuery] bool? requireAvc,
|
||||||
@@ -339,7 +339,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
Height = height,
|
Height = height,
|
||||||
VideoBitRate = videoBitRate,
|
VideoBitRate = videoBitRate,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
SubtitleMethod = subtitleMethod,
|
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||||
MaxRefFrames = maxRefFrames,
|
MaxRefFrames = maxRefFrames,
|
||||||
MaxVideoBitDepth = maxVideoBitDepth,
|
MaxVideoBitDepth = maxVideoBitDepth,
|
||||||
RequireAvc = requireAvc ?? true,
|
RequireAvc = requireAvc ?? true,
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.Extensions;
|
|
||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
using Jellyfin.Api.ModelBinders;
|
using Jellyfin.Api.ModelBinders;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Controller.Channels;
|
using MediaBrowser.Controller.Channels;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
@@ -121,9 +120,9 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] Guid? userId,
|
[FromQuery] Guid? userId,
|
||||||
[FromQuery] int? startIndex,
|
[FromQuery] int? startIndex,
|
||||||
[FromQuery] int? limit,
|
[FromQuery] int? limit,
|
||||||
[FromQuery] string? sortOrder,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
||||||
[FromQuery] string? sortBy,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
|
||||||
{
|
{
|
||||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||||
|
|||||||
@@ -29,18 +29,22 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
private readonly ILogger<DashboardController> _logger;
|
private readonly ILogger<DashboardController> _logger;
|
||||||
private readonly IServerApplicationHost _appHost;
|
private readonly IServerApplicationHost _appHost;
|
||||||
|
private readonly IPluginManager _pluginManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DashboardController"/> class.
|
/// Initializes a new instance of the <see cref="DashboardController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
|
/// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
|
||||||
/// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
|
/// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
|
||||||
|
/// <param name="pluginManager">Instance of <see cref="IPluginManager"/> interface.</param>
|
||||||
public DashboardController(
|
public DashboardController(
|
||||||
ILogger<DashboardController> logger,
|
ILogger<DashboardController> logger,
|
||||||
IServerApplicationHost appHost)
|
IServerApplicationHost appHost,
|
||||||
|
IPluginManager pluginManager)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_appHost = appHost;
|
_appHost = appHost;
|
||||||
|
_pluginManager = pluginManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -83,7 +87,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
.Where(i => i != null)
|
.Where(i => i != null)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages));
|
configPages.AddRange(_pluginManager.Plugins.SelectMany(GetConfigPages));
|
||||||
|
|
||||||
if (pageType.HasValue)
|
if (pageType.HasValue)
|
||||||
{
|
{
|
||||||
@@ -155,24 +159,24 @@ namespace Jellyfin.Api.Controllers
|
|||||||
return NotFound();
|
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 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()
|
private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages()
|
||||||
{
|
{
|
||||||
return _appHost.Plugins.SelectMany(GetPluginPages);
|
return _pluginManager.Plugins.SelectMany(GetPluginPages);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using MediaBrowser.Model.Entities;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers
|
||||||
{
|
{
|
||||||
@@ -22,14 +23,17 @@ namespace Jellyfin.Api.Controllers
|
|||||||
public class DisplayPreferencesController : BaseJellyfinApiController
|
public class DisplayPreferencesController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
private readonly IDisplayPreferencesManager _displayPreferencesManager;
|
private readonly IDisplayPreferencesManager _displayPreferencesManager;
|
||||||
|
private readonly ILogger<DisplayPreferencesController> _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class.
|
/// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="displayPreferencesManager">Instance of <see cref="IDisplayPreferencesManager"/> interface.</param>
|
/// <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;
|
_displayPreferencesManager = displayPreferencesManager;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -61,7 +65,6 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
Client = displayPreferences.Client,
|
Client = displayPreferences.Client,
|
||||||
Id = displayPreferences.ItemId.ToString(),
|
Id = displayPreferences.ItemId.ToString(),
|
||||||
ViewType = itemPreferences.ViewType.ToString(),
|
|
||||||
SortBy = itemPreferences.SortBy,
|
SortBy = itemPreferences.SortBy,
|
||||||
SortOrder = itemPreferences.SortOrder,
|
SortOrder = itemPreferences.SortOrder,
|
||||||
IndexBy = displayPreferences.IndexBy?.ToString(),
|
IndexBy = displayPreferences.IndexBy?.ToString(),
|
||||||
@@ -77,16 +80,12 @@ namespace Jellyfin.Api.Controllers
|
|||||||
dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant();
|
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["chromecastVersion"] = displayPreferences.ChromecastVersion.ToString().ToLowerInvariant();
|
||||||
dto.CustomPrefs["skipForwardLength"] = displayPreferences.SkipForwardLength.ToString(CultureInfo.InvariantCulture);
|
dto.CustomPrefs["skipForwardLength"] = displayPreferences.SkipForwardLength.ToString(CultureInfo.InvariantCulture);
|
||||||
dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString(CultureInfo.InvariantCulture);
|
dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString(CultureInfo.InvariantCulture);
|
||||||
dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture);
|
dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture);
|
||||||
dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
|
dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
|
||||||
|
dto.CustomPrefs["dashboardTheme"] = displayPreferences.DashboardTheme;
|
||||||
|
|
||||||
// Load all custom display preferences
|
// Load all custom display preferences
|
||||||
var customDisplayPreferences = _displayPreferencesManager.ListCustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);
|
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)))
|
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);
|
_logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]);
|
||||||
itemPreferences.ViewType = Enum.Parse<ViewType>(displayPreferences.ViewType);
|
|
||||||
displayPreferences.CustomPrefs.Remove(key);
|
displayPreferences.CustomPrefs.Remove(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,11 +202,6 @@ namespace Jellyfin.Api.Controllers
|
|||||||
itemPrefs.RememberSorting = displayPreferences.RememberSorting;
|
itemPrefs.RememberSorting = displayPreferences.RememberSorting;
|
||||||
itemPrefs.ItemId = itemId;
|
itemPrefs.ItemId = itemId;
|
||||||
|
|
||||||
if (Enum.TryParse<ViewType>(displayPreferences.ViewType, true, out var viewType))
|
|
||||||
{
|
|
||||||
itemPrefs.ViewType = viewType;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set all remaining custom preferences.
|
// Set all remaining custom preferences.
|
||||||
_displayPreferencesManager.SetCustomItemDisplayPreferences(userId, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs);
|
_displayPreferencesManager.SetCustomItemDisplayPreferences(userId, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs);
|
||||||
_displayPreferencesManager.SaveChanges();
|
_displayPreferencesManager.SaveChanges();
|
||||||
|
|||||||
@@ -41,18 +41,25 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serverId">Server UUID.</param>
|
/// <param name="serverId">Server UUID.</param>
|
||||||
/// <response code="200">Description xml returned.</response>
|
/// <response code="200">Description xml returned.</response>
|
||||||
|
/// <response code="503">DLNA is disabled.</response>
|
||||||
/// <returns>An <see cref="OkResult"/> containing the description xml.</returns>
|
/// <returns>An <see cref="OkResult"/> containing the description xml.</returns>
|
||||||
[HttpGet("{serverId}/description")]
|
[HttpGet("{serverId}/description")]
|
||||||
[HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")]
|
[HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
[Produces(MediaTypeNames.Text.Xml)]
|
[Produces(MediaTypeNames.Text.Xml)]
|
||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
public ActionResult GetDescriptionXml([FromRoute, Required] string serverId)
|
public ActionResult GetDescriptionXml([FromRoute, Required] string serverId)
|
||||||
{
|
{
|
||||||
var url = GetAbsoluteUri();
|
if (DlnaEntryPoint.Enabled)
|
||||||
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
|
{
|
||||||
var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress);
|
var url = GetAbsoluteUri();
|
||||||
return Ok(xml);
|
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>
|
/// <summary>
|
||||||
@@ -60,17 +67,24 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serverId">Server UUID.</param>
|
/// <param name="serverId">Server UUID.</param>
|
||||||
/// <response code="200">Dlna content directory returned.</response>
|
/// <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>
|
/// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns>
|
||||||
[HttpGet("{serverId}/ContentDirectory")]
|
[HttpGet("{serverId}/ContentDirectory")]
|
||||||
[HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")]
|
[HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")]
|
||||||
[HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")]
|
[HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
[Produces(MediaTypeNames.Text.Xml)]
|
[Produces(MediaTypeNames.Text.Xml)]
|
||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
public ActionResult GetContentDirectory([FromRoute, Required] string serverId)
|
public ActionResult GetContentDirectory([FromRoute, Required] string serverId)
|
||||||
{
|
{
|
||||||
return Ok(_contentDirectory.GetServiceXml());
|
if (DlnaEntryPoint.Enabled)
|
||||||
|
{
|
||||||
|
return Ok(_contentDirectory.GetServiceXml());
|
||||||
|
}
|
||||||
|
|
||||||
|
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -78,17 +92,24 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serverId">Server UUID.</param>
|
/// <param name="serverId">Server UUID.</param>
|
||||||
/// <response code="200">Dlna media receiver registrar xml returned.</response>
|
/// <response code="200">Dlna media receiver registrar xml returned.</response>
|
||||||
|
/// <response code="503">DLNA is disabled.</response>
|
||||||
/// <returns>Dlna media receiver registrar xml.</returns>
|
/// <returns>Dlna media receiver registrar xml.</returns>
|
||||||
[HttpGet("{serverId}/MediaReceiverRegistrar")]
|
[HttpGet("{serverId}/MediaReceiverRegistrar")]
|
||||||
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")]
|
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")]
|
||||||
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")]
|
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
[Produces(MediaTypeNames.Text.Xml)]
|
[Produces(MediaTypeNames.Text.Xml)]
|
||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
|
public ActionResult GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
|
||||||
{
|
{
|
||||||
return Ok(_mediaReceiverRegistrar.GetServiceXml());
|
if (DlnaEntryPoint.Enabled)
|
||||||
|
{
|
||||||
|
return Ok(_mediaReceiverRegistrar.GetServiceXml());
|
||||||
|
}
|
||||||
|
|
||||||
|
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -96,17 +117,24 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serverId">Server UUID.</param>
|
/// <param name="serverId">Server UUID.</param>
|
||||||
/// <response code="200">Dlna media receiver registrar xml returned.</response>
|
/// <response code="200">Dlna media receiver registrar xml returned.</response>
|
||||||
|
/// <response code="503">DLNA is disabled.</response>
|
||||||
/// <returns>Dlna media receiver registrar xml.</returns>
|
/// <returns>Dlna media receiver registrar xml.</returns>
|
||||||
[HttpGet("{serverId}/ConnectionManager")]
|
[HttpGet("{serverId}/ConnectionManager")]
|
||||||
[HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")]
|
[HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")]
|
||||||
[HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")]
|
[HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
[Produces(MediaTypeNames.Text.Xml)]
|
[Produces(MediaTypeNames.Text.Xml)]
|
||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
public ActionResult GetConnectionManager([FromRoute, Required] string serverId)
|
public ActionResult GetConnectionManager([FromRoute, Required] string serverId)
|
||||||
{
|
{
|
||||||
return Ok(_connectionManager.GetServiceXml());
|
if (DlnaEntryPoint.Enabled)
|
||||||
|
{
|
||||||
|
return Ok(_connectionManager.GetServiceXml());
|
||||||
|
}
|
||||||
|
|
||||||
|
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -114,14 +142,21 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serverId">Server UUID.</param>
|
/// <param name="serverId">Server UUID.</param>
|
||||||
/// <response code="200">Request processed.</response>
|
/// <response code="200">Request processed.</response>
|
||||||
|
/// <response code="503">DLNA is disabled.</response>
|
||||||
/// <returns>Control response.</returns>
|
/// <returns>Control response.</returns>
|
||||||
[HttpPost("{serverId}/ContentDirectory/Control")]
|
[HttpPost("{serverId}/ContentDirectory/Control")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
[Produces(MediaTypeNames.Text.Xml)]
|
[Produces(MediaTypeNames.Text.Xml)]
|
||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId)
|
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>
|
/// <summary>
|
||||||
@@ -129,14 +164,21 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serverId">Server UUID.</param>
|
/// <param name="serverId">Server UUID.</param>
|
||||||
/// <response code="200">Request processed.</response>
|
/// <response code="200">Request processed.</response>
|
||||||
|
/// <response code="503">DLNA is disabled.</response>
|
||||||
/// <returns>Control response.</returns>
|
/// <returns>Control response.</returns>
|
||||||
[HttpPost("{serverId}/ConnectionManager/Control")]
|
[HttpPost("{serverId}/ConnectionManager/Control")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
[Produces(MediaTypeNames.Text.Xml)]
|
[Produces(MediaTypeNames.Text.Xml)]
|
||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId)
|
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>
|
/// <summary>
|
||||||
@@ -144,14 +186,21 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serverId">Server UUID.</param>
|
/// <param name="serverId">Server UUID.</param>
|
||||||
/// <response code="200">Request processed.</response>
|
/// <response code="200">Request processed.</response>
|
||||||
|
/// <response code="503">DLNA is disabled.</response>
|
||||||
/// <returns>Control response.</returns>
|
/// <returns>Control response.</returns>
|
||||||
[HttpPost("{serverId}/MediaReceiverRegistrar/Control")]
|
[HttpPost("{serverId}/MediaReceiverRegistrar/Control")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
[Produces(MediaTypeNames.Text.Xml)]
|
[Produces(MediaTypeNames.Text.Xml)]
|
||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId)
|
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>
|
/// <summary>
|
||||||
@@ -159,17 +208,24 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serverId">Server UUID.</param>
|
/// <param name="serverId">Server UUID.</param>
|
||||||
/// <response code="200">Request processed.</response>
|
/// <response code="200">Request processed.</response>
|
||||||
|
/// <response code="503">DLNA is disabled.</response>
|
||||||
/// <returns>Event subscription response.</returns>
|
/// <returns>Event subscription response.</returns>
|
||||||
[HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")]
|
[HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")]
|
||||||
[HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")]
|
[HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")]
|
||||||
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
|
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
[Produces(MediaTypeNames.Text.Xml)]
|
[Produces(MediaTypeNames.Text.Xml)]
|
||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId)
|
public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId)
|
||||||
{
|
{
|
||||||
return ProcessEventRequest(_mediaReceiverRegistrar);
|
if (DlnaEntryPoint.Enabled)
|
||||||
|
{
|
||||||
|
return ProcessEventRequest(_mediaReceiverRegistrar);
|
||||||
|
}
|
||||||
|
|
||||||
|
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -177,17 +233,24 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serverId">Server UUID.</param>
|
/// <param name="serverId">Server UUID.</param>
|
||||||
/// <response code="200">Request processed.</response>
|
/// <response code="200">Request processed.</response>
|
||||||
|
/// <response code="503">DLNA is disabled.</response>
|
||||||
/// <returns>Event subscription response.</returns>
|
/// <returns>Event subscription response.</returns>
|
||||||
[HttpSubscribe("{serverId}/ContentDirectory/Events")]
|
[HttpSubscribe("{serverId}/ContentDirectory/Events")]
|
||||||
[HttpUnsubscribe("{serverId}/ContentDirectory/Events")]
|
[HttpUnsubscribe("{serverId}/ContentDirectory/Events")]
|
||||||
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
|
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
[Produces(MediaTypeNames.Text.Xml)]
|
[Produces(MediaTypeNames.Text.Xml)]
|
||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId)
|
public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId)
|
||||||
{
|
{
|
||||||
return ProcessEventRequest(_contentDirectory);
|
if (DlnaEntryPoint.Enabled)
|
||||||
|
{
|
||||||
|
return ProcessEventRequest(_contentDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -195,17 +258,24 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serverId">Server UUID.</param>
|
/// <param name="serverId">Server UUID.</param>
|
||||||
/// <response code="200">Request processed.</response>
|
/// <response code="200">Request processed.</response>
|
||||||
|
/// <response code="503">DLNA is disabled.</response>
|
||||||
/// <returns>Event subscription response.</returns>
|
/// <returns>Event subscription response.</returns>
|
||||||
[HttpSubscribe("{serverId}/ConnectionManager/Events")]
|
[HttpSubscribe("{serverId}/ConnectionManager/Events")]
|
||||||
[HttpUnsubscribe("{serverId}/ConnectionManager/Events")]
|
[HttpUnsubscribe("{serverId}/ConnectionManager/Events")]
|
||||||
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
|
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
[Produces(MediaTypeNames.Text.Xml)]
|
[Produces(MediaTypeNames.Text.Xml)]
|
||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId)
|
public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId)
|
||||||
{
|
{
|
||||||
return ProcessEventRequest(_connectionManager);
|
if (DlnaEntryPoint.Enabled)
|
||||||
|
{
|
||||||
|
return ProcessEventRequest(_connectionManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
return StatusCode(StatusCodes.Status503ServiceUnavailable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -213,14 +283,24 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serverId">Server UUID.</param>
|
/// <param name="serverId">Server UUID.</param>
|
||||||
/// <param name="fileName">The icon filename.</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>
|
/// <returns>Icon stream.</returns>
|
||||||
[HttpGet("{serverId}/icons/{fileName}")]
|
[HttpGet("{serverId}/icons/{fileName}")]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
[ProducesImageFile]
|
[ProducesImageFile]
|
||||||
public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName)
|
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>
|
/// <summary>
|
||||||
@@ -228,11 +308,22 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="fileName">The icon filename.</param>
|
/// <param name="fileName">The icon filename.</param>
|
||||||
/// <returns>Icon stream.</returns>
|
/// <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}")]
|
[HttpGet("icons/{fileName}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
[ProducesImageFile]
|
[ProducesImageFile]
|
||||||
public ActionResult GetIcon([FromRoute, Required] string fileName)
|
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)
|
private ActionResult GetIconInternal(string fileName)
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? videoBitRate,
|
[FromQuery] int? videoBitRate,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
[FromQuery] int? maxRefFrames,
|
[FromQuery] int? maxRefFrames,
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
[FromQuery] bool? requireAvc,
|
[FromQuery] bool? requireAvc,
|
||||||
@@ -219,7 +219,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
[FromQuery] EncodingContext context,
|
[FromQuery] EncodingContext? context,
|
||||||
[FromQuery] Dictionary<string, string> streamOptions,
|
[FromQuery] Dictionary<string, string> streamOptions,
|
||||||
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
|
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
|
||||||
{
|
{
|
||||||
@@ -256,7 +256,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
Height = height,
|
Height = height,
|
||||||
VideoBitRate = videoBitRate,
|
VideoBitRate = videoBitRate,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
SubtitleMethod = subtitleMethod,
|
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||||
MaxRefFrames = maxRefFrames,
|
MaxRefFrames = maxRefFrames,
|
||||||
MaxVideoBitDepth = maxVideoBitDepth,
|
MaxVideoBitDepth = maxVideoBitDepth,
|
||||||
RequireAvc = requireAvc ?? true,
|
RequireAvc = requireAvc ?? true,
|
||||||
@@ -271,7 +271,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
TranscodeReasons = transcodeReasons,
|
TranscodeReasons = transcodeReasons,
|
||||||
AudioStreamIndex = audioStreamIndex,
|
AudioStreamIndex = audioStreamIndex,
|
||||||
VideoStreamIndex = videoStreamIndex,
|
VideoStreamIndex = videoStreamIndex,
|
||||||
Context = context,
|
Context = context ?? EncodingContext.Streaming,
|
||||||
StreamOptions = streamOptions,
|
StreamOptions = streamOptions,
|
||||||
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
|
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
|
||||||
};
|
};
|
||||||
@@ -371,7 +371,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? videoBitRate,
|
[FromQuery] int? videoBitRate,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
[FromQuery] int? maxRefFrames,
|
[FromQuery] int? maxRefFrames,
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
[FromQuery] bool? requireAvc,
|
[FromQuery] bool? requireAvc,
|
||||||
@@ -386,7 +386,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
[FromQuery] EncodingContext context,
|
[FromQuery] EncodingContext? context,
|
||||||
[FromQuery] Dictionary<string, string> streamOptions,
|
[FromQuery] Dictionary<string, string> streamOptions,
|
||||||
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
|
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
|
||||||
{
|
{
|
||||||
@@ -423,7 +423,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
Height = height,
|
Height = height,
|
||||||
VideoBitRate = videoBitRate,
|
VideoBitRate = videoBitRate,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
SubtitleMethod = subtitleMethod,
|
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||||
MaxRefFrames = maxRefFrames,
|
MaxRefFrames = maxRefFrames,
|
||||||
MaxVideoBitDepth = maxVideoBitDepth,
|
MaxVideoBitDepth = maxVideoBitDepth,
|
||||||
RequireAvc = requireAvc ?? true,
|
RequireAvc = requireAvc ?? true,
|
||||||
@@ -438,7 +438,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
TranscodeReasons = transcodeReasons,
|
TranscodeReasons = transcodeReasons,
|
||||||
AudioStreamIndex = audioStreamIndex,
|
AudioStreamIndex = audioStreamIndex,
|
||||||
VideoStreamIndex = videoStreamIndex,
|
VideoStreamIndex = videoStreamIndex,
|
||||||
Context = context,
|
Context = context ?? EncodingContext.Streaming,
|
||||||
StreamOptions = streamOptions,
|
StreamOptions = streamOptions,
|
||||||
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
|
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
|
||||||
};
|
};
|
||||||
@@ -534,7 +534,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? videoBitRate,
|
[FromQuery] int? videoBitRate,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
[FromQuery] int? maxRefFrames,
|
[FromQuery] int? maxRefFrames,
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
[FromQuery] bool? requireAvc,
|
[FromQuery] bool? requireAvc,
|
||||||
@@ -549,7 +549,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
[FromQuery] EncodingContext context,
|
[FromQuery] EncodingContext? context,
|
||||||
[FromQuery] Dictionary<string, string> streamOptions)
|
[FromQuery] Dictionary<string, string> streamOptions)
|
||||||
{
|
{
|
||||||
var cancellationTokenSource = new CancellationTokenSource();
|
var cancellationTokenSource = new CancellationTokenSource();
|
||||||
@@ -586,7 +586,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
Height = height,
|
Height = height,
|
||||||
VideoBitRate = videoBitRate,
|
VideoBitRate = videoBitRate,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
SubtitleMethod = subtitleMethod,
|
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||||
MaxRefFrames = maxRefFrames,
|
MaxRefFrames = maxRefFrames,
|
||||||
MaxVideoBitDepth = maxVideoBitDepth,
|
MaxVideoBitDepth = maxVideoBitDepth,
|
||||||
RequireAvc = requireAvc ?? true,
|
RequireAvc = requireAvc ?? true,
|
||||||
@@ -601,7 +601,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
TranscodeReasons = transcodeReasons,
|
TranscodeReasons = transcodeReasons,
|
||||||
AudioStreamIndex = audioStreamIndex,
|
AudioStreamIndex = audioStreamIndex,
|
||||||
VideoStreamIndex = videoStreamIndex,
|
VideoStreamIndex = videoStreamIndex,
|
||||||
Context = context,
|
Context = context ?? EncodingContext.Streaming,
|
||||||
StreamOptions = streamOptions
|
StreamOptions = streamOptions
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -699,7 +699,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? videoBitRate,
|
[FromQuery] int? videoBitRate,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
[FromQuery] int? maxRefFrames,
|
[FromQuery] int? maxRefFrames,
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
[FromQuery] bool? requireAvc,
|
[FromQuery] bool? requireAvc,
|
||||||
@@ -714,7 +714,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
[FromQuery] EncodingContext context,
|
[FromQuery] EncodingContext? context,
|
||||||
[FromQuery] Dictionary<string, string> streamOptions)
|
[FromQuery] Dictionary<string, string> streamOptions)
|
||||||
{
|
{
|
||||||
var cancellationTokenSource = new CancellationTokenSource();
|
var cancellationTokenSource = new CancellationTokenSource();
|
||||||
@@ -751,7 +751,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
Height = height,
|
Height = height,
|
||||||
VideoBitRate = videoBitRate,
|
VideoBitRate = videoBitRate,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
SubtitleMethod = subtitleMethod,
|
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||||
MaxRefFrames = maxRefFrames,
|
MaxRefFrames = maxRefFrames,
|
||||||
MaxVideoBitDepth = maxVideoBitDepth,
|
MaxVideoBitDepth = maxVideoBitDepth,
|
||||||
RequireAvc = requireAvc ?? true,
|
RequireAvc = requireAvc ?? true,
|
||||||
@@ -766,7 +766,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
TranscodeReasons = transcodeReasons,
|
TranscodeReasons = transcodeReasons,
|
||||||
AudioStreamIndex = audioStreamIndex,
|
AudioStreamIndex = audioStreamIndex,
|
||||||
VideoStreamIndex = videoStreamIndex,
|
VideoStreamIndex = videoStreamIndex,
|
||||||
Context = context,
|
Context = context ?? EncodingContext.Streaming,
|
||||||
StreamOptions = streamOptions
|
StreamOptions = streamOptions
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -869,7 +869,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? videoBitRate,
|
[FromQuery] int? videoBitRate,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
[FromQuery] int? maxRefFrames,
|
[FromQuery] int? maxRefFrames,
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
[FromQuery] bool? requireAvc,
|
[FromQuery] bool? requireAvc,
|
||||||
@@ -884,7 +884,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
[FromQuery] EncodingContext context,
|
[FromQuery] EncodingContext? context,
|
||||||
[FromQuery] Dictionary<string, string> streamOptions)
|
[FromQuery] Dictionary<string, string> streamOptions)
|
||||||
{
|
{
|
||||||
var streamingRequest = new VideoRequestDto
|
var streamingRequest = new VideoRequestDto
|
||||||
@@ -921,7 +921,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
Height = height,
|
Height = height,
|
||||||
VideoBitRate = videoBitRate,
|
VideoBitRate = videoBitRate,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
SubtitleMethod = subtitleMethod,
|
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||||
MaxRefFrames = maxRefFrames,
|
MaxRefFrames = maxRefFrames,
|
||||||
MaxVideoBitDepth = maxVideoBitDepth,
|
MaxVideoBitDepth = maxVideoBitDepth,
|
||||||
RequireAvc = requireAvc ?? true,
|
RequireAvc = requireAvc ?? true,
|
||||||
@@ -936,7 +936,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
TranscodeReasons = transcodeReasons,
|
TranscodeReasons = transcodeReasons,
|
||||||
AudioStreamIndex = audioStreamIndex,
|
AudioStreamIndex = audioStreamIndex,
|
||||||
VideoStreamIndex = videoStreamIndex,
|
VideoStreamIndex = videoStreamIndex,
|
||||||
Context = context,
|
Context = context ?? EncodingContext.Streaming,
|
||||||
StreamOptions = streamOptions
|
StreamOptions = streamOptions
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1041,7 +1041,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? videoBitRate,
|
[FromQuery] int? videoBitRate,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
[FromQuery] int? maxRefFrames,
|
[FromQuery] int? maxRefFrames,
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
[FromQuery] bool? requireAvc,
|
[FromQuery] bool? requireAvc,
|
||||||
@@ -1056,7 +1056,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
[FromQuery] EncodingContext context,
|
[FromQuery] EncodingContext? context,
|
||||||
[FromQuery] Dictionary<string, string> streamOptions)
|
[FromQuery] Dictionary<string, string> streamOptions)
|
||||||
{
|
{
|
||||||
var streamingRequest = new StreamingRequestDto
|
var streamingRequest = new StreamingRequestDto
|
||||||
@@ -1093,7 +1093,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
Height = height,
|
Height = height,
|
||||||
VideoBitRate = videoBitRate,
|
VideoBitRate = videoBitRate,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
SubtitleMethod = subtitleMethod,
|
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||||
MaxRefFrames = maxRefFrames,
|
MaxRefFrames = maxRefFrames,
|
||||||
MaxVideoBitDepth = maxVideoBitDepth,
|
MaxVideoBitDepth = maxVideoBitDepth,
|
||||||
RequireAvc = requireAvc ?? true,
|
RequireAvc = requireAvc ?? true,
|
||||||
@@ -1108,7 +1108,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
TranscodeReasons = transcodeReasons,
|
TranscodeReasons = transcodeReasons,
|
||||||
AudioStreamIndex = audioStreamIndex,
|
AudioStreamIndex = audioStreamIndex,
|
||||||
VideoStreamIndex = videoStreamIndex,
|
VideoStreamIndex = videoStreamIndex,
|
||||||
Context = context,
|
Context = context ?? EncodingContext.Streaming,
|
||||||
StreamOptions = streamOptions
|
StreamOptions = streamOptions
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,9 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[HttpDelete("Videos/ActiveEncodings")]
|
[HttpDelete("Videos/ActiveEncodings")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult StopEncodingProcess([FromQuery] string deviceId, [FromQuery] string playSessionId)
|
public ActionResult StopEncodingProcess(
|
||||||
|
[FromQuery, Required] string deviceId,
|
||||||
|
[FromQuery, Required] string playSessionId)
|
||||||
{
|
{
|
||||||
_transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true);
|
_transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("Users/{userId}/Images/{imageType}")]
|
[HttpPost("Users/{userId}/Images/{imageType}")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
|
[AcceptsImageFile]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
|
||||||
@@ -98,7 +99,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
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);
|
var user = _userManager.GetUserById(userId);
|
||||||
@@ -133,6 +134,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("Users/{userId}/Images/{imageType}/{index}")]
|
[HttpPost("Users/{userId}/Images/{imageType}/{index}")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
|
[AcceptsImageFile]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
|
||||||
@@ -144,7 +146,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
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);
|
var user = _userManager.GetUserById(userId);
|
||||||
@@ -190,7 +192,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
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);
|
var user = _userManager.GetUserById(userId);
|
||||||
@@ -229,7 +231,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
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);
|
var user = _userManager.GetUserById(userId);
|
||||||
@@ -312,6 +314,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
||||||
[HttpPost("Items/{itemId}/Images/{imageType}")]
|
[HttpPost("Items/{itemId}/Images/{imageType}")]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
|
[AcceptsImageFile]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
||||||
@@ -325,9 +328,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||||
|
|
||||||
// Handle image/png; charset=utf-8
|
// Handle image/png; charset=utf-8
|
||||||
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
|
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);
|
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
@@ -344,6 +349,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
||||||
[HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")]
|
[HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
|
[AcceptsImageFile]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
||||||
@@ -358,9 +364,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||||
|
|
||||||
// Handle image/png; charset=utf-8
|
// Handle image/png; charset=utf-8
|
||||||
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
|
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);
|
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
@@ -384,7 +392,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromRoute, Required] ImageType imageType,
|
[FromRoute, Required] ImageType imageType,
|
||||||
[FromRoute, Required] int imageIndex,
|
[FromRoute, Required] int imageIndex,
|
||||||
[FromQuery] int newIndex)
|
[FromQuery, Required] int newIndex)
|
||||||
{
|
{
|
||||||
var item = _libraryManager.GetItemById(itemId);
|
var item = _libraryManager.GetItemById(itemId);
|
||||||
if (item == null)
|
if (item == null)
|
||||||
@@ -733,7 +741,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
public async Task<ActionResult> GetArtistImage(
|
public async Task<ActionResult> GetArtistImage(
|
||||||
[FromRoute, Required] string name,
|
[FromRoute, Required] string name,
|
||||||
[FromRoute, Required] ImageType imageType,
|
[FromRoute, Required] ImageType imageType,
|
||||||
[FromQuery] string tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery] ImageFormat? format,
|
[FromQuery] ImageFormat? format,
|
||||||
[FromQuery] int? maxWidth,
|
[FromQuery] int? maxWidth,
|
||||||
[FromQuery] int? maxHeight,
|
[FromQuery] int? maxHeight,
|
||||||
@@ -812,7 +820,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
public async Task<ActionResult> GetGenreImage(
|
public async Task<ActionResult> GetGenreImage(
|
||||||
[FromRoute, Required] string name,
|
[FromRoute, Required] string name,
|
||||||
[FromRoute, Required] ImageType imageType,
|
[FromRoute, Required] ImageType imageType,
|
||||||
[FromQuery] string tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery] ImageFormat? format,
|
[FromQuery] ImageFormat? format,
|
||||||
[FromQuery] int? maxWidth,
|
[FromQuery] int? maxWidth,
|
||||||
[FromQuery] int? maxHeight,
|
[FromQuery] int? maxHeight,
|
||||||
@@ -892,7 +900,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromRoute, Required] string name,
|
[FromRoute, Required] string name,
|
||||||
[FromRoute, Required] ImageType imageType,
|
[FromRoute, Required] ImageType imageType,
|
||||||
[FromRoute, Required] int imageIndex,
|
[FromRoute, Required] int imageIndex,
|
||||||
[FromQuery] string tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery] ImageFormat? format,
|
[FromQuery] ImageFormat? format,
|
||||||
[FromQuery] int? maxWidth,
|
[FromQuery] int? maxWidth,
|
||||||
[FromQuery] int? maxHeight,
|
[FromQuery] int? maxHeight,
|
||||||
@@ -970,7 +978,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
public async Task<ActionResult> GetMusicGenreImage(
|
public async Task<ActionResult> GetMusicGenreImage(
|
||||||
[FromRoute, Required] string name,
|
[FromRoute, Required] string name,
|
||||||
[FromRoute, Required] ImageType imageType,
|
[FromRoute, Required] ImageType imageType,
|
||||||
[FromQuery] string tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery] ImageFormat? format,
|
[FromQuery] ImageFormat? format,
|
||||||
[FromQuery] int? maxWidth,
|
[FromQuery] int? maxWidth,
|
||||||
[FromQuery] int? maxHeight,
|
[FromQuery] int? maxHeight,
|
||||||
@@ -1050,7 +1058,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromRoute, Required] string name,
|
[FromRoute, Required] string name,
|
||||||
[FromRoute, Required] ImageType imageType,
|
[FromRoute, Required] ImageType imageType,
|
||||||
[FromRoute, Required] int imageIndex,
|
[FromRoute, Required] int imageIndex,
|
||||||
[FromQuery] string tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery] ImageFormat? format,
|
[FromQuery] ImageFormat? format,
|
||||||
[FromQuery] int? maxWidth,
|
[FromQuery] int? maxWidth,
|
||||||
[FromQuery] int? maxHeight,
|
[FromQuery] int? maxHeight,
|
||||||
@@ -1128,7 +1136,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
public async Task<ActionResult> GetPersonImage(
|
public async Task<ActionResult> GetPersonImage(
|
||||||
[FromRoute, Required] string name,
|
[FromRoute, Required] string name,
|
||||||
[FromRoute, Required] ImageType imageType,
|
[FromRoute, Required] ImageType imageType,
|
||||||
[FromQuery] string tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery] ImageFormat? format,
|
[FromQuery] ImageFormat? format,
|
||||||
[FromQuery] int? maxWidth,
|
[FromQuery] int? maxWidth,
|
||||||
[FromQuery] int? maxHeight,
|
[FromQuery] int? maxHeight,
|
||||||
@@ -1208,7 +1216,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromRoute, Required] string name,
|
[FromRoute, Required] string name,
|
||||||
[FromRoute, Required] ImageType imageType,
|
[FromRoute, Required] ImageType imageType,
|
||||||
[FromRoute, Required] int imageIndex,
|
[FromRoute, Required] int imageIndex,
|
||||||
[FromQuery] string tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery] ImageFormat? format,
|
[FromQuery] ImageFormat? format,
|
||||||
[FromQuery] int? maxWidth,
|
[FromQuery] int? maxWidth,
|
||||||
[FromQuery] int? maxHeight,
|
[FromQuery] int? maxHeight,
|
||||||
|
|||||||
@@ -346,11 +346,12 @@ namespace Jellyfin.Api.Controllers
|
|||||||
Directory.CreateDirectory(directory);
|
Directory.CreateDirectory(directory);
|
||||||
using (var stream = result.Content)
|
using (var stream = result.Content)
|
||||||
{
|
{
|
||||||
|
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||||
await using var fileStream = new FileStream(
|
await using var fileStream = new FileStream(
|
||||||
fullCachePath,
|
fullCachePath,
|
||||||
FileMode.Create,
|
FileMode.Create,
|
||||||
FileAccess.Write,
|
FileAccess.Write,
|
||||||
FileShare.Read,
|
FileShare.None,
|
||||||
IODefaults.FileStreamBufferSize,
|
IODefaults.FileStreamBufferSize,
|
||||||
true);
|
true);
|
||||||
|
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[HttpPost("Items/{itemId}/ContentType")]
|
[HttpPost("Items/{itemId}/ContentType")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string contentType)
|
public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string? contentType)
|
||||||
{
|
{
|
||||||
var item = _libraryManager.GetItemById(itemId);
|
var item = _libraryManager.GetItemById(itemId);
|
||||||
if (item == null)
|
if (item == null)
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? limit,
|
[FromQuery] int? limit,
|
||||||
[FromQuery] bool? recursive,
|
[FromQuery] bool? recursive,
|
||||||
[FromQuery] string? searchTerm,
|
[FromQuery] string? searchTerm,
|
||||||
[FromQuery] string? sortOrder,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||||
[FromQuery] Guid? parentId,
|
[FromQuery] Guid? parentId,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||||
@@ -184,7 +184,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] bool? isFavorite,
|
[FromQuery] bool? isFavorite,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
|
||||||
[FromQuery] string? sortBy,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||||
[FromQuery] bool? isPlayed,
|
[FromQuery] bool? isPlayed,
|
||||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
||||||
@@ -254,18 +254,18 @@ namespace Jellyfin.Api.Controllers
|
|||||||
includeItemTypes = new[] { "Playlist" };
|
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
|
// 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
|
// 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);
|
var collectionFolders = _libraryManager.GetCollectionFolders(item);
|
||||||
foreach (var collectionFolder in collectionFolders)
|
foreach (var collectionFolder in collectionFolders)
|
||||||
{
|
{
|
||||||
if (user.GetPreference(PreferenceKind.EnabledFolders).Contains(
|
if (user.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(collectionFolder.Id))
|
||||||
collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
|
|
||||||
StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
isInEnabledFolder = true;
|
isInEnabledFolder = true;
|
||||||
}
|
}
|
||||||
@@ -608,7 +608,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? limit,
|
[FromQuery] int? limit,
|
||||||
[FromQuery] bool? recursive,
|
[FromQuery] bool? recursive,
|
||||||
[FromQuery] string? searchTerm,
|
[FromQuery] string? searchTerm,
|
||||||
[FromQuery] string? sortOrder,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||||
[FromQuery] Guid? parentId,
|
[FromQuery] Guid? parentId,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||||
@@ -617,7 +617,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] bool? isFavorite,
|
[FromQuery] bool? isFavorite,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
|
||||||
[FromQuery] string? sortBy,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||||
[FromQuery] bool? isPlayed,
|
[FromQuery] bool? isPlayed,
|
||||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
||||||
@@ -786,12 +786,12 @@ namespace Jellyfin.Api.Controllers
|
|||||||
|
|
||||||
var ancestorIds = Array.Empty<Guid>();
|
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)
|
if (parentIdGuid.Equals(Guid.Empty) && excludeFolderIds.Length > 0)
|
||||||
{
|
{
|
||||||
ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true)
|
ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true)
|
||||||
.Where(i => i is Folder)
|
.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)
|
.Select(i => i.Id)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -667,7 +667,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO determine non-ASCII validity.
|
// TODO determine non-ASCII validity.
|
||||||
return PhysicalFile(path, MimeTypes.GetMimeType(path));
|
return PhysicalFile(path, MimeTypes.GetMimeType(path), filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -742,8 +742,6 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
Limit = limit,
|
Limit = limit,
|
||||||
IncludeItemTypes = includeItemTypes.ToArray(),
|
IncludeItemTypes = includeItemTypes.ToArray(),
|
||||||
IsMovie = isMovie,
|
|
||||||
IsSeries = isSeries,
|
|
||||||
SimilarTo = item,
|
SimilarTo = item,
|
||||||
DtoOptions = dtoOptions,
|
DtoOptions = dtoOptions,
|
||||||
EnableTotalRecordCount = !isMovie ?? true,
|
EnableTotalRecordCount = !isMovie ?? true,
|
||||||
@@ -780,7 +778,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo(
|
public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo(
|
||||||
[FromQuery] string? libraryContentType,
|
[FromQuery] string? libraryContentType,
|
||||||
[FromQuery] bool isNewLibrary)
|
[FromQuery] bool isNewLibrary = false)
|
||||||
{
|
{
|
||||||
var result = new LibraryOptionsResultDto();
|
var result = new LibraryOptionsResultDto();
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public async Task<ActionResult> AddVirtualFolder(
|
public async Task<ActionResult> AddVirtualFolder(
|
||||||
[FromQuery] string? name,
|
[FromQuery] string? name,
|
||||||
[FromQuery] string? collectionType,
|
[FromQuery] CollectionTypeOptions? collectionType,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
|
||||||
[FromBody] AddVirtualFolderDto? libraryOptionsDto,
|
[FromBody] AddVirtualFolderDto? libraryOptionsDto,
|
||||||
[FromQuery] bool refreshLibrary = false)
|
[FromQuery] bool refreshLibrary = false)
|
||||||
@@ -241,23 +241,20 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates a media path.
|
/// Updates a media path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="name">The name of the library.</param>
|
/// <param name="mediaPathRequestDto">The name of the library and path infos.</param>
|
||||||
/// <param name="pathInfo">The path info.</param>
|
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
/// <response code="204">Media path updated.</response>
|
/// <response code="204">Media path updated.</response>
|
||||||
/// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
|
/// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
|
||||||
[HttpPost("Paths/Update")]
|
[HttpPost("Paths/Update")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult UpdateMediaPath(
|
public ActionResult UpdateMediaPath([FromBody, Required] UpdateMediaPathRequestDto mediaPathRequestDto)
|
||||||
[FromQuery] string? name,
|
|
||||||
[FromBody] MediaPathInfo? pathInfo)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
if (string.IsNullOrWhiteSpace(mediaPathRequestDto.Name))
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(name));
|
throw new ArgumentNullException(nameof(mediaPathRequestDto), "Name must not be null or empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
_libraryManager.UpdateMediaPath(name, pathInfo);
|
_libraryManager.UpdateMediaPath(mediaPathRequestDto.Name, mediaPathRequestDto.PathInfo);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -553,8 +553,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] bool? isSports,
|
[FromQuery] bool? isSports,
|
||||||
[FromQuery] int? startIndex,
|
[FromQuery] int? startIndex,
|
||||||
[FromQuery] int? limit,
|
[FromQuery] int? limit,
|
||||||
[FromQuery] string? sortBy,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||||
[FromQuery] string? sortOrder,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
|
||||||
[FromQuery] bool? enableImages,
|
[FromQuery] bool? enableImages,
|
||||||
@@ -1119,20 +1119,15 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set channel mappings.
|
/// Set channel mappings.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="providerId">Provider id.</param>
|
/// <param name="setChannelMappingDto">The set channel mapping dto.</param>
|
||||||
/// <param name="tunerChannelId">Tuner channel id.</param>
|
|
||||||
/// <param name="providerChannelId">Provider channel id.</param>
|
|
||||||
/// <response code="200">Created channel mapping returned.</response>
|
/// <response code="200">Created channel mapping returned.</response>
|
||||||
/// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
|
/// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
|
||||||
[HttpPost("ChannelMappings")]
|
[HttpPost("ChannelMappings")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping(
|
public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto)
|
||||||
[FromQuery] string? providerId,
|
|
||||||
[FromQuery] string? tunerChannelId,
|
|
||||||
[FromQuery] string? providerChannelId)
|
|
||||||
{
|
{
|
||||||
return await _liveTvManager.SetChannelMapping(providerId, tunerChannelId, providerChannelId).ConfigureAwait(false);
|
return await _liveTvManager.SetChannelMapping(setChannelMappingDto.ProviderId, setChannelMappingDto.TunerChannelId, setChannelMappingDto.ProviderChannelId).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ using MediaBrowser.Model.MediaInfo;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers
|
||||||
@@ -82,6 +83,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
|
/// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
|
||||||
|
/// Query parameters are obsolete.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="itemId">The item id.</param>
|
/// <param name="itemId">The item id.</param>
|
||||||
/// <param name="userId">The user id.</param>
|
/// <param name="userId">The user id.</param>
|
||||||
@@ -105,21 +107,21 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
|
public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromQuery] Guid? userId,
|
[FromQuery, ParameterObsolete] Guid? userId,
|
||||||
[FromQuery] int? maxStreamingBitrate,
|
[FromQuery, ParameterObsolete] int? maxStreamingBitrate,
|
||||||
[FromQuery] long? startTimeTicks,
|
[FromQuery, ParameterObsolete] long? startTimeTicks,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery, ParameterObsolete] int? audioStreamIndex,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery, ParameterObsolete] int? subtitleStreamIndex,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery, ParameterObsolete] int? maxAudioChannels,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery, ParameterObsolete] string? mediaSourceId,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery, ParameterObsolete] string? liveStreamId,
|
||||||
[FromQuery] bool? autoOpenLiveStream,
|
[FromQuery, ParameterObsolete] bool? autoOpenLiveStream,
|
||||||
[FromQuery] bool? enableDirectPlay,
|
[FromQuery, ParameterObsolete] bool? enableDirectPlay,
|
||||||
[FromQuery] bool? enableDirectStream,
|
[FromQuery, ParameterObsolete] bool? enableDirectStream,
|
||||||
[FromQuery] bool? enableTranscoding,
|
[FromQuery, ParameterObsolete] bool? enableTranscoding,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery, ParameterObsolete] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery, ParameterObsolete] bool? allowAudioStreamCopy,
|
||||||
[FromBody] PlaybackInfoDto? playbackInfoDto)
|
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] PlaybackInfoDto? playbackInfoDto)
|
||||||
{
|
{
|
||||||
var authInfo = _authContext.GetAuthorizationInfo(Request);
|
var authInfo = _authContext.GetAuthorizationInfo(Request);
|
||||||
|
|
||||||
@@ -258,24 +260,24 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] Guid? itemId,
|
[FromQuery] Guid? itemId,
|
||||||
[FromBody] OpenLiveStreamDto openLiveStreamDto,
|
[FromBody] OpenLiveStreamDto? openLiveStreamDto,
|
||||||
[FromQuery] bool enableDirectPlay = true,
|
[FromQuery] bool? enableDirectPlay,
|
||||||
[FromQuery] bool enableDirectStream = true)
|
[FromQuery] bool? enableDirectStream)
|
||||||
{
|
{
|
||||||
var request = new LiveStreamRequest
|
var request = new LiveStreamRequest
|
||||||
{
|
{
|
||||||
OpenToken = openToken,
|
OpenToken = openToken ?? openLiveStreamDto?.OpenToken,
|
||||||
UserId = userId ?? Guid.Empty,
|
UserId = userId ?? openLiveStreamDto?.UserId ?? Guid.Empty,
|
||||||
PlaySessionId = playSessionId,
|
PlaySessionId = playSessionId ?? openLiveStreamDto?.PlaySessionId,
|
||||||
MaxStreamingBitrate = maxStreamingBitrate,
|
MaxStreamingBitrate = maxStreamingBitrate ?? openLiveStreamDto?.MaxStreamingBitrate,
|
||||||
StartTimeTicks = startTimeTicks,
|
StartTimeTicks = startTimeTicks ?? openLiveStreamDto?.StartTimeTicks,
|
||||||
AudioStreamIndex = audioStreamIndex,
|
AudioStreamIndex = audioStreamIndex ?? openLiveStreamDto?.AudioStreamIndex,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex ?? openLiveStreamDto?.SubtitleStreamIndex,
|
||||||
MaxAudioChannels = maxAudioChannels,
|
MaxAudioChannels = maxAudioChannels ?? openLiveStreamDto?.MaxAudioChannels,
|
||||||
ItemId = itemId ?? Guid.Empty,
|
ItemId = itemId ?? openLiveStreamDto?.ItemId ?? Guid.Empty,
|
||||||
DeviceProfile = openLiveStreamDto?.DeviceProfile,
|
DeviceProfile = openLiveStreamDto?.DeviceProfile,
|
||||||
EnableDirectPlay = enableDirectPlay,
|
EnableDirectPlay = enableDirectPlay ?? openLiveStreamDto?.EnableDirectPlay ?? true,
|
||||||
EnableDirectStream = enableDirectStream,
|
EnableDirectStream = enableDirectStream ?? openLiveStreamDto?.EnableDirectStream ?? true,
|
||||||
DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
|
DirectPlayProtocols = openLiveStreamDto?.DirectPlayProtocols ?? new[] { MediaProtocol.Http }
|
||||||
};
|
};
|
||||||
return await _mediaInfoHelper.OpenMediaSource(Request, request).ConfigureAwait(false);
|
return await _mediaInfoHelper.OpenMediaSource(Request, request).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
|
using MediaBrowser.Common.Json;
|
||||||
using MediaBrowser.Common.Updates;
|
using MediaBrowser.Common.Updates;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Model.Updates;
|
using MediaBrowser.Model.Updates;
|
||||||
@@ -99,7 +100,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
|
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
|
||||||
if (!string.IsNullOrEmpty(repositoryUrl))
|
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();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +158,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[HttpPost("Repositories")]
|
[HttpPost("Repositories")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult SetRepositories([FromBody] List<RepositoryInfo> repositoryInfos)
|
public ActionResult SetRepositories([FromBody, Required] List<RepositoryInfo> repositoryInfos)
|
||||||
{
|
{
|
||||||
_serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos;
|
_serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos;
|
||||||
_serverConfigurationManager.SaveConfiguration();
|
_serverConfigurationManager.SaveConfiguration();
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Api.Attributes;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.Extensions;
|
using Jellyfin.Api.Extensions;
|
||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
@@ -17,6 +19,7 @@ using MediaBrowser.Model.Querying;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers
|
||||||
{
|
{
|
||||||
@@ -53,6 +56,14 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new playlist.
|
/// Creates a new playlist.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
|
||||||
|
/// Query parameters are obsolete.
|
||||||
|
/// </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>
|
/// <param name="createPlaylistRequest">The create playlist payload.</param>
|
||||||
/// <returns>
|
/// <returns>
|
||||||
/// A <see cref="Task" /> that represents the asynchronous operation to create a playlist.
|
/// A <see cref="Task" /> that represents the asynchronous operation to create a playlist.
|
||||||
@@ -61,14 +72,23 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
|
public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
|
||||||
[FromBody, Required] CreatePlaylistDto createPlaylistRequest)
|
[FromQuery, ParameterObsolete] string? name,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList<Guid> ids,
|
||||||
|
[FromQuery, ParameterObsolete] Guid? userId,
|
||||||
|
[FromQuery, ParameterObsolete] 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
|
var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
|
||||||
{
|
{
|
||||||
Name = createPlaylistRequest.Name,
|
Name = name ?? createPlaylistRequest?.Name,
|
||||||
ItemIdList = createPlaylistRequest.Ids,
|
ItemIdList = ids,
|
||||||
UserId = createPlaylistRequest.UserId,
|
UserId = userId ?? createPlaylistRequest?.UserId ?? default,
|
||||||
MediaType = createPlaylistRequest.MediaType
|
MediaType = mediaType ?? createPlaylistRequest?.MediaType
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("Sessions/Playing/Ping")]
|
[HttpPost("Sessions/Playing/Ping")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult PingPlaybackSession([FromQuery] string playSessionId)
|
public ActionResult PingPlaybackSession([FromQuery, Required] string playSessionId)
|
||||||
{
|
{
|
||||||
_transcodingJobHelper.PingTranscodingJob(playSessionId, null);
|
_transcodingJobHelper.PingTranscodingJob(playSessionId, null);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
@@ -202,9 +202,9 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] PlayMethod playMethod,
|
[FromQuery] PlayMethod? playMethod,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] string playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] bool canSeek = false)
|
[FromQuery] bool canSeek = false)
|
||||||
{
|
{
|
||||||
var playbackStartInfo = new PlaybackStartInfo
|
var playbackStartInfo = new PlaybackStartInfo
|
||||||
@@ -214,7 +214,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
MediaSourceId = mediaSourceId,
|
MediaSourceId = mediaSourceId,
|
||||||
AudioStreamIndex = audioStreamIndex,
|
AudioStreamIndex = audioStreamIndex,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
PlayMethod = playMethod,
|
PlayMethod = playMethod ?? PlayMethod.Transcode,
|
||||||
PlaySessionId = playSessionId,
|
PlaySessionId = playSessionId,
|
||||||
LiveStreamId = liveStreamId
|
LiveStreamId = liveStreamId
|
||||||
};
|
};
|
||||||
@@ -254,10 +254,10 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] int? volumeLevel,
|
[FromQuery] int? volumeLevel,
|
||||||
[FromQuery] PlayMethod playMethod,
|
[FromQuery] PlayMethod? playMethod,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] string playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] RepeatMode repeatMode,
|
[FromQuery] RepeatMode? repeatMode,
|
||||||
[FromQuery] bool isPaused = false,
|
[FromQuery] bool isPaused = false,
|
||||||
[FromQuery] bool isMuted = false)
|
[FromQuery] bool isMuted = false)
|
||||||
{
|
{
|
||||||
@@ -271,10 +271,10 @@ namespace Jellyfin.Api.Controllers
|
|||||||
AudioStreamIndex = audioStreamIndex,
|
AudioStreamIndex = audioStreamIndex,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
VolumeLevel = volumeLevel,
|
VolumeLevel = volumeLevel,
|
||||||
PlayMethod = playMethod,
|
PlayMethod = playMethod ?? PlayMethod.Transcode,
|
||||||
PlaySessionId = playSessionId,
|
PlaySessionId = playSessionId,
|
||||||
LiveStreamId = liveStreamId,
|
LiveStreamId = liveStreamId,
|
||||||
RepeatMode = repeatMode
|
RepeatMode = repeatMode ?? RepeatMode.RepeatNone
|
||||||
};
|
};
|
||||||
|
|
||||||
playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
|
playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
|
||||||
@@ -352,7 +352,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
return _userDataRepository.GetUserDataDto(item, user);
|
return _userDataRepository.GetUserDataDto(item, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlayMethod ValidatePlayMethod(PlayMethod method, string playSessionId)
|
private PlayMethod ValidatePlayMethod(PlayMethod method, string? playSessionId)
|
||||||
{
|
{
|
||||||
if (method == PlayMethod.Transcode)
|
if (method == PlayMethod.Transcode)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Mime;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Api.Attributes;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.Models.PluginDtos;
|
using Jellyfin.Api.Models.PluginDtos;
|
||||||
using MediaBrowser.Common;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Json;
|
using MediaBrowser.Common.Json;
|
||||||
using MediaBrowser.Common.Plugins;
|
using MediaBrowser.Common.Plugins;
|
||||||
using MediaBrowser.Common.Updates;
|
using MediaBrowser.Common.Updates;
|
||||||
|
using MediaBrowser.Model.Configuration;
|
||||||
|
using MediaBrowser.Model.Net;
|
||||||
using MediaBrowser.Model.Plugins;
|
using MediaBrowser.Model.Plugins;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
@@ -23,112 +29,26 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
public class PluginsController : BaseJellyfinApiController
|
public class PluginsController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
private readonly IApplicationHost _appHost;
|
|
||||||
private readonly IInstallationManager _installationManager;
|
private readonly IInstallationManager _installationManager;
|
||||||
|
private readonly IPluginManager _pluginManager;
|
||||||
private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions();
|
private readonly IConfigurationManager _config;
|
||||||
|
private readonly JsonSerializerOptions _serializerOptions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="PluginsController"/> class.
|
/// Initializes a new instance of the <see cref="PluginsController"/> class.
|
||||||
/// </summary>
|
/// </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="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(
|
public PluginsController(
|
||||||
IApplicationHost appHost,
|
IInstallationManager installationManager,
|
||||||
IInstallationManager installationManager)
|
IPluginManager pluginManager,
|
||||||
|
IConfigurationManager config)
|
||||||
{
|
{
|
||||||
_appHost = appHost;
|
|
||||||
_installationManager = installationManager;
|
_installationManager = installationManager;
|
||||||
}
|
_pluginManager = pluginManager;
|
||||||
|
_serializerOptions = JsonDefaults.GetOptions();
|
||||||
/// <summary>
|
_config = config;
|
||||||
/// 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -139,7 +59,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[Obsolete("This endpoint should not be used.")]
|
[Obsolete("This endpoint should not be used.")]
|
||||||
[HttpGet("SecurityInfo")]
|
[HttpGet("SecurityInfo")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<PluginSecurityInfo> GetPluginSecurityInfo()
|
public static ActionResult<PluginSecurityInfo> GetPluginSecurityInfo()
|
||||||
{
|
{
|
||||||
return new PluginSecurityInfo
|
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>
|
/// <summary>
|
||||||
/// Gets registration status for a feature.
|
/// Gets registration status for a feature.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -172,7 +77,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[Obsolete("This endpoint should not be used.")]
|
[Obsolete("This endpoint should not be used.")]
|
||||||
[HttpPost("RegistrationRecords/{name}")]
|
[HttpPost("RegistrationRecords/{name}")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name)
|
public static ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute, Required] string name)
|
||||||
{
|
{
|
||||||
return new MBRegistrationRecord
|
return new MBRegistrationRecord
|
||||||
{
|
{
|
||||||
@@ -194,11 +99,251 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[Obsolete("Paid plugins are not supported")]
|
[Obsolete("Paid plugins are not supported")]
|
||||||
[HttpGet("Registrations/{name}")]
|
[HttpGet("Registrations/{name}")]
|
||||||
[ProducesResponseType(StatusCodes.Status501NotImplemented)]
|
[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,
|
// 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.
|
// delete all these registration endpoints. They are only kept for compatibility.
|
||||||
throw new NotImplementedException();
|
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 (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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
if (_quickConnect.State == QuickConnectState.Unavailable)
|
if (_quickConnect.State == QuickConnectState.Unavailable)
|
||||||
{
|
{
|
||||||
return Forbid("Quick connect is unavailable");
|
return StatusCode(StatusCodes.Status403Forbidden, "Quick connect is unavailable");
|
||||||
}
|
}
|
||||||
|
|
||||||
_quickConnect.Activate();
|
_quickConnect.Activate();
|
||||||
@@ -126,7 +126,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
|
var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
|
||||||
if (!userId.HasValue)
|
if (!userId.HasValue)
|
||||||
{
|
{
|
||||||
return Forbid("Unknown user id");
|
return StatusCode(StatusCodes.Status403Forbidden, "Unknown user id");
|
||||||
}
|
}
|
||||||
|
|
||||||
return _quickConnect.AuthorizeRequest(userId.Value, code);
|
return _quickConnect.AuthorizeRequest(userId.Value, code);
|
||||||
|
|||||||
@@ -259,7 +259,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
|
|
||||||
var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
|
var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
|
||||||
Directory.CreateDirectory(fullCacheDirectory);
|
Directory.CreateDirectory(fullCacheDirectory);
|
||||||
await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
|
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||||
|
await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
|
||||||
await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
|
await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||||
|
|
||||||
var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
|
var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
|
||||||
|
|||||||
@@ -371,6 +371,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="204">Subtitle uploaded.</response>
|
/// <response code="204">Subtitle uploaded.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("Videos/{itemId}/Subtitles")]
|
[HttpPost("Videos/{itemId}/Subtitles")]
|
||||||
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public async Task<ActionResult> UploadSubtitle(
|
public async Task<ActionResult> UploadSubtitle(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The sync play controller.
|
/// The sync play controller.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.SyncPlayAccess)]
|
[Authorize(Policy = Policies.SyncPlayHasAccess)]
|
||||||
public class SyncPlayController : BaseJellyfinApiController
|
public class SyncPlayController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
private readonly ISessionManager _sessionManager;
|
private readonly ISessionManager _sessionManager;
|
||||||
@@ -51,7 +51,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("New")]
|
[HttpPost("New")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[Authorize(Policy = Policies.SyncPlayCreateGroupAccess)]
|
[Authorize(Policy = Policies.SyncPlayCreateGroup)]
|
||||||
public ActionResult SyncPlayCreateGroup(
|
public ActionResult SyncPlayCreateGroup(
|
||||||
[FromBody, Required] NewGroupRequestDto requestData)
|
[FromBody, Required] NewGroupRequestDto requestData)
|
||||||
{
|
{
|
||||||
@@ -69,7 +69,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Join")]
|
[HttpPost("Join")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[Authorize(Policy = Policies.SyncPlayAccess)]
|
[Authorize(Policy = Policies.SyncPlayJoinGroup)]
|
||||||
public ActionResult SyncPlayJoinGroup(
|
public ActionResult SyncPlayJoinGroup(
|
||||||
[FromBody, Required] JoinGroupRequestDto requestData)
|
[FromBody, Required] JoinGroupRequestDto requestData)
|
||||||
{
|
{
|
||||||
@@ -86,6 +86,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Leave")]
|
[HttpPost("Leave")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlayLeaveGroup()
|
public ActionResult SyncPlayLeaveGroup()
|
||||||
{
|
{
|
||||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
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>
|
/// <returns>An <see cref="IEnumerable{GroupInfoView}"/> containing the available SyncPlay groups.</returns>
|
||||||
[HttpGet("List")]
|
[HttpGet("List")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[Authorize(Policy = Policies.SyncPlayAccess)]
|
[Authorize(Policy = Policies.SyncPlayJoinGroup)]
|
||||||
public ActionResult<IEnumerable<GroupInfoDto>> SyncPlayGetGroups()
|
public ActionResult<IEnumerable<GroupInfoDto>> SyncPlayGetGroups()
|
||||||
{
|
{
|
||||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||||
@@ -117,6 +118,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("SetNewQueue")]
|
[HttpPost("SetNewQueue")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlaySetNewQueue(
|
public ActionResult SyncPlaySetNewQueue(
|
||||||
[FromBody, Required] PlayRequestDto requestData)
|
[FromBody, Required] PlayRequestDto requestData)
|
||||||
{
|
{
|
||||||
@@ -137,6 +139,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("SetPlaylistItem")]
|
[HttpPost("SetPlaylistItem")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlaySetPlaylistItem(
|
public ActionResult SyncPlaySetPlaylistItem(
|
||||||
[FromBody, Required] SetPlaylistItemRequestDto requestData)
|
[FromBody, Required] SetPlaylistItemRequestDto requestData)
|
||||||
{
|
{
|
||||||
@@ -154,6 +157,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("RemoveFromPlaylist")]
|
[HttpPost("RemoveFromPlaylist")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlayRemoveFromPlaylist(
|
public ActionResult SyncPlayRemoveFromPlaylist(
|
||||||
[FromBody, Required] RemoveFromPlaylistRequestDto requestData)
|
[FromBody, Required] RemoveFromPlaylistRequestDto requestData)
|
||||||
{
|
{
|
||||||
@@ -171,6 +175,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("MovePlaylistItem")]
|
[HttpPost("MovePlaylistItem")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlayMovePlaylistItem(
|
public ActionResult SyncPlayMovePlaylistItem(
|
||||||
[FromBody, Required] MovePlaylistItemRequestDto requestData)
|
[FromBody, Required] MovePlaylistItemRequestDto requestData)
|
||||||
{
|
{
|
||||||
@@ -188,6 +193,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Queue")]
|
[HttpPost("Queue")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlayQueue(
|
public ActionResult SyncPlayQueue(
|
||||||
[FromBody, Required] QueueRequestDto requestData)
|
[FromBody, Required] QueueRequestDto requestData)
|
||||||
{
|
{
|
||||||
@@ -204,6 +210,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Unpause")]
|
[HttpPost("Unpause")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlayUnpause()
|
public ActionResult SyncPlayUnpause()
|
||||||
{
|
{
|
||||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||||
@@ -219,6 +226,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Pause")]
|
[HttpPost("Pause")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlayPause()
|
public ActionResult SyncPlayPause()
|
||||||
{
|
{
|
||||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||||
@@ -234,6 +242,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Stop")]
|
[HttpPost("Stop")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlayStop()
|
public ActionResult SyncPlayStop()
|
||||||
{
|
{
|
||||||
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
var currentSession = RequestHelpers.GetSession(_sessionManager, _authorizationContext, Request);
|
||||||
@@ -250,6 +259,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Seek")]
|
[HttpPost("Seek")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlaySeek(
|
public ActionResult SyncPlaySeek(
|
||||||
[FromBody, Required] SeekRequestDto requestData)
|
[FromBody, Required] SeekRequestDto requestData)
|
||||||
{
|
{
|
||||||
@@ -267,6 +277,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Buffering")]
|
[HttpPost("Buffering")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlayBuffering(
|
public ActionResult SyncPlayBuffering(
|
||||||
[FromBody, Required] BufferRequestDto requestData)
|
[FromBody, Required] BufferRequestDto requestData)
|
||||||
{
|
{
|
||||||
@@ -288,6 +299,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("Ready")]
|
[HttpPost("Ready")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlayReady(
|
public ActionResult SyncPlayReady(
|
||||||
[FromBody, Required] ReadyRequestDto requestData)
|
[FromBody, Required] ReadyRequestDto requestData)
|
||||||
{
|
{
|
||||||
@@ -309,6 +321,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("SetIgnoreWait")]
|
[HttpPost("SetIgnoreWait")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlaySetIgnoreWait(
|
public ActionResult SyncPlaySetIgnoreWait(
|
||||||
[FromBody, Required] IgnoreWaitRequestDto requestData)
|
[FromBody, Required] IgnoreWaitRequestDto requestData)
|
||||||
{
|
{
|
||||||
@@ -326,6 +339,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("NextItem")]
|
[HttpPost("NextItem")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlayNextItem(
|
public ActionResult SyncPlayNextItem(
|
||||||
[FromBody, Required] NextItemRequestDto requestData)
|
[FromBody, Required] NextItemRequestDto requestData)
|
||||||
{
|
{
|
||||||
@@ -343,6 +357,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("PreviousItem")]
|
[HttpPost("PreviousItem")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlayPreviousItem(
|
public ActionResult SyncPlayPreviousItem(
|
||||||
[FromBody, Required] PreviousItemRequestDto requestData)
|
[FromBody, Required] PreviousItemRequestDto requestData)
|
||||||
{
|
{
|
||||||
@@ -360,6 +375,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("SetRepeatMode")]
|
[HttpPost("SetRepeatMode")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlaySetRepeatMode(
|
public ActionResult SyncPlaySetRepeatMode(
|
||||||
[FromBody, Required] SetRepeatModeRequestDto requestData)
|
[FromBody, Required] SetRepeatModeRequestDto requestData)
|
||||||
{
|
{
|
||||||
@@ -377,6 +393,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
[HttpPost("SetShuffleMode")]
|
[HttpPost("SetShuffleMode")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[Authorize(Policy = Policies.SyncPlayIsInGroup)]
|
||||||
public ActionResult SyncPlaySetShuffleMode(
|
public ActionResult SyncPlaySetShuffleMode(
|
||||||
[FromBody, Required] SetShuffleModeRequestDto requestData)
|
[FromBody, Required] SetShuffleModeRequestDto requestData)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -66,7 +67,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<SystemInfo> GetSystemInfo()
|
public ActionResult<SystemInfo> GetSystemInfo()
|
||||||
{
|
{
|
||||||
return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress);
|
return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -78,7 +79,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
|
public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
|
||||||
{
|
{
|
||||||
return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress);
|
return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.ModelBinders;
|
using Jellyfin.Api.ModelBinders;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
@@ -144,7 +145,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? limit,
|
[FromQuery] int? limit,
|
||||||
[FromQuery] bool? recursive,
|
[FromQuery] bool? recursive,
|
||||||
[FromQuery] string? searchTerm,
|
[FromQuery] string? searchTerm,
|
||||||
[FromQuery] string? sortOrder,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||||
[FromQuery] Guid? parentId,
|
[FromQuery] Guid? parentId,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||||
@@ -152,7 +153,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] bool? isFavorite,
|
[FromQuery] bool? isFavorite,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
|
||||||
[FromQuery] string? sortBy,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||||
[FromQuery] bool? isPlayed,
|
[FromQuery] bool? isPlayed,
|
||||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||||
/// <param name="enableUserData">Optional. Include user data.</param>
|
/// <param name="enableUserData">Optional. Include user data.</param>
|
||||||
/// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param>
|
/// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param>
|
||||||
|
/// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param>
|
||||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
|
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
|
||||||
[HttpGet("NextUp")]
|
[HttpGet("NextUp")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
@@ -81,7 +82,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? imageTypeLimit,
|
[FromQuery] int? imageTypeLimit,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||||
[FromQuery] bool? enableUserData,
|
[FromQuery] bool? enableUserData,
|
||||||
[FromQuery] bool enableTotalRecordCount = true)
|
[FromQuery] bool enableTotalRecordCount = true,
|
||||||
|
[FromQuery] bool disableFirstEpisode = false)
|
||||||
{
|
{
|
||||||
var options = new DtoOptions { Fields = fields }
|
var options = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(Request)
|
.AddClientFields(Request)
|
||||||
@@ -95,7 +97,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
SeriesId = seriesId,
|
SeriesId = seriesId,
|
||||||
StartIndex = startIndex,
|
StartIndex = startIndex,
|
||||||
UserId = userId ?? Guid.Empty,
|
UserId = userId ?? Guid.Empty,
|
||||||
EnableTotalRecordCount = enableTotalRecordCount
|
EnableTotalRecordCount = enableTotalRecordCount,
|
||||||
|
DisableFirstEpisode = disableFirstEpisode
|
||||||
},
|
},
|
||||||
options);
|
options);
|
||||||
|
|
||||||
@@ -267,7 +270,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
if (startItemId.HasValue)
|
if (startItemId.HasValue)
|
||||||
{
|
{
|
||||||
episodes = episodes
|
episodes = episodes
|
||||||
.SkipWhile(i => startItemId.Value.Equals(i.Id))
|
.SkipWhile(i => !startItemId.Value.Equals(i.Id))
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? maxAudioSampleRate,
|
[FromQuery] int? maxAudioSampleRate,
|
||||||
[FromQuery] int? maxAudioBitDepth,
|
[FromQuery] int? maxAudioBitDepth,
|
||||||
[FromQuery] bool? enableRemoteMedia,
|
[FromQuery] bool? enableRemoteMedia,
|
||||||
[FromQuery] bool breakOnNonKeyFrames,
|
[FromQuery] bool breakOnNonKeyFrames = false,
|
||||||
[FromQuery] bool enableRedirection = true)
|
[FromQuery] bool enableRedirection = true)
|
||||||
{
|
{
|
||||||
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
|
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
|
|
||||||
if (!string.IsNullOrEmpty(password) && string.IsNullOrEmpty(pw))
|
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
|
// Password should always be null
|
||||||
@@ -267,11 +267,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult> UpdateUserPassword(
|
public async Task<ActionResult> UpdateUserPassword(
|
||||||
[FromRoute, Required] Guid userId,
|
[FromRoute, Required] Guid userId,
|
||||||
[FromBody] UpdateUserPassword request)
|
[FromBody, Required] UpdateUserPassword request)
|
||||||
{
|
{
|
||||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
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);
|
var user = _userManager.GetUserById(userId);
|
||||||
@@ -296,7 +296,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
|
|
||||||
if (success == null)
|
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);
|
await _userManager.ChangePassword(user, request.NewPw).ConfigureAwait(false);
|
||||||
@@ -325,11 +325,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public ActionResult UpdateUserEasyPassword(
|
public ActionResult UpdateUserEasyPassword(
|
||||||
[FromRoute, Required] Guid userId,
|
[FromRoute, Required] Guid userId,
|
||||||
[FromBody] UpdateUserEasyPassword request)
|
[FromBody, Required] UpdateUserEasyPassword request)
|
||||||
{
|
{
|
||||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
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);
|
var user = _userManager.GetUserById(userId);
|
||||||
@@ -367,16 +367,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public async Task<ActionResult> UpdateUser(
|
public async Task<ActionResult> UpdateUser(
|
||||||
[FromRoute, Required] Guid userId,
|
[FromRoute, Required] Guid userId,
|
||||||
[FromBody] UserDto updateUser)
|
[FromBody, Required] UserDto updateUser)
|
||||||
{
|
{
|
||||||
if (updateUser == null)
|
|
||||||
{
|
|
||||||
return BadRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
|
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);
|
var user = _userManager.GetUserById(userId);
|
||||||
@@ -407,13 +402,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public async Task<ActionResult> UpdateUserPolicy(
|
public async Task<ActionResult> UpdateUserPolicy(
|
||||||
[FromRoute, Required] Guid userId,
|
[FromRoute, Required] Guid userId,
|
||||||
[FromBody] UserPolicy newPolicy)
|
[FromBody, Required] UserPolicy newPolicy)
|
||||||
{
|
{
|
||||||
if (newPolicy == null)
|
|
||||||
{
|
|
||||||
return BadRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
var user = _userManager.GetUserById(userId);
|
var user = _userManager.GetUserById(userId);
|
||||||
|
|
||||||
// If removing admin access
|
// If removing admin access
|
||||||
@@ -421,14 +411,14 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
|
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 disabling
|
||||||
if (newPolicy.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator))
|
if (newPolicy.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator))
|
||||||
{
|
{
|
||||||
return Forbid("Administrators cannot be disabled.");
|
return StatusCode(StatusCodes.Status403Forbidden, "Administrators cannot be disabled.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// If disabling
|
// If disabling
|
||||||
@@ -436,7 +426,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1)
|
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;
|
var currentToken = _authContext.GetAuthorizationInfo(Request).Token;
|
||||||
@@ -462,11 +452,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public async Task<ActionResult> UpdateUserConfiguration(
|
public async Task<ActionResult> UpdateUserConfiguration(
|
||||||
[FromRoute, Required] Guid userId,
|
[FromRoute, Required] Guid userId,
|
||||||
[FromBody] UserConfiguration userConfig)
|
[FromBody, Required] UserConfiguration userConfig)
|
||||||
{
|
{
|
||||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, false))
|
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);
|
await _userManager.UpdateConfigurationAsync(userId, userConfig).ConfigureAwait(false);
|
||||||
@@ -483,7 +473,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[HttpPost("New")]
|
[HttpPost("New")]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[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);
|
var newUser = await _userManager.CreateUserAsync(request.Name).ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -519,14 +509,14 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Redeems a forgot password pin.
|
/// Redeems a forgot password pin.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pin">The pin.</param>
|
/// <param name="forgotPasswordPinRequest">The forgot password pin request containing the entered pin.</param>
|
||||||
/// <response code="200">Pin reset process started.</response>
|
/// <response code="200">Pin reset process started.</response>
|
||||||
/// <returns>A <see cref="Task"/> containing a <see cref="PinRedeemResult"/>.</returns>
|
/// <returns>A <see cref="Task"/> containing a <see cref="PinRedeemResult"/>.</returns>
|
||||||
[HttpPost("ForgotPassword/Pin")]
|
[HttpPost("ForgotPassword/Pin")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody] string? pin)
|
public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody, Required] ForgotPasswordPinDto forgotPasswordPinRequest)
|
||||||
{
|
{
|
||||||
var result = await _userManager.RedeemPasswordResetPin(pin).ConfigureAwait(false);
|
var result = await _userManager.RedeemPasswordResetPin(forgotPasswordPinRequest.Pin).ConfigureAwait(false);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? videoBitRate,
|
[FromQuery] int? videoBitRate,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
[FromQuery] int? maxRefFrames,
|
[FromQuery] int? maxRefFrames,
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
[FromQuery] bool? requireAvc,
|
[FromQuery] bool? requireAvc,
|
||||||
@@ -214,7 +214,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
[FromQuery] EncodingContext context,
|
[FromQuery] EncodingContext? context,
|
||||||
[FromQuery] Dictionary<string, string> streamOptions,
|
[FromQuery] Dictionary<string, string> streamOptions,
|
||||||
[FromQuery] int? maxWidth,
|
[FromQuery] int? maxWidth,
|
||||||
[FromQuery] int? maxHeight,
|
[FromQuery] int? maxHeight,
|
||||||
@@ -254,7 +254,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
Height = height,
|
Height = height,
|
||||||
VideoBitRate = videoBitRate,
|
VideoBitRate = videoBitRate,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
SubtitleMethod = subtitleMethod,
|
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||||
MaxRefFrames = maxRefFrames,
|
MaxRefFrames = maxRefFrames,
|
||||||
MaxVideoBitDepth = maxVideoBitDepth,
|
MaxVideoBitDepth = maxVideoBitDepth,
|
||||||
RequireAvc = requireAvc ?? true,
|
RequireAvc = requireAvc ?? true,
|
||||||
@@ -269,7 +269,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
TranscodeReasons = transcodeReasons,
|
TranscodeReasons = transcodeReasons,
|
||||||
AudioStreamIndex = audioStreamIndex,
|
AudioStreamIndex = audioStreamIndex,
|
||||||
VideoStreamIndex = videoStreamIndex,
|
VideoStreamIndex = videoStreamIndex,
|
||||||
Context = context,
|
Context = context ?? EncodingContext.Streaming,
|
||||||
StreamOptions = streamOptions,
|
StreamOptions = streamOptions,
|
||||||
MaxHeight = maxHeight,
|
MaxHeight = maxHeight,
|
||||||
MaxWidth = maxWidth,
|
MaxWidth = maxWidth,
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Merges videos into a single record.
|
/// Merges videos into a single record.
|
||||||
/// </summary>
|
/// </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="204">Videos merged.</response>
|
||||||
/// <response code="400">Supply at least 2 video ids.</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>
|
/// <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)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[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))
|
.Select(i => _libraryManager.GetItemById(i))
|
||||||
.OfType<Video>()
|
.OfType<Video>()
|
||||||
.OrderBy(i => i.Id)
|
.OrderBy(i => i.Id)
|
||||||
@@ -217,9 +217,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
return BadRequest("Please supply at least two videos to merge.");
|
return BadRequest("Please supply at least two videos to merge.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var videosWithVersions = items.Where(i => i.MediaSourceCount > 1).ToList();
|
var primaryVersion = items.FirstOrDefault(i => i.MediaSourceCount > 1 && string.IsNullOrEmpty(i.PrimaryVersionId));
|
||||||
|
|
||||||
var primaryVersion = videosWithVersions.FirstOrDefault();
|
|
||||||
if (primaryVersion == null)
|
if (primaryVersion == null)
|
||||||
{
|
{
|
||||||
primaryVersion = items
|
primaryVersion = items
|
||||||
@@ -364,7 +362,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? videoBitRate,
|
[FromQuery] int? videoBitRate,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
[FromQuery] int? maxRefFrames,
|
[FromQuery] int? maxRefFrames,
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
[FromQuery] bool? requireAvc,
|
[FromQuery] bool? requireAvc,
|
||||||
@@ -379,7 +377,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
[FromQuery] EncodingContext context,
|
[FromQuery] EncodingContext? context,
|
||||||
[FromQuery] Dictionary<string, string> streamOptions)
|
[FromQuery] Dictionary<string, string> streamOptions)
|
||||||
{
|
{
|
||||||
var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
|
var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
|
||||||
@@ -418,7 +416,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
Height = height,
|
Height = height,
|
||||||
VideoBitRate = videoBitRate,
|
VideoBitRate = videoBitRate,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
SubtitleMethod = subtitleMethod,
|
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||||
MaxRefFrames = maxRefFrames,
|
MaxRefFrames = maxRefFrames,
|
||||||
MaxVideoBitDepth = maxVideoBitDepth,
|
MaxVideoBitDepth = maxVideoBitDepth,
|
||||||
RequireAvc = requireAvc ?? true,
|
RequireAvc = requireAvc ?? true,
|
||||||
@@ -433,7 +431,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
TranscodeReasons = transcodeReasons,
|
TranscodeReasons = transcodeReasons,
|
||||||
AudioStreamIndex = audioStreamIndex,
|
AudioStreamIndex = audioStreamIndex,
|
||||||
VideoStreamIndex = videoStreamIndex,
|
VideoStreamIndex = videoStreamIndex,
|
||||||
Context = context,
|
Context = context ?? EncodingContext.Streaming,
|
||||||
StreamOptions = streamOptions
|
StreamOptions = streamOptions
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -620,7 +618,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? videoBitRate,
|
[FromQuery] int? videoBitRate,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
[FromQuery] int? maxRefFrames,
|
[FromQuery] int? maxRefFrames,
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
[FromQuery] bool? requireAvc,
|
[FromQuery] bool? requireAvc,
|
||||||
@@ -635,7 +633,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
[FromQuery] EncodingContext context,
|
[FromQuery] EncodingContext? context,
|
||||||
[FromQuery] Dictionary<string, string> streamOptions)
|
[FromQuery] Dictionary<string, string> streamOptions)
|
||||||
{
|
{
|
||||||
return GetVideoStream(
|
return GetVideoStream(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Jellyfin.Api.Extensions;
|
|||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
using Jellyfin.Api.ModelBinders;
|
using Jellyfin.Api.ModelBinders;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
@@ -70,13 +71,13 @@ namespace Jellyfin.Api.Controllers
|
|||||||
public ActionResult<QueryResult<BaseItemDto>> GetYears(
|
public ActionResult<QueryResult<BaseItemDto>> GetYears(
|
||||||
[FromQuery] int? startIndex,
|
[FromQuery] int? startIndex,
|
||||||
[FromQuery] int? limit,
|
[FromQuery] int? limit,
|
||||||
[FromQuery] string? sortOrder,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||||
[FromQuery] Guid? parentId,
|
[FromQuery] Guid? parentId,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||||
[FromQuery] string? sortBy,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||||
[FromQuery] bool? enableUserData,
|
[FromQuery] bool? enableUserData,
|
||||||
[FromQuery] int? imageTypeLimit,
|
[FromQuery] int? imageTypeLimit,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||||
|
|||||||
@@ -427,7 +427,7 @@ namespace Jellyfin.Api.Helpers
|
|||||||
if (framerate.HasValue)
|
if (framerate.HasValue)
|
||||||
{
|
{
|
||||||
builder.Append(",FRAME-RATE=")
|
builder.Append(",FRAME-RATE=")
|
||||||
.Append(framerate.Value);
|
.Append(framerate.Value.ToString(CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user