Merge remote-tracking branch 'upstream/master' into quickconnect
This commit is contained in:
commit
4fa3d3f4f3
|
@ -12,10 +12,12 @@ parameters:
|
|||
jobs:
|
||||
- job: CompatibilityCheck
|
||||
displayName: Compatibility Check
|
||||
dependsOn: Build
|
||||
condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
|
||||
|
||||
pool:
|
||||
vmImage: "${{ parameters.LinuxImage }}"
|
||||
# only execute for pull requests
|
||||
condition: and(succeeded(), variables['System.PullRequest.PullRequestNumber'])
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
${{ each Package in parameters.Packages }}:
|
||||
|
@ -23,7 +25,7 @@ jobs:
|
|||
NugetPackageName: ${{ Package.value.NugetPackageName }}
|
||||
AssemblyFileName: ${{ Package.value.AssemblyFileName }}
|
||||
maxParallel: 2
|
||||
dependsOn: Build
|
||||
|
||||
steps:
|
||||
- checkout: none
|
||||
|
||||
|
@ -34,32 +36,33 @@ jobs:
|
|||
version: ${{ parameters.DotNetSdkVersion }}
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Install ABI CompatibilityChecker tool'
|
||||
displayName: 'Install ABI CompatibilityChecker Tool'
|
||||
inputs:
|
||||
command: custom
|
||||
custom: tool
|
||||
arguments: 'update compatibilitychecker -g'
|
||||
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: "Download New Assembly Build Artifact"
|
||||
displayName: 'Download New Assembly Build Artifact'
|
||||
inputs:
|
||||
source: "current"
|
||||
source: 'current'
|
||||
artifact: "$(NugetPackageName)"
|
||||
path: "$(System.ArtifactsDirectory)/new-artifacts"
|
||||
runVersion: "latest"
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: "Copy New Assembly Build Artifact"
|
||||
displayName: 'Copy New Assembly Build Artifact'
|
||||
inputs:
|
||||
sourceFolder: $(System.ArtifactsDirectory)/new-artifacts
|
||||
contents: "**/*.dll"
|
||||
contents: '**/*.dll'
|
||||
targetFolder: $(System.ArtifactsDirectory)/new-release
|
||||
cleanTargetFolder: true
|
||||
overWrite: true
|
||||
flattenFolders: true
|
||||
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: "Download Reference Assembly Build Artifact"
|
||||
displayName: 'Download Reference Assembly Build Artifact'
|
||||
enabled: false
|
||||
inputs:
|
||||
source: "specific"
|
||||
artifact: "$(NugetPackageName)"
|
||||
|
@ -70,18 +73,19 @@ jobs:
|
|||
runBranch: "refs/heads/$(System.PullRequest.TargetBranch)"
|
||||
|
||||
- task: CopyFiles@2
|
||||
displayName: "Copy Reference Assembly Build Artifact"
|
||||
displayName: 'Copy Reference Assembly Build Artifact'
|
||||
enabled: false
|
||||
inputs:
|
||||
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
|
||||
contents: "**/*.dll"
|
||||
contents: '**/*.dll'
|
||||
targetFolder: $(System.ArtifactsDirectory)/current-release
|
||||
cleanTargetFolder: true
|
||||
overWrite: true
|
||||
flattenFolders: true
|
||||
|
||||
# The `--warnings-only` switch will swallow the return code and not emit any errors.
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Execute ABI Compatibility Check Tool'
|
||||
enabled: false
|
||||
inputs:
|
||||
command: custom
|
||||
custom: compat
|
||||
|
|
|
@ -80,7 +80,15 @@ jobs:
|
|||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
variables:
|
||||
- name: JellyfinVersion
|
||||
value: 0.0.0
|
||||
|
||||
steps:
|
||||
- script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
|
||||
displayName: Set release version (stable)
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
|
||||
|
||||
- task: Docker@2
|
||||
displayName: 'Push Unstable Image'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
|
@ -105,9 +113,10 @@ jobs:
|
|||
containerRegistry: Docker Hub
|
||||
tags: |
|
||||
stable-$(Build.BuildNumber)-$(BuildConfiguration)
|
||||
stable-$(BuildConfiguration)
|
||||
$(JellyfinVersion)-$(BuildConfiguration)
|
||||
|
||||
- job: CollectArtifacts
|
||||
timeoutInMinutes: 10
|
||||
displayName: 'Collect Artifacts'
|
||||
dependsOn:
|
||||
- BuildPackage
|
||||
|
@ -123,29 +132,23 @@ jobs:
|
|||
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
inputs:
|
||||
sshEndpoint: repository
|
||||
runOptions: 'inline'
|
||||
inline: |
|
||||
sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable
|
||||
rm $0
|
||||
exit
|
||||
runOptions: 'commands'
|
||||
commands: sudo -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable
|
||||
|
||||
- task: SSH@0
|
||||
displayName: 'Update Stable Repository'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
|
||||
inputs:
|
||||
sshEndpoint: repository
|
||||
runOptions: 'inline'
|
||||
inline: |
|
||||
sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)
|
||||
rm $0
|
||||
exit
|
||||
|
||||
runOptions: 'commands'
|
||||
commands: sudo -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)
|
||||
|
||||
- job: PublishNuget
|
||||
displayName: 'Publish NuGet packages'
|
||||
dependsOn:
|
||||
- BuildPackage
|
||||
condition: and(succeeded('BuildPackage'), startsWith(variables['Build.SourceBranch'], 'refs/tags'))
|
||||
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
|
|
14
.vscode/launch.json
vendored
14
.vscode/launch.json
vendored
|
@ -6,11 +6,21 @@
|
|||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
// If you have changed target frameworks, make sure to update the program path.
|
||||
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
||||
// For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false,
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
},
|
||||
{
|
||||
"name": ".NET Core Launch (nowebclient)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin.dll",
|
||||
"args": ["--nowebclient"],
|
||||
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false,
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
|
|
|
@ -1,386 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna.Main;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Emby.Dlna.Api
|
||||
{
|
||||
[Route("/Dlna/{UuId}/description.xml", "GET", Summary = "Gets dlna server info")]
|
||||
[Route("/Dlna/{UuId}/description", "GET", Summary = "Gets dlna server info")]
|
||||
public class GetDescriptionXml
|
||||
{
|
||||
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string UuId { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Dlna/{UuId}/contentdirectory/contentdirectory.xml", "GET", Summary = "Gets dlna content directory xml")]
|
||||
[Route("/Dlna/{UuId}/contentdirectory/contentdirectory", "GET", Summary = "Gets dlna content directory xml")]
|
||||
public class GetContentDirectory
|
||||
{
|
||||
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string UuId { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Dlna/{UuId}/connectionmanager/connectionmanager.xml", "GET", Summary = "Gets dlna connection manager xml")]
|
||||
[Route("/Dlna/{UuId}/connectionmanager/connectionmanager", "GET", Summary = "Gets dlna connection manager xml")]
|
||||
public class GetConnnectionManager
|
||||
{
|
||||
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string UuId { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Dlna/{UuId}/mediareceiverregistrar/mediareceiverregistrar.xml", "GET", Summary = "Gets dlna mediareceiverregistrar xml")]
|
||||
[Route("/Dlna/{UuId}/mediareceiverregistrar/mediareceiverregistrar", "GET", Summary = "Gets dlna mediareceiverregistrar xml")]
|
||||
public class GetMediaReceiverRegistrar
|
||||
{
|
||||
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string UuId { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Dlna/{UuId}/contentdirectory/control", "POST", Summary = "Processes a control request")]
|
||||
public class ProcessContentDirectoryControlRequest : IRequiresRequestStream
|
||||
{
|
||||
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string UuId { get; set; }
|
||||
|
||||
public Stream RequestStream { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Dlna/{UuId}/connectionmanager/control", "POST", Summary = "Processes a control request")]
|
||||
public class ProcessConnectionManagerControlRequest : IRequiresRequestStream
|
||||
{
|
||||
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string UuId { get; set; }
|
||||
|
||||
public Stream RequestStream { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Dlna/{UuId}/mediareceiverregistrar/control", "POST", Summary = "Processes a control request")]
|
||||
public class ProcessMediaReceiverRegistrarControlRequest : IRequiresRequestStream
|
||||
{
|
||||
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string UuId { get; set; }
|
||||
|
||||
public Stream RequestStream { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Dlna/{UuId}/mediareceiverregistrar/events", "SUBSCRIBE", Summary = "Processes an event subscription request")]
|
||||
[Route("/Dlna/{UuId}/mediareceiverregistrar/events", "UNSUBSCRIBE", Summary = "Processes an event subscription request")]
|
||||
public class ProcessMediaReceiverRegistrarEventRequest
|
||||
{
|
||||
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,UNSUBSCRIBE")]
|
||||
public string UuId { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Dlna/{UuId}/contentdirectory/events", "SUBSCRIBE", Summary = "Processes an event subscription request")]
|
||||
[Route("/Dlna/{UuId}/contentdirectory/events", "UNSUBSCRIBE", Summary = "Processes an event subscription request")]
|
||||
public class ProcessContentDirectoryEventRequest
|
||||
{
|
||||
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,UNSUBSCRIBE")]
|
||||
public string UuId { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Dlna/{UuId}/connectionmanager/events", "SUBSCRIBE", Summary = "Processes an event subscription request")]
|
||||
[Route("/Dlna/{UuId}/connectionmanager/events", "UNSUBSCRIBE", Summary = "Processes an event subscription request")]
|
||||
public class ProcessConnectionManagerEventRequest
|
||||
{
|
||||
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "path", Verb = "SUBSCRIBE,UNSUBSCRIBE")]
|
||||
public string UuId { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Dlna/{UuId}/icons/{Filename}", "GET", Summary = "Gets a server icon")]
|
||||
[Route("/Dlna/icons/{Filename}", "GET", Summary = "Gets a server icon")]
|
||||
public class GetIcon
|
||||
{
|
||||
[ApiMember(Name = "UuId", Description = "Server UuId", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")]
|
||||
public string UuId { get; set; }
|
||||
|
||||
[ApiMember(Name = "Filename", Description = "The icon filename", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string Filename { get; set; }
|
||||
}
|
||||
|
||||
public class DlnaServerService : IService
|
||||
{
|
||||
private const string XMLContentType = "text/xml; charset=UTF-8";
|
||||
|
||||
private readonly IDlnaManager _dlnaManager;
|
||||
private readonly IHttpResultFactory _resultFactory;
|
||||
private readonly IServerConfigurationManager _configurationManager;
|
||||
|
||||
public IRequest Request { get; set; }
|
||||
|
||||
private IContentDirectory ContentDirectory => DlnaEntryPoint.Current.ContentDirectory;
|
||||
|
||||
private IConnectionManager ConnectionManager => DlnaEntryPoint.Current.ConnectionManager;
|
||||
|
||||
private IMediaReceiverRegistrar MediaReceiverRegistrar => DlnaEntryPoint.Current.MediaReceiverRegistrar;
|
||||
|
||||
public DlnaServerService(
|
||||
IDlnaManager dlnaManager,
|
||||
IHttpResultFactory httpResultFactory,
|
||||
IServerConfigurationManager configurationManager,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_dlnaManager = dlnaManager;
|
||||
_resultFactory = httpResultFactory;
|
||||
_configurationManager = configurationManager;
|
||||
Request = httpContextAccessor?.HttpContext.GetServiceStackRequest() ?? throw new ArgumentNullException(nameof(httpContextAccessor));
|
||||
}
|
||||
|
||||
private string GetHeader(string name)
|
||||
{
|
||||
return Request.Headers[name];
|
||||
}
|
||||
|
||||
public object Get(GetDescriptionXml request)
|
||||
{
|
||||
var url = Request.AbsoluteUri;
|
||||
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
|
||||
var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, request.UuId, serverAddress);
|
||||
|
||||
var cacheLength = TimeSpan.FromDays(1);
|
||||
var cacheKey = Request.RawUrl.GetMD5();
|
||||
var bytes = Encoding.UTF8.GetBytes(xml);
|
||||
|
||||
return _resultFactory.GetStaticResult(Request, cacheKey, null, cacheLength, XMLContentType, () => Task.FromResult<Stream>(new MemoryStream(bytes)));
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
||||
public object Get(GetContentDirectory request)
|
||||
{
|
||||
var xml = ContentDirectory.GetServiceXml();
|
||||
|
||||
return _resultFactory.GetResult(Request, xml, XMLContentType);
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
||||
public object Get(GetMediaReceiverRegistrar request)
|
||||
{
|
||||
var xml = MediaReceiverRegistrar.GetServiceXml();
|
||||
|
||||
return _resultFactory.GetResult(Request, xml, XMLContentType);
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
||||
public object Get(GetConnnectionManager request)
|
||||
{
|
||||
var xml = ConnectionManager.GetServiceXml();
|
||||
|
||||
return _resultFactory.GetResult(Request, xml, XMLContentType);
|
||||
}
|
||||
|
||||
public async Task<object> Post(ProcessMediaReceiverRegistrarControlRequest request)
|
||||
{
|
||||
var response = await PostAsync(request.RequestStream, MediaReceiverRegistrar).ConfigureAwait(false);
|
||||
|
||||
return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
|
||||
}
|
||||
|
||||
public async Task<object> Post(ProcessContentDirectoryControlRequest request)
|
||||
{
|
||||
var response = await PostAsync(request.RequestStream, ContentDirectory).ConfigureAwait(false);
|
||||
|
||||
return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
|
||||
}
|
||||
|
||||
public async Task<object> Post(ProcessConnectionManagerControlRequest request)
|
||||
{
|
||||
var response = await PostAsync(request.RequestStream, ConnectionManager).ConfigureAwait(false);
|
||||
|
||||
return _resultFactory.GetResult(Request, response.Xml, XMLContentType);
|
||||
}
|
||||
|
||||
private Task<ControlResponse> PostAsync(Stream requestStream, IUpnpService service)
|
||||
{
|
||||
var id = GetPathValue(2).ToString();
|
||||
|
||||
return service.ProcessControlRequestAsync(new ControlRequest
|
||||
{
|
||||
Headers = Request.Headers,
|
||||
InputXml = requestStream,
|
||||
TargetServerUuId = id,
|
||||
RequestedUrl = Request.AbsoluteUri
|
||||
});
|
||||
}
|
||||
|
||||
// Copied from MediaBrowser.Api/BaseApiService.cs
|
||||
// TODO: Remove code duplication
|
||||
/// <summary>
|
||||
/// Gets the path segment at the specified index.
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the path segment.</param>
|
||||
/// <returns>The path segment at the specified index.</returns>
|
||||
/// <exception cref="IndexOutOfRangeException" >Path doesn't contain enough segments.</exception>
|
||||
/// <exception cref="InvalidDataException" >Path doesn't start with the base url.</exception>
|
||||
protected internal ReadOnlySpan<char> GetPathValue(int index)
|
||||
{
|
||||
static void ThrowIndexOutOfRangeException()
|
||||
=> throw new IndexOutOfRangeException("Path doesn't contain enough segments.");
|
||||
|
||||
static void ThrowInvalidDataException()
|
||||
=> throw new InvalidDataException("Path doesn't start with the base url.");
|
||||
|
||||
ReadOnlySpan<char> path = Request.PathInfo;
|
||||
|
||||
// Remove the protocol part from the url
|
||||
int pos = path.LastIndexOf("://");
|
||||
if (pos != -1)
|
||||
{
|
||||
path = path.Slice(pos + 3);
|
||||
}
|
||||
|
||||
// Remove the query string
|
||||
pos = path.LastIndexOf('?');
|
||||
if (pos != -1)
|
||||
{
|
||||
path = path.Slice(0, pos);
|
||||
}
|
||||
|
||||
// Remove the domain
|
||||
pos = path.IndexOf('/');
|
||||
if (pos != -1)
|
||||
{
|
||||
path = path.Slice(pos);
|
||||
}
|
||||
|
||||
// Remove base url
|
||||
string baseUrl = _configurationManager.Configuration.BaseUrl;
|
||||
int baseUrlLen = baseUrl.Length;
|
||||
if (baseUrlLen != 0)
|
||||
{
|
||||
if (path.StartsWith(baseUrl, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
path = path.Slice(baseUrlLen);
|
||||
}
|
||||
else
|
||||
{
|
||||
// The path doesn't start with the base url,
|
||||
// how did we get here?
|
||||
ThrowInvalidDataException();
|
||||
}
|
||||
}
|
||||
|
||||
// Remove leading /
|
||||
path = path.Slice(1);
|
||||
|
||||
// Backwards compatibility
|
||||
const string Emby = "emby/";
|
||||
if (path.StartsWith(Emby, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
path = path.Slice(Emby.Length);
|
||||
}
|
||||
|
||||
const string MediaBrowser = "mediabrowser/";
|
||||
if (path.StartsWith(MediaBrowser, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
path = path.Slice(MediaBrowser.Length);
|
||||
}
|
||||
|
||||
// Skip segments until we are at the right index
|
||||
for (int i = 0; i < index; i++)
|
||||
{
|
||||
pos = path.IndexOf('/');
|
||||
if (pos == -1)
|
||||
{
|
||||
ThrowIndexOutOfRangeException();
|
||||
}
|
||||
|
||||
path = path.Slice(pos + 1);
|
||||
}
|
||||
|
||||
// Remove the rest
|
||||
pos = path.IndexOf('/');
|
||||
if (pos != -1)
|
||||
{
|
||||
path = path.Slice(0, pos);
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
public object Get(GetIcon request)
|
||||
{
|
||||
var contentType = "image/" + Path.GetExtension(request.Filename)
|
||||
.TrimStart('.')
|
||||
.ToLowerInvariant();
|
||||
|
||||
var cacheLength = TimeSpan.FromDays(365);
|
||||
var cacheKey = Request.RawUrl.GetMD5();
|
||||
|
||||
return _resultFactory.GetStaticResult(Request, cacheKey, null, cacheLength, contentType, () => Task.FromResult(_dlnaManager.GetIcon(request.Filename).Stream));
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
||||
public object Subscribe(ProcessContentDirectoryEventRequest request)
|
||||
{
|
||||
return ProcessEventRequest(ContentDirectory);
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
||||
public object Subscribe(ProcessConnectionManagerEventRequest request)
|
||||
{
|
||||
return ProcessEventRequest(ConnectionManager);
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
||||
public object Subscribe(ProcessMediaReceiverRegistrarEventRequest request)
|
||||
{
|
||||
return ProcessEventRequest(MediaReceiverRegistrar);
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
||||
public object Unsubscribe(ProcessContentDirectoryEventRequest request)
|
||||
{
|
||||
return ProcessEventRequest(ContentDirectory);
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
||||
public object Unsubscribe(ProcessConnectionManagerEventRequest request)
|
||||
{
|
||||
return ProcessEventRequest(ConnectionManager);
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
||||
public object Unsubscribe(ProcessMediaReceiverRegistrarEventRequest request)
|
||||
{
|
||||
return ProcessEventRequest(MediaReceiverRegistrar);
|
||||
}
|
||||
|
||||
private object ProcessEventRequest(IEventManager eventManager)
|
||||
{
|
||||
var subscriptionId = GetHeader("SID");
|
||||
|
||||
if (string.Equals(Request.Verb, "SUBSCRIBE", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var notificationType = GetHeader("NT");
|
||||
|
||||
var callback = GetHeader("CALLBACK");
|
||||
var timeoutString = GetHeader("TIMEOUT");
|
||||
|
||||
if (string.IsNullOrEmpty(notificationType))
|
||||
{
|
||||
return GetSubscriptionResponse(eventManager.RenewEventSubscription(subscriptionId, notificationType, timeoutString, callback));
|
||||
}
|
||||
|
||||
return GetSubscriptionResponse(eventManager.CreateEventSubscription(notificationType, timeoutString, callback));
|
||||
}
|
||||
|
||||
return GetSubscriptionResponse(eventManager.CancelEventSubscription(subscriptionId));
|
||||
}
|
||||
|
||||
private object GetSubscriptionResponse(EventSubscriptionResponse response)
|
||||
{
|
||||
return _resultFactory.GetResult(Request, response.Content, response.ContentType, response.Headers);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Services;
|
||||
|
||||
namespace Emby.Dlna.Api
|
||||
{
|
||||
[Route("/Dlna/ProfileInfos", "GET", Summary = "Gets a list of profiles")]
|
||||
public class GetProfileInfos : IReturn<DeviceProfileInfo[]>
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/Dlna/Profiles/{Id}", "DELETE", Summary = "Deletes a profile")]
|
||||
public class DeleteProfile : IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "Profile Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "DELETE")]
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Dlna/Profiles/Default", "GET", Summary = "Gets the default profile")]
|
||||
public class GetDefaultProfile : IReturn<DeviceProfile>
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/Dlna/Profiles/{Id}", "GET", Summary = "Gets a single profile")]
|
||||
public class GetProfile : IReturn<DeviceProfile>
|
||||
{
|
||||
[ApiMember(Name = "Id", Description = "Profile Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string Id { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Dlna/Profiles/{Id}", "POST", Summary = "Updates a profile")]
|
||||
public class UpdateProfile : DeviceProfile, IReturnVoid
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/Dlna/Profiles", "POST", Summary = "Creates a profile")]
|
||||
public class CreateProfile : DeviceProfile, IReturnVoid
|
||||
{
|
||||
}
|
||||
|
||||
[Authenticated(Roles = "Admin")]
|
||||
public class DlnaService : IService
|
||||
{
|
||||
private readonly IDlnaManager _dlnaManager;
|
||||
|
||||
public DlnaService(IDlnaManager dlnaManager)
|
||||
{
|
||||
_dlnaManager = dlnaManager;
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
||||
public object Get(GetProfileInfos request)
|
||||
{
|
||||
return _dlnaManager.GetProfileInfos().ToArray();
|
||||
}
|
||||
|
||||
public object Get(GetProfile request)
|
||||
{
|
||||
return _dlnaManager.GetProfile(request.Id);
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
||||
public object Get(GetDefaultProfile request)
|
||||
{
|
||||
return _dlnaManager.GetDefaultProfile();
|
||||
}
|
||||
|
||||
public void Delete(DeleteProfile request)
|
||||
{
|
||||
_dlnaManager.DeleteProfile(request.Id);
|
||||
}
|
||||
|
||||
public void Post(UpdateProfile request)
|
||||
{
|
||||
_dlnaManager.UpdateProfile(request);
|
||||
}
|
||||
|
||||
public void Post(CreateProfile request)
|
||||
{
|
||||
_dlnaManager.CreateProfile(request);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ using System.Xml;
|
|||
using Emby.Dlna.Didl;
|
||||
using Emby.Dlna.Service;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
|
|
|
@ -122,15 +122,15 @@ namespace Emby.Dlna
|
|||
var builder = new StringBuilder();
|
||||
|
||||
builder.AppendLine("No matching device profile found. The default will need to be used.");
|
||||
builder.AppendLine(string.Format("DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty));
|
||||
builder.AppendLine(string.Format("FriendlyName:{0}", profile.FriendlyName ?? string.Empty));
|
||||
builder.AppendLine(string.Format("Manufacturer:{0}", profile.Manufacturer ?? string.Empty));
|
||||
builder.AppendLine(string.Format("ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty));
|
||||
builder.AppendLine(string.Format("ModelDescription:{0}", profile.ModelDescription ?? string.Empty));
|
||||
builder.AppendLine(string.Format("ModelName:{0}", profile.ModelName ?? string.Empty));
|
||||
builder.AppendLine(string.Format("ModelNumber:{0}", profile.ModelNumber ?? string.Empty));
|
||||
builder.AppendLine(string.Format("ModelUrl:{0}", profile.ModelUrl ?? string.Empty));
|
||||
builder.AppendLine(string.Format("SerialNumber:{0}", profile.SerialNumber ?? string.Empty));
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, "DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty).AppendLine();
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, "FriendlyName:{0}", profile.FriendlyName ?? string.Empty).AppendLine();
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, "Manufacturer:{0}", profile.Manufacturer ?? string.Empty).AppendLine();
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, "ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty).AppendLine();
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, "ModelDescription:{0}", profile.ModelDescription ?? string.Empty).AppendLine();
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, "ModelName:{0}", profile.ModelName ?? string.Empty).AppendLine();
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, "ModelNumber:{0}", profile.ModelNumber ?? string.Empty).AppendLine();
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, "ModelUrl:{0}", profile.ModelUrl ?? string.Empty).AppendLine();
|
||||
builder.AppendFormat(CultureInfo.InvariantCulture, "SerialNumber:{0}", profile.SerialNumber ?? string.Empty).AppendLine();
|
||||
|
||||
_logger.LogInformation(builder.ToString());
|
||||
}
|
||||
|
|
|
@ -54,11 +54,11 @@ namespace Emby.Dlna.Main
|
|||
private SsdpDevicePublisher _publisher;
|
||||
private ISsdpCommunicationsServer _communicationsServer;
|
||||
|
||||
internal IContentDirectory ContentDirectory { get; private set; }
|
||||
public IContentDirectory ContentDirectory { get; private set; }
|
||||
|
||||
internal IConnectionManager ConnectionManager { get; private set; }
|
||||
public IConnectionManager ConnectionManager { get; private set; }
|
||||
|
||||
internal IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
|
||||
public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
|
||||
|
||||
public static DlnaEntryPoint Current;
|
||||
|
||||
|
|
|
@ -1,191 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1402
|
||||
#pragma warning disable SA1649
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Notifications;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Notifications;
|
||||
using MediaBrowser.Model.Services;
|
||||
|
||||
namespace Emby.Notifications.Api
|
||||
{
|
||||
[Route("/Notifications/{UserId}", "GET", Summary = "Gets notifications")]
|
||||
public class GetNotifications : IReturn<NotificationResult>
|
||||
{
|
||||
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string UserId { get; set; } = string.Empty;
|
||||
|
||||
[ApiMember(Name = "IsRead", Description = "An optional filter by IsRead", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")]
|
||||
public bool? IsRead { get; set; }
|
||||
|
||||
[ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? StartIndex { get; set; }
|
||||
|
||||
[ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")]
|
||||
public int? Limit { get; set; }
|
||||
}
|
||||
|
||||
public class Notification
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
public string UserId { get; set; } = string.Empty;
|
||||
|
||||
public DateTime Date { get; set; }
|
||||
|
||||
public bool IsRead { get; set; }
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
public NotificationLevel Level { get; set; }
|
||||
}
|
||||
|
||||
public class NotificationResult
|
||||
{
|
||||
public IReadOnlyList<Notification> Notifications { get; set; } = Array.Empty<Notification>();
|
||||
|
||||
public int TotalRecordCount { get; set; }
|
||||
}
|
||||
|
||||
public class NotificationsSummary
|
||||
{
|
||||
public int UnreadCount { get; set; }
|
||||
|
||||
public NotificationLevel MaxUnreadNotificationLevel { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Notifications/{UserId}/Summary", "GET", Summary = "Gets a notification summary for a user")]
|
||||
public class GetNotificationsSummary : IReturn<NotificationsSummary>
|
||||
{
|
||||
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string UserId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
[Route("/Notifications/Types", "GET", Summary = "Gets notification types")]
|
||||
public class GetNotificationTypes : IReturn<List<NotificationTypeInfo>>
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/Notifications/Services", "GET", Summary = "Gets notification types")]
|
||||
public class GetNotificationServices : IReturn<List<NameIdPair>>
|
||||
{
|
||||
}
|
||||
|
||||
[Route("/Notifications/Admin", "POST", Summary = "Sends a notification to all admin users")]
|
||||
public class AddAdminNotification : IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "Name", Description = "The notification's name", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[ApiMember(Name = "Description", Description = "The notification's description", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
[ApiMember(Name = "ImageUrl", Description = "The notification's image url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string? ImageUrl { get; set; }
|
||||
|
||||
[ApiMember(Name = "Url", Description = "The notification's info url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string? Url { get; set; }
|
||||
|
||||
[ApiMember(Name = "Level", Description = "The notification level", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public NotificationLevel Level { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Notifications/{UserId}/Read", "POST", Summary = "Marks notifications as read")]
|
||||
public class MarkRead : IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
public string UserId { get; set; } = string.Empty;
|
||||
|
||||
[ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
|
||||
public string Ids { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
[Route("/Notifications/{UserId}/Unread", "POST", Summary = "Marks notifications as unread")]
|
||||
public class MarkUnread : IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
public string UserId { get; set; } = string.Empty;
|
||||
|
||||
[ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)]
|
||||
public string Ids { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
[Authenticated]
|
||||
public class NotificationsService : IService
|
||||
{
|
||||
private readonly INotificationManager _notificationManager;
|
||||
private readonly IUserManager _userManager;
|
||||
|
||||
public NotificationsService(INotificationManager notificationManager, IUserManager userManager)
|
||||
{
|
||||
_notificationManager = notificationManager;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
||||
public object Get(GetNotificationTypes request)
|
||||
{
|
||||
return _notificationManager.GetNotificationTypes();
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
||||
public object Get(GetNotificationServices request)
|
||||
{
|
||||
return _notificationManager.GetNotificationServices().ToList();
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
||||
public object Get(GetNotificationsSummary request)
|
||||
{
|
||||
return new NotificationsSummary();
|
||||
}
|
||||
|
||||
public Task Post(AddAdminNotification request)
|
||||
{
|
||||
// This endpoint really just exists as post of a real with sickbeard
|
||||
var notification = new NotificationRequest
|
||||
{
|
||||
Date = DateTime.UtcNow,
|
||||
Description = request.Description,
|
||||
Level = request.Level,
|
||||
Name = request.Name,
|
||||
Url = request.Url,
|
||||
UserIds = _userManager.Users
|
||||
.Where(user => user.HasPermission(PermissionKind.IsAdministrator))
|
||||
.Select(user => user.Id)
|
||||
.ToArray()
|
||||
};
|
||||
|
||||
return _notificationManager.SendNotification(notification, CancellationToken.None);
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
||||
public void Post(MarkRead request)
|
||||
{
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
||||
public void Post(MarkUnread request)
|
||||
{
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")]
|
||||
public object Get(GetNotifications request)
|
||||
{
|
||||
return new NotificationResult();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,7 +4,6 @@ using System;
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
|
@ -46,7 +45,7 @@ using Emby.Server.Implementations.Session;
|
|||
using Emby.Server.Implementations.SyncPlay;
|
||||
using Emby.Server.Implementations.TV;
|
||||
using Emby.Server.Implementations.Updates;
|
||||
using MediaBrowser.Api;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Events;
|
||||
|
@ -98,7 +97,6 @@ using MediaBrowser.Providers.Chapters;
|
|||
using MediaBrowser.Providers.Manager;
|
||||
using MediaBrowser.Providers.Plugins.TheTvdb;
|
||||
using MediaBrowser.Providers.Subtitles;
|
||||
using MediaBrowser.WebDashboard.Api;
|
||||
using MediaBrowser.XbmcMetadata.Providers;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
@ -556,8 +554,6 @@ namespace Emby.Server.Implementations
|
|||
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
|
||||
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IDisplayPreferencesRepository, SqliteDisplayPreferencesRepository>();
|
||||
|
||||
serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
|
||||
|
||||
serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
|
||||
|
@ -637,6 +633,8 @@ namespace Emby.Server.Implementations
|
|||
serviceCollection.AddSingleton<EncodingHelper>();
|
||||
|
||||
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
|
||||
|
||||
serviceCollection.AddSingleton<TranscodingJobHelper>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -653,7 +651,6 @@ namespace Emby.Server.Implementations
|
|||
_httpServer = Resolve<IHttpServer>();
|
||||
_httpClient = Resolve<IHttpClient>();
|
||||
|
||||
((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize();
|
||||
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
|
||||
|
||||
SetStaticProperties();
|
||||
|
@ -1037,12 +1034,6 @@ namespace Emby.Server.Implementations
|
|||
}
|
||||
}
|
||||
|
||||
// Include composable parts in the Api assembly
|
||||
yield return typeof(ApiEntryPoint).Assembly;
|
||||
|
||||
// Include composable parts in the Dashboard assembly
|
||||
yield return typeof(DashboardService).Assembly;
|
||||
|
||||
// Include composable parts in the Model assembly
|
||||
yield return typeof(SystemInfo).Assembly;
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
using System;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Browser
|
||||
|
@ -24,7 +26,7 @@ namespace Emby.Server.Implementations.Browser
|
|||
/// <param name="appHost">The app host.</param>
|
||||
public static void OpenSwaggerPage(IServerApplicationHost appHost)
|
||||
{
|
||||
TryOpenUrl(appHost, "/swagger/index.html");
|
||||
TryOpenUrl(appHost, "/api-docs/swagger");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
@ -7,6 +6,7 @@ using System.Linq;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Progress;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
|
@ -22,6 +22,7 @@ using MediaBrowser.Model.Entities;
|
|||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||
using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
|
||||
|
@ -45,10 +46,7 @@ namespace Emby.Server.Implementations.Channels
|
|||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IProviderManager _providerManager;
|
||||
|
||||
private readonly ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>> _channelItemMediaInfo =
|
||||
new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>();
|
||||
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
|
||||
|
||||
/// <summary>
|
||||
|
@ -63,6 +61,7 @@ namespace Emby.Server.Implementations.Channels
|
|||
/// <param name="userDataManager">The user data manager.</param>
|
||||
/// <param name="jsonSerializer">The JSON serializer.</param>
|
||||
/// <param name="providerManager">The provider manager.</param>
|
||||
/// <param name="memoryCache">The memory cache.</param>
|
||||
public ChannelManager(
|
||||
IUserManager userManager,
|
||||
IDtoService dtoService,
|
||||
|
@ -72,7 +71,8 @@ namespace Emby.Server.Implementations.Channels
|
|||
IFileSystem fileSystem,
|
||||
IUserDataManager userDataManager,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IProviderManager providerManager)
|
||||
IProviderManager providerManager,
|
||||
IMemoryCache memoryCache)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_dtoService = dtoService;
|
||||
|
@ -83,6 +83,7 @@ namespace Emby.Server.Implementations.Channels
|
|||
_userDataManager = userDataManager;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_providerManager = providerManager;
|
||||
_memoryCache = memoryCache;
|
||||
}
|
||||
|
||||
internal IChannel[] Channels { get; private set; }
|
||||
|
@ -417,20 +418,15 @@ namespace Emby.Server.Implementations.Channels
|
|||
|
||||
private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_channelItemMediaInfo.TryGetValue(id, out Tuple<DateTime, List<MediaSourceInfo>> cachedInfo))
|
||||
if (_memoryCache.TryGetValue(id, out List<MediaSourceInfo> cachedInfo))
|
||||
{
|
||||
if ((DateTime.UtcNow - cachedInfo.Item1).TotalMinutes < 5)
|
||||
{
|
||||
return cachedInfo.Item2;
|
||||
}
|
||||
return cachedInfo;
|
||||
}
|
||||
|
||||
var mediaInfo = await channel.GetChannelItemMediaInfo(id, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var list = mediaInfo.ToList();
|
||||
|
||||
var item2 = new Tuple<DateTime, List<MediaSourceInfo>>(DateTime.UtcNow, list);
|
||||
_channelItemMediaInfo.AddOrUpdate(id, item2, (key, oldValue) => item2);
|
||||
_memoryCache.Set(id, list, DateTimeOffset.UtcNow.AddMinutes(5));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
|
|
@ -1,225 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Json;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SQLitePCL.pretty;
|
||||
|
||||
namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// Class SQLiteDisplayPreferencesRepository.
|
||||
/// </summary>
|
||||
public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
|
||||
public SqliteDisplayPreferencesRepository(ILogger<SqliteDisplayPreferencesRepository> logger, IApplicationPaths appPaths, IFileSystem fileSystem)
|
||||
: base(logger)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
|
||||
_jsonOptions = JsonDefaults.GetOptions();
|
||||
|
||||
DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the repository.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name => "SQLite";
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
try
|
||||
{
|
||||
InitializeInternal();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error loading database file. Will reset and retry.");
|
||||
|
||||
_fileSystem.DeleteFile(DbFilePath);
|
||||
|
||||
InitializeInternal();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the connection to the database.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
private void InitializeInternal()
|
||||
{
|
||||
string[] queries =
|
||||
{
|
||||
"create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)",
|
||||
"create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)"
|
||||
};
|
||||
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.RunQueries(queries);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save the display preferences associated with an item in the repo.
|
||||
/// </summary>
|
||||
/// <param name="displayPreferences">The display preferences.</param>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <exception cref="ArgumentNullException">item</exception>
|
||||
public void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, CancellationToken cancellationToken)
|
||||
{
|
||||
if (displayPreferences == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(displayPreferences));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(displayPreferences.Id))
|
||||
{
|
||||
throw new ArgumentException("Display preferences has an invalid Id", nameof(displayPreferences));
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.RunInTransaction(
|
||||
db => SaveDisplayPreferences(displayPreferences, userId, client, db),
|
||||
TransactionMode);
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, IDatabaseConnection connection)
|
||||
{
|
||||
var serialized = JsonSerializer.SerializeToUtf8Bytes(displayPreferences, _jsonOptions);
|
||||
|
||||
using (var statement = connection.PrepareStatement("replace into userdisplaypreferences (id, userid, client, data) values (@id, @userId, @client, @data)"))
|
||||
{
|
||||
statement.TryBind("@id", new Guid(displayPreferences.Id).ToByteArray());
|
||||
statement.TryBind("@userId", userId.ToByteArray());
|
||||
statement.TryBind("@client", client);
|
||||
statement.TryBind("@data", serialized);
|
||||
|
||||
statement.MoveNext();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save all display preferences associated with a user in the repo.
|
||||
/// </summary>
|
||||
/// <param name="displayPreferences">The display preferences.</param>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <exception cref="ArgumentNullException">item</exception>
|
||||
public void SaveAllDisplayPreferences(IEnumerable<DisplayPreferences> displayPreferences, Guid userId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (displayPreferences == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(displayPreferences));
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using (var connection = GetConnection())
|
||||
{
|
||||
connection.RunInTransaction(
|
||||
db =>
|
||||
{
|
||||
foreach (var displayPreference in displayPreferences)
|
||||
{
|
||||
SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db);
|
||||
}
|
||||
},
|
||||
TransactionMode);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display preferences.
|
||||
/// </summary>
|
||||
/// <param name="displayPreferencesId">The display preferences id.</param>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <param name="client">The client.</param>
|
||||
/// <returns>Task{DisplayPreferences}.</returns>
|
||||
/// <exception cref="ArgumentNullException">item</exception>
|
||||
public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, Guid userId, string client)
|
||||
{
|
||||
if (string.IsNullOrEmpty(displayPreferencesId))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(displayPreferencesId));
|
||||
}
|
||||
|
||||
var guidId = displayPreferencesId.GetMD5();
|
||||
|
||||
using (var connection = GetConnection(true))
|
||||
{
|
||||
using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where id = @id and userId=@userId and client=@client"))
|
||||
{
|
||||
statement.TryBind("@id", guidId.ToByteArray());
|
||||
statement.TryBind("@userId", userId.ToByteArray());
|
||||
statement.TryBind("@client", client);
|
||||
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
return Get(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new DisplayPreferences
|
||||
{
|
||||
Id = guidId.ToString("N", CultureInfo.InvariantCulture)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all display preferences for the given user.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <returns>Task{DisplayPreferences}.</returns>
|
||||
/// <exception cref="ArgumentNullException">item</exception>
|
||||
public IEnumerable<DisplayPreferences> GetAllDisplayPreferences(Guid userId)
|
||||
{
|
||||
var list = new List<DisplayPreferences>();
|
||||
|
||||
using (var connection = GetConnection(true))
|
||||
using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId"))
|
||||
{
|
||||
statement.TryBind("@userId", userId.ToByteArray());
|
||||
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
list.Add(Get(row));
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private DisplayPreferences Get(IReadOnlyList<IResultSetValue> row)
|
||||
=> JsonSerializer.Deserialize<DisplayPreferences>(row[0].ToBlob(), _jsonOptions);
|
||||
|
||||
public void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, CancellationToken cancellationToken)
|
||||
=> SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken);
|
||||
|
||||
public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client)
|
||||
=> GetDisplayPreferences(displayPreferencesId, new Guid(userId), client);
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ using System.Text;
|
|||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using Emby.Server.Implementations.Playlists;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Json;
|
||||
using MediaBrowser.Controller;
|
||||
|
@ -400,6 +401,8 @@ namespace Emby.Server.Implementations.Data
|
|||
"OwnerId"
|
||||
};
|
||||
|
||||
private static readonly string _retriveItemColumnsSelectQuery = $"select {string.Join(',', _retriveItemColumns)} from TypedBaseItems where guid = @guid";
|
||||
|
||||
private static readonly string[] _mediaStreamSaveColumns =
|
||||
{
|
||||
"ItemId",
|
||||
|
@ -439,6 +442,12 @@ namespace Emby.Server.Implementations.Data
|
|||
"ColorTransfer"
|
||||
};
|
||||
|
||||
private static readonly string _mediaStreamSaveColumnsInsertQuery =
|
||||
$"insert into mediastreams ({string.Join(',', _mediaStreamSaveColumns)}) values ";
|
||||
|
||||
private static readonly string _mediaStreamSaveColumnsSelectQuery =
|
||||
$"select {string.Join(',', _mediaStreamSaveColumns)} from mediastreams where ItemId=@ItemId";
|
||||
|
||||
private static readonly string[] _mediaAttachmentSaveColumns =
|
||||
{
|
||||
"ItemId",
|
||||
|
@ -450,102 +459,15 @@ namespace Emby.Server.Implementations.Data
|
|||
"MIMEType"
|
||||
};
|
||||
|
||||
private static readonly string _mediaAttachmentSaveColumnsSelectQuery =
|
||||
$"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId";
|
||||
|
||||
private static readonly string _mediaAttachmentInsertPrefix;
|
||||
|
||||
private static string GetSaveItemCommandText()
|
||||
{
|
||||
var saveColumns = new[]
|
||||
{
|
||||
"guid",
|
||||
"type",
|
||||
"data",
|
||||
"Path",
|
||||
"StartDate",
|
||||
"EndDate",
|
||||
"ChannelId",
|
||||
"IsMovie",
|
||||
"IsSeries",
|
||||
"EpisodeTitle",
|
||||
"IsRepeat",
|
||||
"CommunityRating",
|
||||
"CustomRating",
|
||||
"IndexNumber",
|
||||
"IsLocked",
|
||||
"Name",
|
||||
"OfficialRating",
|
||||
"MediaType",
|
||||
"Overview",
|
||||
"ParentIndexNumber",
|
||||
"PremiereDate",
|
||||
"ProductionYear",
|
||||
"ParentId",
|
||||
"Genres",
|
||||
"InheritedParentalRatingValue",
|
||||
"SortName",
|
||||
"ForcedSortName",
|
||||
"RunTimeTicks",
|
||||
"Size",
|
||||
"DateCreated",
|
||||
"DateModified",
|
||||
"PreferredMetadataLanguage",
|
||||
"PreferredMetadataCountryCode",
|
||||
"Width",
|
||||
"Height",
|
||||
"DateLastRefreshed",
|
||||
"DateLastSaved",
|
||||
"IsInMixedFolder",
|
||||
"LockedFields",
|
||||
"Studios",
|
||||
"Audio",
|
||||
"ExternalServiceId",
|
||||
"Tags",
|
||||
"IsFolder",
|
||||
"UnratedType",
|
||||
"TopParentId",
|
||||
"TrailerTypes",
|
||||
"CriticRating",
|
||||
"CleanName",
|
||||
"PresentationUniqueKey",
|
||||
"OriginalTitle",
|
||||
"PrimaryVersionId",
|
||||
"DateLastMediaAdded",
|
||||
"Album",
|
||||
"IsVirtualItem",
|
||||
"SeriesName",
|
||||
"UserDataKey",
|
||||
"SeasonName",
|
||||
"SeasonId",
|
||||
"SeriesId",
|
||||
"ExternalSeriesId",
|
||||
"Tagline",
|
||||
"ProviderIds",
|
||||
"Images",
|
||||
"ProductionLocations",
|
||||
"ExtraIds",
|
||||
"TotalBitrate",
|
||||
"ExtraType",
|
||||
"Artists",
|
||||
"AlbumArtists",
|
||||
"ExternalId",
|
||||
"SeriesPresentationUniqueKey",
|
||||
"ShowId",
|
||||
"OwnerId"
|
||||
};
|
||||
|
||||
var saveItemCommandCommandText = "replace into TypedBaseItems (" + string.Join(",", saveColumns) + ") values (";
|
||||
|
||||
for (var i = 0; i < saveColumns.Length; i++)
|
||||
{
|
||||
if (i != 0)
|
||||
{
|
||||
saveItemCommandCommandText += ",";
|
||||
}
|
||||
|
||||
saveItemCommandCommandText += "@" + saveColumns[i];
|
||||
}
|
||||
|
||||
return saveItemCommandCommandText + ")";
|
||||
}
|
||||
private const string SaveItemCommandText =
|
||||
@"replace into TypedBaseItems
|
||||
(guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
|
||||
values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
|
||||
|
||||
/// <summary>
|
||||
/// Save a standard item in the repo.
|
||||
|
@ -636,7 +558,7 @@ namespace Emby.Server.Implementations.Data
|
|||
{
|
||||
var statements = PrepareAll(db, new string[]
|
||||
{
|
||||
GetSaveItemCommandText(),
|
||||
SaveItemCommandText,
|
||||
"delete from AncestorIds where ItemId=@ItemId"
|
||||
}).ToList();
|
||||
|
||||
|
@ -1056,7 +978,10 @@ namespace Emby.Server.Implementations.Data
|
|||
continue;
|
||||
}
|
||||
|
||||
str.Append($"{i.Key}={i.Value}|");
|
||||
str.Append(i.Key)
|
||||
.Append('=')
|
||||
.Append(i.Value)
|
||||
.Append('|');
|
||||
}
|
||||
|
||||
if (str.Length == 0)
|
||||
|
@ -1110,8 +1035,8 @@ namespace Emby.Server.Implementations.Data
|
|||
continue;
|
||||
}
|
||||
|
||||
str.Append(ToValueString(i))
|
||||
.Append('|');
|
||||
AppendItemImageInfo(str, i);
|
||||
str.Append('|');
|
||||
}
|
||||
|
||||
str.Length -= 1; // Remove last |
|
||||
|
@ -1145,26 +1070,26 @@ namespace Emby.Server.Implementations.Data
|
|||
item.ImageInfos = list.ToArray();
|
||||
}
|
||||
|
||||
public string ToValueString(ItemImageInfo image)
|
||||
public void AppendItemImageInfo(StringBuilder bldr, ItemImageInfo image)
|
||||
{
|
||||
const string Delimeter = "*";
|
||||
const char Delimiter = '*';
|
||||
|
||||
var path = image.Path ?? string.Empty;
|
||||
var hash = image.BlurHash ?? string.Empty;
|
||||
|
||||
return GetPathToSave(path) +
|
||||
Delimeter +
|
||||
image.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) +
|
||||
Delimeter +
|
||||
image.Type +
|
||||
Delimeter +
|
||||
image.Width.ToString(CultureInfo.InvariantCulture) +
|
||||
Delimeter +
|
||||
image.Height.ToString(CultureInfo.InvariantCulture) +
|
||||
Delimeter +
|
||||
// Replace delimiters with other characters.
|
||||
// This can be removed when we migrate to a proper DB.
|
||||
hash.Replace('*', '/').Replace('|', '\\');
|
||||
bldr.Append(GetPathToSave(path))
|
||||
.Append(Delimiter)
|
||||
.Append(image.DateModified.Ticks)
|
||||
.Append(Delimiter)
|
||||
.Append(image.Type)
|
||||
.Append(Delimiter)
|
||||
.Append(image.Width)
|
||||
.Append(Delimiter)
|
||||
.Append(image.Height)
|
||||
.Append(Delimiter)
|
||||
// Replace delimiters with other characters.
|
||||
// This can be removed when we migrate to a proper DB.
|
||||
.Append(hash.Replace('*', '/').Replace('|', '\\'));
|
||||
}
|
||||
|
||||
public ItemImageInfo ItemImageInfoFromValueString(string value)
|
||||
|
@ -1226,7 +1151,7 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
using (var connection = GetConnection(true))
|
||||
{
|
||||
using (var statement = PrepareStatement(connection, "select " + string.Join(",", _retriveItemColumns) + " from TypedBaseItems where guid = @guid"))
|
||||
using (var statement = PrepareStatement(connection, _retriveItemColumnsSelectQuery))
|
||||
{
|
||||
statement.TryBind("@guid", id);
|
||||
|
||||
|
@ -2776,82 +2701,82 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
private string FixUnicodeChars(string buffer)
|
||||
{
|
||||
if (buffer.IndexOf('\u2013') > -1)
|
||||
if (buffer.IndexOf('\u2013', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2013', '-'); // en dash
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2014') > -1)
|
||||
if (buffer.IndexOf('\u2014', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2014', '-'); // em dash
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2015') > -1)
|
||||
if (buffer.IndexOf('\u2015', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2015', '-'); // horizontal bar
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2017') > -1)
|
||||
if (buffer.IndexOf('\u2017', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2017', '_'); // double low line
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2018') > -1)
|
||||
if (buffer.IndexOf('\u2018', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2018', '\''); // left single quotation mark
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2019') > -1)
|
||||
if (buffer.IndexOf('\u2019', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2019', '\''); // right single quotation mark
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u201a') > -1)
|
||||
if (buffer.IndexOf('\u201a', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u201b') > -1)
|
||||
if (buffer.IndexOf('\u201b', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u201c') > -1)
|
||||
if (buffer.IndexOf('\u201c', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u201d') > -1)
|
||||
if (buffer.IndexOf('\u201d', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u201e') > -1)
|
||||
if (buffer.IndexOf('\u201e', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2026') > -1)
|
||||
if (buffer.IndexOf('\u2026', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace("\u2026", "..."); // horizontal ellipsis
|
||||
buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2032') > -1)
|
||||
if (buffer.IndexOf('\u2032', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2032', '\''); // prime
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u2033') > -1)
|
||||
if (buffer.IndexOf('\u2033', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u2033', '\"'); // double prime
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u0060') > -1)
|
||||
if (buffer.IndexOf('\u0060', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u0060', '\''); // grave accent
|
||||
}
|
||||
|
||||
if (buffer.IndexOf('\u00B4') > -1)
|
||||
if (buffer.IndexOf('\u00B4', StringComparison.Ordinal) > -1)
|
||||
{
|
||||
buffer = buffer.Replace('\u00B4', '\''); // acute accent
|
||||
}
|
||||
|
@ -3000,7 +2925,6 @@ namespace Emby.Server.Implementations.Data
|
|||
{
|
||||
connection.RunInTransaction(db =>
|
||||
{
|
||||
|
||||
var statements = PrepareAll(db, statementTexts).ToList();
|
||||
|
||||
if (!isReturningZeroItems)
|
||||
|
@ -4670,8 +4594,12 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
if (query.BlockUnratedItems.Length > 1)
|
||||
{
|
||||
var inClause = string.Join(",", query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'"));
|
||||
whereClauses.Add(string.Format("(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))", inClause));
|
||||
var inClause = string.Join(',', query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'"));
|
||||
whereClauses.Add(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))",
|
||||
inClause));
|
||||
}
|
||||
|
||||
if (query.ExcludeInheritedTags.Length > 0)
|
||||
|
@ -4680,7 +4608,7 @@ namespace Emby.Server.Implementations.Data
|
|||
if (statement == null)
|
||||
{
|
||||
int index = 0;
|
||||
string excludedTags = string.Join(",", query.ExcludeInheritedTags.Select(t => paramName + index++));
|
||||
string excludedTags = string.Join(',', query.ExcludeInheritedTags.Select(t => paramName + index++));
|
||||
whereClauses.Add("((select CleanValue from itemvalues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)");
|
||||
}
|
||||
else
|
||||
|
@ -5734,10 +5662,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
const int Limit = 100;
|
||||
var startIndex = 0;
|
||||
|
||||
const string StartInsertText = "insert into ItemValues (ItemId, Type, Value, CleanValue) values ";
|
||||
var insertText = new StringBuilder(StartInsertText);
|
||||
while (startIndex < values.Count)
|
||||
{
|
||||
var insertText = new StringBuilder("insert into ItemValues (ItemId, Type, Value, CleanValue) values ");
|
||||
|
||||
var endIndex = Math.Min(values.Count, startIndex + Limit);
|
||||
|
||||
for (var i = startIndex; i < endIndex; i++)
|
||||
|
@ -5779,6 +5707,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
}
|
||||
|
||||
startIndex += Limit;
|
||||
insertText.Length = StartInsertText.Length;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5816,10 +5745,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
var startIndex = 0;
|
||||
var listIndex = 0;
|
||||
|
||||
const string StartInsertText = "insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values ";
|
||||
var insertText = new StringBuilder(StartInsertText);
|
||||
while (startIndex < people.Count)
|
||||
{
|
||||
var insertText = new StringBuilder("insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values ");
|
||||
|
||||
var endIndex = Math.Min(people.Count, startIndex + Limit);
|
||||
for (var i = startIndex; i < endIndex; i++)
|
||||
{
|
||||
|
@ -5853,6 +5782,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
}
|
||||
|
||||
startIndex += Limit;
|
||||
insertText.Length = StartInsertText.Length;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5891,10 +5821,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
throw new ArgumentNullException(nameof(query));
|
||||
}
|
||||
|
||||
var cmdText = "select "
|
||||
+ string.Join(",", _mediaStreamSaveColumns)
|
||||
+ " from mediastreams where"
|
||||
+ " ItemId=@ItemId";
|
||||
var cmdText = _mediaStreamSaveColumnsSelectQuery;
|
||||
|
||||
if (query.Type.HasValue)
|
||||
{
|
||||
|
@ -5971,18 +5898,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
const int Limit = 10;
|
||||
var startIndex = 0;
|
||||
|
||||
var insertText = new StringBuilder(_mediaStreamSaveColumnsInsertQuery);
|
||||
while (startIndex < streams.Count)
|
||||
{
|
||||
var insertText = new StringBuilder("insert into mediastreams (");
|
||||
foreach (var column in _mediaStreamSaveColumns)
|
||||
{
|
||||
insertText.Append(column).Append(',');
|
||||
}
|
||||
|
||||
// Remove last comma
|
||||
insertText.Length--;
|
||||
insertText.Append(") values ");
|
||||
|
||||
var endIndex = Math.Min(streams.Count, startIndex + Limit);
|
||||
|
||||
for (var i = startIndex; i < endIndex; i++)
|
||||
|
@ -6065,6 +5983,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
}
|
||||
|
||||
startIndex += Limit;
|
||||
insertText.Length = _mediaStreamSaveColumnsInsertQuery.Length;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6248,10 +6167,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
throw new ArgumentNullException(nameof(query));
|
||||
}
|
||||
|
||||
var cmdText = "select "
|
||||
+ string.Join(",", _mediaAttachmentSaveColumns)
|
||||
+ " from mediaattachments where"
|
||||
+ " ItemId=@ItemId";
|
||||
var cmdText = _mediaAttachmentSaveColumnsSelectQuery;
|
||||
|
||||
if (query.Index.HasValue)
|
||||
{
|
||||
|
@ -6319,10 +6235,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
{
|
||||
const int InsertAtOnce = 10;
|
||||
|
||||
var insertText = new StringBuilder(_mediaAttachmentInsertPrefix);
|
||||
for (var startIndex = 0; startIndex < attachments.Count; startIndex += InsertAtOnce)
|
||||
{
|
||||
var insertText = new StringBuilder(_mediaAttachmentInsertPrefix);
|
||||
|
||||
var endIndex = Math.Min(attachments.Count, startIndex + InsertAtOnce);
|
||||
|
||||
for (var i = startIndex; i < endIndex; i++)
|
||||
|
@ -6368,6 +6283,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
statement.Reset();
|
||||
statement.MoveNext();
|
||||
}
|
||||
|
||||
insertText.Length = _mediaAttachmentInsertPrefix.Length;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@ using System.Collections.Generic;
|
|||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
|
@ -17,16 +17,17 @@ using MediaBrowser.Model.Events;
|
|||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace Emby.Server.Implementations.Devices
|
||||
{
|
||||
public class DeviceManager : IDeviceManager
|
||||
{
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly IJsonSerializer _json;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IAuthenticationRepository _authRepo;
|
||||
private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache;
|
||||
private readonly object _capabilitiesSyncLock = new object();
|
||||
|
||||
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
|
||||
|
@ -35,13 +36,14 @@ namespace Emby.Server.Implementations.Devices
|
|||
IAuthenticationRepository authRepo,
|
||||
IJsonSerializer json,
|
||||
IUserManager userManager,
|
||||
IServerConfigurationManager config)
|
||||
IServerConfigurationManager config,
|
||||
IMemoryCache memoryCache)
|
||||
{
|
||||
_json = json;
|
||||
_userManager = userManager;
|
||||
_config = config;
|
||||
_memoryCache = memoryCache;
|
||||
_authRepo = authRepo;
|
||||
_capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
|
||||
|
@ -51,8 +53,7 @@ namespace Emby.Server.Implementations.Devices
|
|||
|
||||
lock (_capabilitiesSyncLock)
|
||||
{
|
||||
_capabilitiesCache[deviceId] = capabilities;
|
||||
|
||||
_memoryCache.Set(deviceId, capabilities);
|
||||
_json.SerializeToFile(capabilities, path);
|
||||
}
|
||||
}
|
||||
|
@ -71,13 +72,13 @@ namespace Emby.Server.Implementations.Devices
|
|||
|
||||
public ClientCapabilities GetCapabilities(string id)
|
||||
{
|
||||
if (_memoryCache.TryGetValue(id, out ClientCapabilities result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
lock (_capabilitiesSyncLock)
|
||||
{
|
||||
if (_capabilitiesCache.TryGetValue(id, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var path = Path.Combine(GetDevicePath(id), "capabilities.json");
|
||||
try
|
||||
{
|
||||
|
|
|
@ -13,10 +13,8 @@
|
|||
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj" />
|
||||
<ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Api\MediaBrowser.Api.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj" />
|
||||
<ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" />
|
||||
<ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" />
|
||||
|
@ -25,7 +23,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="IPNetwork2" Version="2.5.211" />
|
||||
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.0-pre1" />
|
||||
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.2.0" />
|
||||
|
@ -38,10 +36,10 @@
|
|||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" />
|
||||
<PackageReference Include="Mono.Nat" Version="2.0.1" />
|
||||
<PackageReference Include="Mono.Nat" Version="2.0.2" />
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
|
||||
<PackageReference Include="ServiceStack.Text.Core" Version="5.9.0" />
|
||||
<PackageReference Include="sharpcompress" Version="0.25.1" />
|
||||
<PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" />
|
||||
<PackageReference Include="sharpcompress" Version="0.26.0" />
|
||||
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
|
||||
<PackageReference Include="DotNet.Glob" Version="3.0.9" />
|
||||
</ItemGroup>
|
||||
|
@ -54,7 +52,7 @@
|
|||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors>
|
||||
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
|
|
|
@ -23,10 +23,12 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
public class LibraryChangedNotifier : IServerEntryPoint
|
||||
{
|
||||
/// <summary>
|
||||
/// The library manager.
|
||||
/// The library update duration.
|
||||
/// </summary>
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private const int LibraryUpdateDuration = 30000;
|
||||
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ILogger<LibraryChangedNotifier> _logger;
|
||||
|
@ -38,23 +40,10 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
|
||||
private readonly List<Folder> _foldersAddedTo = new List<Folder>();
|
||||
private readonly List<Folder> _foldersRemovedFrom = new List<Folder>();
|
||||
|
||||
private readonly List<BaseItem> _itemsAdded = new List<BaseItem>();
|
||||
private readonly List<BaseItem> _itemsRemoved = new List<BaseItem>();
|
||||
private readonly List<BaseItem> _itemsUpdated = new List<BaseItem>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the library update timer.
|
||||
/// </summary>
|
||||
/// <value>The library update timer.</value>
|
||||
private Timer LibraryUpdateTimer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The library update duration.
|
||||
/// </summary>
|
||||
private const int LibraryUpdateDuration = 30000;
|
||||
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly Dictionary<Guid, DateTime> _lastProgressMessageTimes = new Dictionary<Guid, DateTime>();
|
||||
|
||||
public LibraryChangedNotifier(
|
||||
ILibraryManager libraryManager,
|
||||
|
@ -70,22 +59,26 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
_providerManager = providerManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the library update timer.
|
||||
/// </summary>
|
||||
/// <value>The library update timer.</value>
|
||||
private Timer LibraryUpdateTimer { get; set; }
|
||||
|
||||
public Task RunAsync()
|
||||
{
|
||||
_libraryManager.ItemAdded += libraryManager_ItemAdded;
|
||||
_libraryManager.ItemUpdated += libraryManager_ItemUpdated;
|
||||
_libraryManager.ItemRemoved += libraryManager_ItemRemoved;
|
||||
_libraryManager.ItemAdded += OnLibraryItemAdded;
|
||||
_libraryManager.ItemUpdated += OnLibraryItemUpdated;
|
||||
_libraryManager.ItemRemoved += OnLibraryItemRemoved;
|
||||
|
||||
_providerManager.RefreshCompleted += _providerManager_RefreshCompleted;
|
||||
_providerManager.RefreshStarted += _providerManager_RefreshStarted;
|
||||
_providerManager.RefreshProgress += _providerManager_RefreshProgress;
|
||||
_providerManager.RefreshCompleted += OnProviderRefreshCompleted;
|
||||
_providerManager.RefreshStarted += OnProviderRefreshStarted;
|
||||
_providerManager.RefreshProgress += OnProviderRefreshProgress;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Dictionary<Guid, DateTime> _lastProgressMessageTimes = new Dictionary<Guid, DateTime>();
|
||||
|
||||
private void _providerManager_RefreshProgress(object sender, GenericEventArgs<Tuple<BaseItem, double>> e)
|
||||
private void OnProviderRefreshProgress(object sender, GenericEventArgs<Tuple<BaseItem, double>> e)
|
||||
{
|
||||
var item = e.Argument.Item1;
|
||||
|
||||
|
@ -122,9 +115,11 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
|
||||
foreach (var collectionFolder in collectionFolders)
|
||||
{
|
||||
var collectionFolderDict = new Dictionary<string, string>();
|
||||
collectionFolderDict["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
collectionFolderDict["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture);
|
||||
var collectionFolderDict = new Dictionary<string, string>
|
||||
{
|
||||
["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
|
||||
["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture)
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -136,21 +131,19 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
}
|
||||
}
|
||||
|
||||
private void _providerManager_RefreshStarted(object sender, GenericEventArgs<BaseItem> e)
|
||||
private void OnProviderRefreshStarted(object sender, GenericEventArgs<BaseItem> e)
|
||||
{
|
||||
_providerManager_RefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
|
||||
OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
|
||||
}
|
||||
|
||||
private void _providerManager_RefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
|
||||
private void OnProviderRefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
|
||||
{
|
||||
_providerManager_RefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100)));
|
||||
OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 100)));
|
||||
}
|
||||
|
||||
private static bool EnableRefreshMessage(BaseItem item)
|
||||
{
|
||||
var folder = item as Folder;
|
||||
|
||||
if (folder == null)
|
||||
if (!(item is Folder folder))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
@ -183,7 +176,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</param>
|
||||
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
|
||||
void libraryManager_ItemAdded(object sender, ItemChangeEventArgs e)
|
||||
private void OnLibraryItemAdded(object sender, ItemChangeEventArgs e)
|
||||
{
|
||||
if (!FilterItem(e.Item))
|
||||
{
|
||||
|
@ -205,8 +198,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
|
||||
}
|
||||
|
||||
var parent = e.Item.GetParent() as Folder;
|
||||
if (parent != null)
|
||||
if (e.Item.GetParent() is Folder parent)
|
||||
{
|
||||
_foldersAddedTo.Add(parent);
|
||||
}
|
||||
|
@ -220,7 +212,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</param>
|
||||
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
|
||||
void libraryManager_ItemUpdated(object sender, ItemChangeEventArgs e)
|
||||
private void OnLibraryItemUpdated(object sender, ItemChangeEventArgs e)
|
||||
{
|
||||
if (!FilterItem(e.Item))
|
||||
{
|
||||
|
@ -231,8 +223,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
{
|
||||
if (LibraryUpdateTimer == null)
|
||||
{
|
||||
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration,
|
||||
Timeout.Infinite);
|
||||
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -248,7 +239,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</param>
|
||||
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
|
||||
void libraryManager_ItemRemoved(object sender, ItemChangeEventArgs e)
|
||||
private void OnLibraryItemRemoved(object sender, ItemChangeEventArgs e)
|
||||
{
|
||||
if (!FilterItem(e.Item))
|
||||
{
|
||||
|
@ -259,16 +250,14 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
{
|
||||
if (LibraryUpdateTimer == null)
|
||||
{
|
||||
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration,
|
||||
Timeout.Infinite);
|
||||
LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
|
||||
}
|
||||
else
|
||||
{
|
||||
LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
|
||||
}
|
||||
|
||||
var parent = e.Parent as Folder;
|
||||
if (parent != null)
|
||||
if (e.Parent is Folder parent)
|
||||
{
|
||||
_foldersRemovedFrom.Add(parent);
|
||||
}
|
||||
|
@ -486,13 +475,13 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
LibraryUpdateTimer = null;
|
||||
}
|
||||
|
||||
_libraryManager.ItemAdded -= libraryManager_ItemAdded;
|
||||
_libraryManager.ItemUpdated -= libraryManager_ItemUpdated;
|
||||
_libraryManager.ItemRemoved -= libraryManager_ItemRemoved;
|
||||
_libraryManager.ItemAdded -= OnLibraryItemAdded;
|
||||
_libraryManager.ItemUpdated -= OnLibraryItemUpdated;
|
||||
_libraryManager.ItemRemoved -= OnLibraryItemRemoved;
|
||||
|
||||
_providerManager.RefreshCompleted -= _providerManager_RefreshCompleted;
|
||||
_providerManager.RefreshStarted -= _providerManager_RefreshStarted;
|
||||
_providerManager.RefreshProgress -= _providerManager_RefreshProgress;
|
||||
_providerManager.RefreshCompleted -= OnProviderRefreshCompleted;
|
||||
_providerManager.RefreshStarted -= OnProviderRefreshStarted;
|
||||
_providerManager.RefreshProgress -= OnProviderRefreshProgress;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -449,7 +449,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
httpRes.StatusCode = 200;
|
||||
foreach(var (key, value) in GetDefaultCorsHeaders(httpReq))
|
||||
foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
|
||||
{
|
||||
httpRes.Headers.Add(key, value);
|
||||
}
|
||||
|
@ -486,7 +486,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
var handler = GetServiceHandler(httpReq);
|
||||
if (handler != null)
|
||||
{
|
||||
await handler.ProcessRequestAsync(this, httpReq, httpRes, _logger, cancellationToken).ConfigureAwait(false);
|
||||
await handler.ProcessRequestAsync(this, httpReq, httpRes, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -567,7 +567,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
|
||||
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
|
||||
|
||||
var connection = new WebSocketConnection(
|
||||
using var connection = new WebSocketConnection(
|
||||
_loggerFactory.CreateLogger<WebSocketConnection>(),
|
||||
webSocket,
|
||||
context.Connection.RemoteIpAddress,
|
||||
|
|
|
@ -35,9 +35,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
_networkManager = networkManager;
|
||||
}
|
||||
|
||||
public void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues)
|
||||
public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes)
|
||||
{
|
||||
ValidateUser(request, authAttribtues);
|
||||
ValidateUser(request, authAttributes);
|
||||
}
|
||||
|
||||
public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
|
||||
|
@ -63,17 +63,17 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
return auth;
|
||||
}
|
||||
|
||||
private User ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues)
|
||||
private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes)
|
||||
{
|
||||
// This code is executed before the service
|
||||
var auth = _authorizationContext.GetAuthorizationInfo(request);
|
||||
|
||||
if (!IsExemptFromAuthenticationToken(authAttribtues, request))
|
||||
if (!IsExemptFromAuthenticationToken(authAttributes, request))
|
||||
{
|
||||
ValidateSecurityToken(request, auth.Token);
|
||||
}
|
||||
|
||||
if (authAttribtues.AllowLocalOnly && !request.IsLocal)
|
||||
if (authAttributes.AllowLocalOnly && !request.IsLocal)
|
||||
{
|
||||
throw new SecurityException("Operation not found.");
|
||||
}
|
||||
|
@ -87,14 +87,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
|
||||
if (user != null)
|
||||
{
|
||||
ValidateUserAccess(user, request, authAttribtues, auth);
|
||||
ValidateUserAccess(user, request, authAttributes);
|
||||
}
|
||||
|
||||
var info = GetTokenInfo(request);
|
||||
|
||||
if (!IsExemptFromRoles(auth, authAttribtues, request, info))
|
||||
if (!IsExemptFromRoles(auth, authAttributes, request, info))
|
||||
{
|
||||
var roles = authAttribtues.GetRoles();
|
||||
var roles = authAttributes.GetRoles();
|
||||
|
||||
ValidateRoles(roles, user);
|
||||
}
|
||||
|
@ -118,8 +118,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
private void ValidateUserAccess(
|
||||
User user,
|
||||
IRequest request,
|
||||
IAuthenticationAttributes authAttributes,
|
||||
AuthorizationInfo auth)
|
||||
IAuthenticationAttributes authAttributes)
|
||||
{
|
||||
if (user.HasPermission(PermissionKind.IsDisabled))
|
||||
{
|
||||
|
@ -158,6 +157,11 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
return true;
|
||||
}
|
||||
|
||||
if (authAttribtues.IgnoreLegacyAuth)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -237,16 +241,6 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
{
|
||||
throw new AuthenticationException("Access token is invalid or expired.");
|
||||
}
|
||||
|
||||
// if (!string.IsNullOrEmpty(info.UserId))
|
||||
//{
|
||||
// var user = _userManager.GetUserById(info.UserId);
|
||||
|
||||
// if (user == null || user.Configuration.IsDisabled)
|
||||
// {
|
||||
// throw new SecurityException("User account has been disabled.");
|
||||
// }
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -97,6 +97,12 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
token = headers["X-MediaBrowser-Token"];
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
token = queryString["ApiKey"];
|
||||
}
|
||||
|
||||
// TODO deprecate this query parameter.
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
token = queryString["api_key"];
|
||||
|
@ -276,12 +282,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
|
||||
private static string NormalizeValue(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return WebUtility.HtmlEncode(value);
|
||||
return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
/// <summary>
|
||||
/// Class WebSocketConnection.
|
||||
/// </summary>
|
||||
public class WebSocketConnection : IWebSocketConnection
|
||||
public class WebSocketConnection : IWebSocketConnection, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The logger.
|
||||
|
@ -119,7 +119,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
Memory<byte> memory = writer.GetMemory(512);
|
||||
try
|
||||
{
|
||||
receiveresult = await _socket.ReceiveAsync(memory, cancellationToken);
|
||||
receiveresult = await _socket.ReceiveAsync(memory, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (WebSocketException ex)
|
||||
{
|
||||
|
@ -137,7 +137,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
writer.Advance(bytesRead);
|
||||
|
||||
// Make the data available to the PipeReader
|
||||
FlushResult flushResult = await writer.FlushAsync();
|
||||
FlushResult flushResult = await writer.FlushAsync().ConfigureAwait(false);
|
||||
if (flushResult.IsCompleted)
|
||||
{
|
||||
// The PipeReader stopped reading
|
||||
|
@ -223,7 +223,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||
|
||||
if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal))
|
||||
{
|
||||
await SendKeepAliveResponse();
|
||||
await SendKeepAliveResponse().ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -21,6 +21,7 @@ namespace Emby.Server.Implementations.IO
|
|||
private readonly List<string> _affectedPaths = new List<string>();
|
||||
private readonly object _timerLock = new object();
|
||||
private Timer _timer;
|
||||
private bool _disposed;
|
||||
|
||||
public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
|
||||
{
|
||||
|
@ -213,11 +214,11 @@ namespace Emby.Server.Implementations.IO
|
|||
}
|
||||
}
|
||||
|
||||
private bool _disposed;
|
||||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
DisposeTimer();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Emby.Server.Implementations.Images;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Server.Implementations.Images;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
|
|
|
@ -18,7 +18,21 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
"**/small.jpg",
|
||||
"**/albumart.jpg",
|
||||
"**/*sample*",
|
||||
|
||||
// We have neither non-greedy matching or character group repetitions, working around that here.
|
||||
// https://github.com/dazinator/DotNet.Glob#patterns
|
||||
// .*/sample\..{1,5}
|
||||
"**/sample.?",
|
||||
"**/sample.??",
|
||||
"**/sample.???", // Matches sample.mkv
|
||||
"**/sample.????", // Matches sample.webm
|
||||
"**/sample.?????",
|
||||
"**/*.sample.?",
|
||||
"**/*.sample.??",
|
||||
"**/*.sample.???",
|
||||
"**/*.sample.????",
|
||||
"**/*.sample.?????",
|
||||
"**/sample/*",
|
||||
|
||||
// Directories
|
||||
"**/metadata/**",
|
||||
|
@ -64,10 +78,13 @@ namespace Emby.Server.Implementations.Library
|
|||
"**/.grab/**",
|
||||
"**/.grab",
|
||||
|
||||
// Unix hidden files and directories
|
||||
"**/.*/**",
|
||||
// Unix hidden files
|
||||
"**/.*",
|
||||
|
||||
// Mac - if you ever remove the above.
|
||||
// "**/._*",
|
||||
// "**/.DS_Store",
|
||||
|
||||
// thumbs.db
|
||||
"**/thumbs.db",
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
@ -46,11 +45,11 @@ using MediaBrowser.Model.Net;
|
|||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using MediaBrowser.Providers.MediaInfo;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||
using Genre = MediaBrowser.Controller.Entities.Genre;
|
||||
using Person = MediaBrowser.Controller.Entities.Person;
|
||||
using SortOrder = MediaBrowser.Model.Entities.SortOrder;
|
||||
using VideoResolver = Emby.Naming.Video.VideoResolver;
|
||||
|
||||
namespace Emby.Server.Implementations.Library
|
||||
|
@ -63,6 +62,7 @@ namespace Emby.Server.Implementations.Library
|
|||
private const string ShortcutFileExtension = ".mblink";
|
||||
|
||||
private readonly ILogger<LibraryManager> _logger;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly ITaskManager _taskManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IUserDataManager _userDataRepository;
|
||||
|
@ -74,7 +74,6 @@ namespace Emby.Server.Implementations.Library
|
|||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IItemRepository _itemRepository;
|
||||
private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
|
||||
/// <summary>
|
||||
|
@ -112,6 +111,7 @@ namespace Emby.Server.Implementations.Library
|
|||
/// <param name="mediaEncoder">The media encoder.</param>
|
||||
/// <param name="itemRepository">The item repository.</param>
|
||||
/// <param name="imageProcessor">The image processor.</param>
|
||||
/// <param name="memoryCache">The memory cache.</param>
|
||||
public LibraryManager(
|
||||
IServerApplicationHost appHost,
|
||||
ILogger<LibraryManager> logger,
|
||||
|
@ -125,7 +125,8 @@ namespace Emby.Server.Implementations.Library
|
|||
Lazy<IUserViewManager> userviewManagerFactory,
|
||||
IMediaEncoder mediaEncoder,
|
||||
IItemRepository itemRepository,
|
||||
IImageProcessor imageProcessor)
|
||||
IImageProcessor imageProcessor,
|
||||
IMemoryCache memoryCache)
|
||||
{
|
||||
_appHost = appHost;
|
||||
_logger = logger;
|
||||
|
@ -140,8 +141,7 @@ namespace Emby.Server.Implementations.Library
|
|||
_mediaEncoder = mediaEncoder;
|
||||
_itemRepository = itemRepository;
|
||||
_imageProcessor = imageProcessor;
|
||||
|
||||
_libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>();
|
||||
_memoryCache = memoryCache;
|
||||
|
||||
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
|
||||
|
||||
|
@ -299,7 +299,7 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
}
|
||||
|
||||
_libraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; });
|
||||
_memoryCache.Set(item.Id, item);
|
||||
}
|
||||
|
||||
public void DeleteItem(BaseItem item, DeleteOptions options)
|
||||
|
@ -447,7 +447,7 @@ namespace Emby.Server.Implementations.Library
|
|||
_itemRepository.DeleteItem(child.Id);
|
||||
}
|
||||
|
||||
_libraryItemsCache.TryRemove(item.Id, out BaseItem removed);
|
||||
_memoryCache.Remove(item.Id);
|
||||
|
||||
ReportItemRemoved(item, parent);
|
||||
}
|
||||
|
@ -1248,7 +1248,7 @@ namespace Emby.Server.Implementations.Library
|
|||
throw new ArgumentException("Guid can't be empty", nameof(id));
|
||||
}
|
||||
|
||||
if (_libraryItemsCache.TryGetValue(id, out BaseItem item))
|
||||
if (_memoryCache.TryGetValue(id, out BaseItem item))
|
||||
{
|
||||
return item;
|
||||
}
|
||||
|
@ -1591,7 +1591,6 @@ namespace Emby.Server.Implementations.Library
|
|||
public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
|
||||
{
|
||||
var tasks = IntroProviders
|
||||
.OrderBy(i => i.GetType().Name.Contains("Default", StringComparison.OrdinalIgnoreCase) ? 1 : 0)
|
||||
.Take(1)
|
||||
.Select(i => GetIntros(i, item, user));
|
||||
|
||||
|
|
|
@ -46,8 +46,6 @@ namespace Emby.Server.Implementations.Library
|
|||
private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
|
||||
|
||||
private readonly object _disposeLock = new object();
|
||||
|
||||
private IMediaSourceProvider[] _providers;
|
||||
|
||||
public MediaSourceManager(
|
||||
|
@ -623,12 +621,14 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
if (liveStreamInfo is IDirectStreamProvider)
|
||||
{
|
||||
var info = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
|
||||
{
|
||||
MediaSource = mediaSource,
|
||||
ExtractChapters = false,
|
||||
MediaType = DlnaProfileType.Video
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
var info = await _mediaEncoder.GetMediaInfo(
|
||||
new MediaInfoRequest
|
||||
{
|
||||
MediaSource = mediaSource,
|
||||
ExtractChapters = false,
|
||||
MediaType = DlnaProfileType.Video
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
mediaSource.MediaStreams = info.MediaStreams;
|
||||
mediaSource.Container = info.Container;
|
||||
|
@ -859,11 +859,11 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
}
|
||||
|
||||
private Tuple<IMediaSourceProvider, string> GetProvider(string key)
|
||||
private (IMediaSourceProvider, string) GetProvider(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
throw new ArgumentException("key");
|
||||
throw new ArgumentException("Key can't be empty.", nameof(key));
|
||||
}
|
||||
|
||||
var keys = key.Split(new[] { LiveStreamIdDelimeter }, 2);
|
||||
|
@ -873,7 +873,7 @@ namespace Emby.Server.Implementations.Library
|
|||
var splitIndex = key.IndexOf(LiveStreamIdDelimeter, StringComparison.Ordinal);
|
||||
var keyId = key.Substring(splitIndex + 1);
|
||||
|
||||
return new Tuple<IMediaSourceProvider, string>(provider, keyId);
|
||||
return (provider, keyId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -893,15 +893,12 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
if (dispose)
|
||||
{
|
||||
lock (_disposeLock)
|
||||
foreach (var key in _openStreams.Keys.ToList())
|
||||
{
|
||||
foreach (var key in _openStreams.Keys.ToList())
|
||||
{
|
||||
var task = CloseLiveStream(key);
|
||||
|
||||
Task.WaitAll(task);
|
||||
}
|
||||
CloseLiveStream(key).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
_liveStreamSemaphore.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,12 +4,12 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
|
||||
|
||||
|
|
|
@ -4,12 +4,12 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Extensions;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Search;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
|
@ -12,6 +12,7 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using Emby.Server.Implementations.Library;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
|
|
|
@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
}
|
||||
}
|
||||
|
||||
public class HdHomerunManager : IDisposable
|
||||
public sealed class HdHomerunManager : IDisposable
|
||||
{
|
||||
public const int HdHomeRunPort = 65001;
|
||||
|
||||
|
@ -105,6 +105,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
StopStreaming(socket).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
|
||||
|
@ -162,7 +164,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
}
|
||||
|
||||
_activeTuner = i;
|
||||
var lockKeyString = string.Format("{0:d}", lockKeyValue);
|
||||
var lockKeyString = string.Format(CultureInfo.InvariantCulture, "{0:d}", lockKeyValue);
|
||||
var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null);
|
||||
await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false);
|
||||
int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
|
||||
|
@ -173,8 +175,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
continue;
|
||||
}
|
||||
|
||||
var commandList = commands.GetCommands();
|
||||
foreach (var command in commandList)
|
||||
foreach (var command in commands.GetCommands())
|
||||
{
|
||||
var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue);
|
||||
await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
|
||||
|
@ -188,7 +189,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
}
|
||||
}
|
||||
|
||||
var targetValue = string.Format("rtp://{0}:{1}", localIp, localPort);
|
||||
var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort);
|
||||
var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue);
|
||||
|
||||
await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false);
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"CameraImageUploadedFrom": "একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে {0} থেকে",
|
||||
"Books": "বই",
|
||||
"AuthenticationSucceededWithUserName": "{0} যাচাই সফল",
|
||||
"Artists": "শিল্পী",
|
||||
"Artists": "শিল্পীরা",
|
||||
"Application": "অ্যাপ্লিকেশন",
|
||||
"Albums": "অ্যালবামগুলো",
|
||||
"HeaderFavoriteEpisodes": "প্রিব পর্বগুলো",
|
||||
|
@ -19,7 +19,7 @@
|
|||
"Genres": "ঘরানা",
|
||||
"Folders": "ফোল্ডারগুলো",
|
||||
"Favorites": "ফেভারিটগুলো",
|
||||
"FailedLoginAttemptWithUserName": "{0} থেকে লগিন করতে ব্যর্থ",
|
||||
"FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
|
||||
"AppDeviceValues": "এপ: {0}, ডিভাইস: {0}",
|
||||
"VersionNumber": "সংস্করণ {0}",
|
||||
"ValueSpecialEpisodeName": "বিশেষ - {0}",
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"Artists": "Interpreten",
|
||||
"AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich angemeldet",
|
||||
"Books": "Bücher",
|
||||
"CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen",
|
||||
"CameraImageUploadedFrom": "Ein neues Kamerafoto wurde von {0} hochgeladen",
|
||||
"Channels": "Kanäle",
|
||||
"ChapterNameValue": "Kapitel {0}",
|
||||
"Collections": "Sammlungen",
|
||||
|
@ -101,12 +101,12 @@
|
|||
"TaskCleanTranscode": "Lösche Transkodier Pfad",
|
||||
"TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.",
|
||||
"TaskUpdatePlugins": "Update Plugins",
|
||||
"TaskRefreshPeopleDescription": "Erneuert Metadaten für Schausteller und Regisseure in deinen Bibliotheken.",
|
||||
"TaskRefreshPeople": "Erneuere Schausteller",
|
||||
"TaskRefreshPeopleDescription": "Erneuert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
|
||||
"TaskRefreshPeople": "Erneuere Schauspieler",
|
||||
"TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.",
|
||||
"TaskCleanLogs": "Lösche Log Pfad",
|
||||
"TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",
|
||||
"TaskRefreshLibrary": "Scanne alle Bibliotheken",
|
||||
"TaskRefreshLibrary": "Scanne Medien-Bibliothek",
|
||||
"TaskRefreshChapterImagesDescription": "Kreiert Vorschaubilder für Videos welche Kapitel haben.",
|
||||
"TaskRefreshChapterImages": "Extrahiert Kapitel-Bilder",
|
||||
"TaskCleanCacheDescription": "Löscht Zwischenspeicherdatein die nicht länger von System gebraucht werden.",
|
||||
|
|
|
@ -18,13 +18,13 @@
|
|||
"HeaderAlbumArtists": "אמני האלבום",
|
||||
"HeaderCameraUploads": "העלאות ממצלמה",
|
||||
"HeaderContinueWatching": "המשך לצפות",
|
||||
"HeaderFavoriteAlbums": "אלבומים שאהבתי",
|
||||
"HeaderFavoriteAlbums": "אלבומים מועדפים",
|
||||
"HeaderFavoriteArtists": "אמנים מועדפים",
|
||||
"HeaderFavoriteEpisodes": "פרקים מועדפים",
|
||||
"HeaderFavoriteShows": "סדרות מועדפות",
|
||||
"HeaderFavoriteShows": "תוכניות מועדפות",
|
||||
"HeaderFavoriteSongs": "שירים מועדפים",
|
||||
"HeaderLiveTV": "שידורים חיים",
|
||||
"HeaderNextUp": "הבא",
|
||||
"HeaderNextUp": "הבא בתור",
|
||||
"HeaderRecordingGroups": "קבוצות הקלטה",
|
||||
"HomeVideos": "סרטונים בייתים",
|
||||
"Inherit": "הורש",
|
||||
|
@ -45,37 +45,37 @@
|
|||
"NameSeasonNumber": "עונה {0}",
|
||||
"NameSeasonUnknown": "עונה לא ידועה",
|
||||
"NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Application update available",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Application update installed",
|
||||
"NotificationOptionAudioPlayback": "Audio playback started",
|
||||
"NotificationOptionAudioPlaybackStopped": "Audio playback stopped",
|
||||
"NotificationOptionCameraImageUploaded": "Camera image uploaded",
|
||||
"NotificationOptionApplicationUpdateAvailable": "קיים עדכון זמין ליישום",
|
||||
"NotificationOptionApplicationUpdateInstalled": "עדכון ליישום הותקן",
|
||||
"NotificationOptionAudioPlayback": "ניגון שמע החל",
|
||||
"NotificationOptionAudioPlaybackStopped": "ניגון שמע הופסק",
|
||||
"NotificationOptionCameraImageUploaded": "תמונת מצלמה הועלתה",
|
||||
"NotificationOptionInstallationFailed": "התקנה נכשלה",
|
||||
"NotificationOptionNewLibraryContent": "New content added",
|
||||
"NotificationOptionPluginError": "Plugin failure",
|
||||
"NotificationOptionNewLibraryContent": "תוכן חדש הוסף",
|
||||
"NotificationOptionPluginError": "כשלון בתוסף",
|
||||
"NotificationOptionPluginInstalled": "התוסף הותקן",
|
||||
"NotificationOptionPluginUninstalled": "התוסף הוסר",
|
||||
"NotificationOptionPluginUpdateInstalled": "העדכון לתוסף הותקן",
|
||||
"NotificationOptionServerRestartRequired": "יש לאתחל את השרת",
|
||||
"NotificationOptionTaskFailed": "Scheduled task failure",
|
||||
"NotificationOptionUserLockedOut": "User locked out",
|
||||
"NotificationOptionVideoPlayback": "Video playback started",
|
||||
"NotificationOptionVideoPlaybackStopped": "Video playback stopped",
|
||||
"NotificationOptionTaskFailed": "משימה מתוזמנת נכשלה",
|
||||
"NotificationOptionUserLockedOut": "משתמש ננעל",
|
||||
"NotificationOptionVideoPlayback": "ניגון וידאו החל",
|
||||
"NotificationOptionVideoPlaybackStopped": "ניגון וידאו הופסק",
|
||||
"Photos": "תמונות",
|
||||
"Playlists": "רשימות הפעלה",
|
||||
"Plugin": "Plugin",
|
||||
"PluginInstalledWithName": "{0} was installed",
|
||||
"PluginUninstalledWithName": "{0} was uninstalled",
|
||||
"PluginUpdatedWithName": "{0} was updated",
|
||||
"PluginInstalledWithName": "{0} הותקן",
|
||||
"PluginUninstalledWithName": "{0} הוסר",
|
||||
"PluginUpdatedWithName": "{0} עודכן",
|
||||
"ProviderValue": "Provider: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} failed",
|
||||
"ScheduledTaskStartedWithName": "{0} started",
|
||||
"ServerNameNeedsToBeRestarted": "{0} needs to be restarted",
|
||||
"ScheduledTaskFailedWithName": "{0} נכשל",
|
||||
"ScheduledTaskStartedWithName": "{0} החל",
|
||||
"ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש",
|
||||
"Shows": "סדרות",
|
||||
"Songs": "שירים",
|
||||
"StartupEmbyServerIsLoading": "שרת Jellyfin בהליכי טעינה. אנא נסה שנית בעוד זמן קצר.",
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}",
|
||||
"SubtitleDownloadFailureFromForItem": "הורדת כתוביות נכשלה מ-{0} עבור {1}",
|
||||
"Sync": "סנכרן",
|
||||
"System": "System",
|
||||
"TvShows": "סדרות טלוויזיה",
|
||||
|
@ -83,14 +83,14 @@
|
|||
"UserCreatedWithName": "המשתמש {0} נוצר",
|
||||
"UserDeletedWithName": "המשתמש {0} הוסר",
|
||||
"UserDownloadingItemWithValues": "{0} מוריד את {1}",
|
||||
"UserLockedOutWithName": "User {0} has been locked out",
|
||||
"UserOfflineFromDevice": "{0} has disconnected from {1}",
|
||||
"UserOnlineFromDevice": "{0} is online from {1}",
|
||||
"UserPasswordChangedWithName": "Password has been changed for user {0}",
|
||||
"UserPolicyUpdatedWithName": "User policy has been updated for {0}",
|
||||
"UserLockedOutWithName": "המשתמש {0} ננעל",
|
||||
"UserOfflineFromDevice": "{0} התנתק מ-{1}",
|
||||
"UserOnlineFromDevice": "{0} מחובר מ-{1}",
|
||||
"UserPasswordChangedWithName": "הסיסמה שונתה עבור המשתמש {0}",
|
||||
"UserPolicyUpdatedWithName": "מדיניות המשתמש {0} עודכנה",
|
||||
"UserStartedPlayingItemWithValues": "{0} מנגן את {1} על {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} סיים לנגן את {1} על {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
|
||||
"ValueHasBeenAddedToLibrary": "{0} התווסף לספריית המדיה שלך",
|
||||
"ValueSpecialEpisodeName": "מיוחד- {0}",
|
||||
"VersionNumber": "Version {0}",
|
||||
"TaskRefreshLibrary": "סרוק ספריית מדיה",
|
||||
|
@ -109,7 +109,7 @@
|
|||
"TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
|
||||
"TasksChannelsCategory": "ערוצי אינטרנט",
|
||||
"TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט עבור הכתוביות החסרות בהתבסס על המטה-דיאטה.",
|
||||
"TaskDownloadMissingSubtitles": "הורד כתוביות חסרות.",
|
||||
"TaskDownloadMissingSubtitles": "הורד כתוביות חסרות",
|
||||
"TaskRefreshChannelsDescription": "רענן פרטי ערוץ אינטרנטי.",
|
||||
"TaskRefreshChannels": "רענן ערוץ",
|
||||
"TaskCleanTranscodeDescription": "מחק קבצי transcode שנוצרו מלפני יותר מיום.",
|
||||
|
|
|
@ -84,8 +84,8 @@
|
|||
"UserDeletedWithName": "L'utente {0} è stato rimosso",
|
||||
"UserDownloadingItemWithValues": "{0} sta scaricando {1}",
|
||||
"UserLockedOutWithName": "L'utente {0} è stato bloccato",
|
||||
"UserOfflineFromDevice": "{0} è stato disconnesso da {1}",
|
||||
"UserOnlineFromDevice": "{0} è online da {1}",
|
||||
"UserOfflineFromDevice": "{0} si è disconnesso su {1}",
|
||||
"UserOnlineFromDevice": "{0} è online su {1}",
|
||||
"UserPasswordChangedWithName": "La password è stata cambiata per l'utente {0}",
|
||||
"UserPolicyUpdatedWithName": "La policy dell'utente è stata aggiornata per {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} ha avviato la riproduzione di {1} su {2}",
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"MusicVideos": "Музичні відео",
|
||||
"MusicVideos": "Музичні кліпи",
|
||||
"Music": "Музика",
|
||||
"Movies": "Фільми",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server був оновлений до версії {0}",
|
||||
"MessageApplicationUpdated": "Jellyfin Server був оновлений",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server оновлено до версії {0}",
|
||||
"MessageApplicationUpdated": "Jellyfin Server оновлено",
|
||||
"Latest": "Останні",
|
||||
"LabelIpAddressValue": "IP-адреси: {0}",
|
||||
"ItemRemovedWithName": "{0} видалено з бібліотеки",
|
||||
"ItemAddedWithName": "{0} додано до бібліотеки",
|
||||
"LabelIpAddressValue": "IP-адреса: {0}",
|
||||
"ItemRemovedWithName": "{0} видалено з медіатеки",
|
||||
"ItemAddedWithName": "{0} додано до медіатеки",
|
||||
"HeaderNextUp": "Наступний",
|
||||
"HeaderLiveTV": "Ефірне ТБ",
|
||||
"HeaderFavoriteSongs": "Улюблені пісні",
|
||||
|
@ -17,20 +17,101 @@
|
|||
"HeaderFavoriteAlbums": "Улюблені альбоми",
|
||||
"HeaderContinueWatching": "Продовжити перегляд",
|
||||
"HeaderCameraUploads": "Завантажено з камери",
|
||||
"HeaderAlbumArtists": "Виконавці альбомів",
|
||||
"HeaderAlbumArtists": "Виконавці альбому",
|
||||
"Genres": "Жанри",
|
||||
"Folders": "Директорії",
|
||||
"Folders": "Каталоги",
|
||||
"Favorites": "Улюблені",
|
||||
"DeviceOnlineWithName": "{0} під'єднано",
|
||||
"DeviceOfflineWithName": "{0} від'єднано",
|
||||
"DeviceOnlineWithName": "Пристрій {0} підключився",
|
||||
"DeviceOfflineWithName": "Пристрій {0} відключився",
|
||||
"Collections": "Колекції",
|
||||
"ChapterNameValue": "Глава {0}",
|
||||
"ChapterNameValue": "Розділ {0}",
|
||||
"Channels": "Канали",
|
||||
"CameraImageUploadedFrom": "Нова фотографія завантажена з {0}",
|
||||
"Books": "Книги",
|
||||
"AuthenticationSucceededWithUserName": "{0} успішно авторизовані",
|
||||
"AuthenticationSucceededWithUserName": "{0} успішно авторизований",
|
||||
"Artists": "Виконавці",
|
||||
"Application": "Додаток",
|
||||
"AppDeviceValues": "Додаток: {0}, Пристрій: {1}",
|
||||
"Albums": "Альбоми"
|
||||
"Albums": "Альбоми",
|
||||
"NotificationOptionServerRestartRequired": "Необхідно перезапустити сервер",
|
||||
"NotificationOptionPluginUpdateInstalled": "Встановлено оновлення плагіна",
|
||||
"NotificationOptionPluginUninstalled": "Плагін видалено",
|
||||
"NotificationOptionPluginInstalled": "Плагін встановлено",
|
||||
"NotificationOptionPluginError": "Помилка плагіна",
|
||||
"NotificationOptionNewLibraryContent": "Додано новий контент",
|
||||
"HomeVideos": "Домашнє відео",
|
||||
"FailedLoginAttemptWithUserName": "Невдала спроба входу від {0}",
|
||||
"LabelRunningTimeValue": "Тривалість: {0}",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Шукає в Інтернеті відсутні субтитри на основі конфігурації метаданих.",
|
||||
"TaskDownloadMissingSubtitles": "Завантажити відсутні субтитри",
|
||||
"TaskRefreshChannelsDescription": "Оновлення інформації про Інтернет-канали.",
|
||||
"TaskRefreshChannels": "Оновити канали",
|
||||
"TaskCleanTranscodeDescription": "Вилучає файли для перекодування старше одного дня.",
|
||||
"TaskCleanTranscode": "Очистити каталог перекодування",
|
||||
"TaskUpdatePluginsDescription": "Завантажує та встановлює оновлення для плагінів, налаштованих на автоматичне оновлення.",
|
||||
"TaskUpdatePlugins": "Оновити плагіни",
|
||||
"TaskRefreshPeopleDescription": "Оновлення метаданих для акторів та режисерів у вашій медіатеці.",
|
||||
"TaskRefreshPeople": "Оновити людей",
|
||||
"TaskCleanLogsDescription": "Видаляє файли журналу, яким більше {0} днів.",
|
||||
"TaskCleanLogs": "Очистити журнали",
|
||||
"TaskRefreshLibraryDescription": "Сканує медіатеку на нові файли та оновлює метадані.",
|
||||
"TaskRefreshLibrary": "Сканувати медіатеку",
|
||||
"TaskRefreshChapterImagesDescription": "Створює ескізи для відео, які мають розділи.",
|
||||
"TaskRefreshChapterImages": "Створити ескізи розділів",
|
||||
"TaskCleanCacheDescription": "Видаляє файли кешу, які більше не потрібні системі.",
|
||||
"TaskCleanCache": "Очистити кеш",
|
||||
"TasksChannelsCategory": "Інтернет-канали",
|
||||
"TasksApplicationCategory": "Додаток",
|
||||
"TasksLibraryCategory": "Медіатека",
|
||||
"TasksMaintenanceCategory": "Обслуговування",
|
||||
"VersionNumber": "Версія {0}",
|
||||
"ValueSpecialEpisodeName": "Спецепізод - {0}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} додано до медіатеки",
|
||||
"UserStoppedPlayingItemWithValues": "{0} закінчив відтворення {1} на {2}",
|
||||
"UserStartedPlayingItemWithValues": "{0} відтворює {1} на {2}",
|
||||
"UserPolicyUpdatedWithName": "Політика користувача оновлена для {0}",
|
||||
"UserPasswordChangedWithName": "Пароль змінено для користувача {0}",
|
||||
"UserOnlineFromDevice": "{0} підключився з {1}",
|
||||
"UserOfflineFromDevice": "{0} відключився від {1}",
|
||||
"UserLockedOutWithName": "Користувача {0} заблоковано",
|
||||
"UserDownloadingItemWithValues": "{0} завантажує {1}",
|
||||
"UserDeletedWithName": "Користувача {0} видалено",
|
||||
"UserCreatedWithName": "Користувача {0} створено",
|
||||
"User": "Користувач",
|
||||
"TvShows": "ТВ-шоу",
|
||||
"System": "Система",
|
||||
"Sync": "Синхронізація",
|
||||
"SubtitleDownloadFailureFromForItem": "Не вдалося завантажити субтитри з {0} для {1}",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server завантажується. Будь ласка, спробуйте трішки пізніше.",
|
||||
"Songs": "Пісні",
|
||||
"Shows": "Шоу",
|
||||
"ServerNameNeedsToBeRestarted": "{0} потрібно перезапустити",
|
||||
"ScheduledTaskStartedWithName": "{0} розпочато",
|
||||
"ScheduledTaskFailedWithName": "Помилка {0}",
|
||||
"ProviderValue": "Постачальник: {0}",
|
||||
"PluginUpdatedWithName": "{0} оновлено",
|
||||
"PluginUninstalledWithName": "{0} видалено",
|
||||
"PluginInstalledWithName": "{0} встановлено",
|
||||
"Plugin": "Плагін",
|
||||
"Playlists": "Плейлисти",
|
||||
"Photos": "Фотографії",
|
||||
"NotificationOptionVideoPlaybackStopped": "Відтворення відео зупинено",
|
||||
"NotificationOptionVideoPlayback": "Розпочато відтворення відео",
|
||||
"NotificationOptionUserLockedOut": "Користувача заблоковано",
|
||||
"NotificationOptionTaskFailed": "Помилка запланованого завдання",
|
||||
"NotificationOptionInstallationFailed": "Помилка встановлення",
|
||||
"NotificationOptionCameraImageUploaded": "Фотографію завантажено",
|
||||
"NotificationOptionAudioPlaybackStopped": "Відтворення аудіо зупинено",
|
||||
"NotificationOptionAudioPlayback": "Розпочато відтворення аудіо",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Встановлено оновлення додатка",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Доступне оновлення додатка",
|
||||
"NewVersionIsAvailable": "Для завантаження доступна нова версія Jellyfin Server.",
|
||||
"NameSeasonUnknown": "Сезон Невідомий",
|
||||
"NameSeasonNumber": "Сезон {0}",
|
||||
"NameInstallFailed": "Не вдалося встановити {0}",
|
||||
"MixedContent": "Змішаний контент",
|
||||
"MessageServerConfigurationUpdated": "Конфігурація сервера оновлена",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Розділ конфігурації сервера {0} оновлено",
|
||||
"Inherit": "Успадкувати",
|
||||
"HeaderRecordingGroups": "Групи запису"
|
||||
}
|
||||
|
|
|
@ -92,7 +92,7 @@
|
|||
"HeaderRecordingGroups": "錄製組",
|
||||
"Inherit": "繼承",
|
||||
"SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕",
|
||||
"TaskDownloadMissingSubtitlesDescription": "在網路上透過描述資料搜尋遺失的字幕。",
|
||||
"TaskDownloadMissingSubtitlesDescription": "在網路上透過中繼資料搜尋遺失的字幕。",
|
||||
"TaskDownloadMissingSubtitles": "下載遺失的字幕",
|
||||
"TaskRefreshChannels": "重新整理頻道",
|
||||
"TaskUpdatePlugins": "更新插件",
|
||||
|
|
|
@ -15,13 +15,11 @@ namespace Emby.Server.Implementations.Net
|
|||
public sealed class UdpSocket : ISocket, IDisposable
|
||||
{
|
||||
private Socket _socket;
|
||||
private int _localPort;
|
||||
private readonly int _localPort;
|
||||
private bool _disposed = false;
|
||||
|
||||
public Socket Socket => _socket;
|
||||
|
||||
public IPAddress LocalIPAddress { get; }
|
||||
|
||||
private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs()
|
||||
{
|
||||
SocketFlags = SocketFlags.None
|
||||
|
@ -51,18 +49,33 @@ namespace Emby.Server.Implementations.Net
|
|||
InitReceiveSocketAsyncEventArgs();
|
||||
}
|
||||
|
||||
public UdpSocket(Socket socket, IPEndPoint endPoint)
|
||||
{
|
||||
if (socket == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(socket));
|
||||
}
|
||||
|
||||
_socket = socket;
|
||||
_socket.Connect(endPoint);
|
||||
|
||||
InitReceiveSocketAsyncEventArgs();
|
||||
}
|
||||
|
||||
public IPAddress LocalIPAddress { get; }
|
||||
|
||||
private void InitReceiveSocketAsyncEventArgs()
|
||||
{
|
||||
var receiveBuffer = new byte[8192];
|
||||
_receiveSocketAsyncEventArgs.SetBuffer(receiveBuffer, 0, receiveBuffer.Length);
|
||||
_receiveSocketAsyncEventArgs.Completed += _receiveSocketAsyncEventArgs_Completed;
|
||||
_receiveSocketAsyncEventArgs.Completed += OnReceiveSocketAsyncEventArgsCompleted;
|
||||
|
||||
var sendBuffer = new byte[8192];
|
||||
_sendSocketAsyncEventArgs.SetBuffer(sendBuffer, 0, sendBuffer.Length);
|
||||
_sendSocketAsyncEventArgs.Completed += _sendSocketAsyncEventArgs_Completed;
|
||||
_sendSocketAsyncEventArgs.Completed += OnSendSocketAsyncEventArgsCompleted;
|
||||
}
|
||||
|
||||
private void _receiveSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e)
|
||||
private void OnReceiveSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e)
|
||||
{
|
||||
var tcs = _currentReceiveTaskCompletionSource;
|
||||
if (tcs != null)
|
||||
|
@ -86,7 +99,7 @@ namespace Emby.Server.Implementations.Net
|
|||
}
|
||||
}
|
||||
|
||||
private void _sendSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e)
|
||||
private void OnSendSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e)
|
||||
{
|
||||
var tcs = _currentSendTaskCompletionSource;
|
||||
if (tcs != null)
|
||||
|
@ -104,19 +117,6 @@ namespace Emby.Server.Implementations.Net
|
|||
}
|
||||
}
|
||||
|
||||
public UdpSocket(Socket socket, IPEndPoint endPoint)
|
||||
{
|
||||
if (socket == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(socket));
|
||||
}
|
||||
|
||||
_socket = socket;
|
||||
_socket.Connect(endPoint);
|
||||
|
||||
InitReceiveSocketAsyncEventArgs();
|
||||
}
|
||||
|
||||
public IAsyncResult BeginReceive(byte[] buffer, int offset, int count, AsyncCallback callback)
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
|
@ -247,6 +247,7 @@ namespace Emby.Server.Implementations.Net
|
|||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
|
@ -255,6 +256,8 @@ namespace Emby.Server.Implementations.Net
|
|||
}
|
||||
|
||||
_socket?.Dispose();
|
||||
_receiveSocketAsyncEventArgs.Dispose();
|
||||
_sendSocketAsyncEventArgs.Dispose();
|
||||
_currentReceiveTaskCompletionSource?.TrySetCanceled();
|
||||
_currentSendTaskCompletionSource?.TrySetCanceled();
|
||||
|
||||
|
|
|
@ -165,7 +165,7 @@ namespace Emby.Server.Implementations.Networking
|
|||
(octet[0] == 127) || // RFC1122
|
||||
(octet[0] == 169 && octet[1] == 254)) // RFC3927
|
||||
{
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint))
|
||||
|
|
|
@ -349,16 +349,14 @@ namespace Emby.Server.Implementations.Playlists
|
|||
AlbumTitle = child.Album
|
||||
};
|
||||
|
||||
var hasAlbumArtist = child as IHasAlbumArtist;
|
||||
if (hasAlbumArtist != null)
|
||||
if (child is IHasAlbumArtist hasAlbumArtist)
|
||||
{
|
||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
|
||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
|
||||
}
|
||||
|
||||
var hasArtist = child as IHasArtist;
|
||||
if (hasArtist != null)
|
||||
if (child is IHasArtist hasArtist)
|
||||
{
|
||||
entry.TrackArtist = hasArtist.Artists.FirstOrDefault();
|
||||
entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
|
||||
}
|
||||
|
||||
if (child.RunTimeTicks.HasValue)
|
||||
|
@ -385,16 +383,14 @@ namespace Emby.Server.Implementations.Playlists
|
|||
AlbumTitle = child.Album
|
||||
};
|
||||
|
||||
var hasAlbumArtist = child as IHasAlbumArtist;
|
||||
if (hasAlbumArtist != null)
|
||||
if (child is IHasAlbumArtist hasAlbumArtist)
|
||||
{
|
||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
|
||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
|
||||
}
|
||||
|
||||
var hasArtist = child as IHasArtist;
|
||||
if (hasArtist != null)
|
||||
if (child is IHasArtist hasArtist)
|
||||
{
|
||||
entry.TrackArtist = hasArtist.Artists.FirstOrDefault();
|
||||
entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null;
|
||||
}
|
||||
|
||||
if (child.RunTimeTicks.HasValue)
|
||||
|
@ -411,8 +407,10 @@ namespace Emby.Server.Implementations.Playlists
|
|||
|
||||
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var playlist = new M3uPlaylist();
|
||||
playlist.IsExtended = true;
|
||||
var playlist = new M3uPlaylist
|
||||
{
|
||||
IsExtended = true
|
||||
};
|
||||
foreach (var child in item.GetLinkedChildren())
|
||||
{
|
||||
var entry = new M3uPlaylistEntry()
|
||||
|
@ -422,10 +420,9 @@ namespace Emby.Server.Implementations.Playlists
|
|||
Album = child.Album
|
||||
};
|
||||
|
||||
var hasAlbumArtist = child as IHasAlbumArtist;
|
||||
if (hasAlbumArtist != null)
|
||||
if (child is IHasAlbumArtist hasAlbumArtist)
|
||||
{
|
||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
|
||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
|
||||
}
|
||||
|
||||
if (child.RunTimeTicks.HasValue)
|
||||
|
@ -453,10 +450,9 @@ namespace Emby.Server.Implementations.Playlists
|
|||
Album = child.Album
|
||||
};
|
||||
|
||||
var hasAlbumArtist = child as IHasAlbumArtist;
|
||||
if (hasAlbumArtist != null)
|
||||
if (child is IHasAlbumArtist hasAlbumArtist)
|
||||
{
|
||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
|
||||
entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null;
|
||||
}
|
||||
|
||||
if (child.RunTimeTicks.HasValue)
|
||||
|
@ -514,7 +510,7 @@ namespace Emby.Server.Implementations.Playlists
|
|||
|
||||
if (!folderPath.EndsWith(Path.DirectorySeparatorChar))
|
||||
{
|
||||
folderPath = folderPath + Path.DirectorySeparatorChar;
|
||||
folderPath += Path.DirectorySeparatorChar;
|
||||
}
|
||||
|
||||
var folderUri = new Uri(folderPath);
|
||||
|
@ -537,32 +533,12 @@ namespace Emby.Server.Implementations.Playlists
|
|||
return relativePath;
|
||||
}
|
||||
|
||||
private static string UnEscape(string content)
|
||||
{
|
||||
if (content == null)
|
||||
{
|
||||
return content;
|
||||
}
|
||||
|
||||
return content.Replace("&", "&").Replace("'", "'").Replace(""", "\"").Replace(">", ">").Replace("<", "<");
|
||||
}
|
||||
|
||||
private static string Escape(string content)
|
||||
{
|
||||
if (content == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return content.Replace("&", "&").Replace("'", "'").Replace("\"", """).Replace(">", ">").Replace("<", "<");
|
||||
}
|
||||
|
||||
public Folder GetPlaylistsFolder(Guid userId)
|
||||
{
|
||||
var typeName = "PlaylistsFolder";
|
||||
const string TypeName = "PlaylistsFolder";
|
||||
|
||||
return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, typeName, StringComparison.Ordinal)) ??
|
||||
_libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, typeName, StringComparison.Ordinal));
|
||||
return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal)) ??
|
||||
_libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.HttpServer;
|
||||
using MediaBrowser.Model.Services;
|
||||
|
@ -91,12 +92,22 @@ namespace Emby.Server.Implementations.Services
|
|||
{
|
||||
if (restPath.Path[0] != '/')
|
||||
{
|
||||
throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetMethodName()));
|
||||
throw new ArgumentException(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"Route '{0}' on '{1}' must start with a '/'",
|
||||
restPath.Path,
|
||||
restPath.RequestType.GetMethodName()));
|
||||
}
|
||||
|
||||
if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1)
|
||||
{
|
||||
throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. ", restPath.Path, restPath.RequestType.GetMethodName()));
|
||||
throw new ArgumentException(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"Route '{0}' on '{1}' contains invalid chars. ",
|
||||
restPath.Path,
|
||||
restPath.RequestType.GetMethodName()));
|
||||
}
|
||||
|
||||
if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch))
|
||||
|
@ -179,8 +190,7 @@ namespace Emby.Server.Implementations.Services
|
|||
|
||||
var service = httpHost.CreateInstance(serviceType);
|
||||
|
||||
var serviceRequiresContext = service as IRequiresRequest;
|
||||
if (serviceRequiresContext != null)
|
||||
if (service is IRequiresRequest serviceRequiresContext)
|
||||
{
|
||||
serviceRequiresContext.Request = req;
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.Services
|
|||
return null;
|
||||
}
|
||||
|
||||
public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, ILogger logger, CancellationToken cancellationToken)
|
||||
public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, CancellationToken cancellationToken)
|
||||
{
|
||||
httpReq.Items["__route"] = _restPath;
|
||||
|
||||
|
@ -80,10 +80,10 @@ namespace Emby.Server.Implementations.Services
|
|||
httpReq.ResponseContentType = _responseContentType;
|
||||
}
|
||||
|
||||
var request = await CreateRequest(httpHost, httpReq, _restPath, logger).ConfigureAwait(false);
|
||||
var request = await CreateRequest(httpHost, httpReq, _restPath).ConfigureAwait(false);
|
||||
|
||||
httpHost.ApplyRequestFilters(httpReq, httpRes, request);
|
||||
|
||||
|
||||
httpRes.HttpContext.SetServiceStackRequest(httpReq);
|
||||
var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false);
|
||||
|
||||
|
@ -96,7 +96,7 @@ namespace Emby.Server.Implementations.Services
|
|||
await ResponseHelper.WriteToResponse(httpRes, httpReq, response, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath, ILogger logger)
|
||||
public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath)
|
||||
{
|
||||
var requestType = restPath.RequestType;
|
||||
|
||||
|
|
|
@ -1,287 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Emby.Server.Implementations.HttpServer;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Services;
|
||||
|
||||
namespace Emby.Server.Implementations.Services
|
||||
{
|
||||
[Route("/swagger", "GET", Summary = "Gets the swagger specifications")]
|
||||
[Route("/swagger.json", "GET", Summary = "Gets the swagger specifications")]
|
||||
public class GetSwaggerSpec : IReturn<SwaggerSpec>
|
||||
{
|
||||
}
|
||||
|
||||
public class SwaggerSpec
|
||||
{
|
||||
public string swagger { get; set; }
|
||||
|
||||
public string[] schemes { get; set; }
|
||||
|
||||
public SwaggerInfo info { get; set; }
|
||||
|
||||
public string host { get; set; }
|
||||
|
||||
public string basePath { get; set; }
|
||||
|
||||
public SwaggerTag[] tags { get; set; }
|
||||
|
||||
public IDictionary<string, Dictionary<string, SwaggerMethod>> paths { get; set; }
|
||||
|
||||
public Dictionary<string, SwaggerDefinition> definitions { get; set; }
|
||||
|
||||
public SwaggerComponents components { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerComponents
|
||||
{
|
||||
public Dictionary<string, SwaggerSecurityScheme> securitySchemes { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerSecurityScheme
|
||||
{
|
||||
public string name { get; set; }
|
||||
|
||||
public string type { get; set; }
|
||||
|
||||
public string @in { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerInfo
|
||||
{
|
||||
public string description { get; set; }
|
||||
|
||||
public string version { get; set; }
|
||||
|
||||
public string title { get; set; }
|
||||
|
||||
public string termsOfService { get; set; }
|
||||
|
||||
public SwaggerConcactInfo contact { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerConcactInfo
|
||||
{
|
||||
public string email { get; set; }
|
||||
|
||||
public string name { get; set; }
|
||||
|
||||
public string url { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerTag
|
||||
{
|
||||
public string description { get; set; }
|
||||
|
||||
public string name { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerMethod
|
||||
{
|
||||
public string summary { get; set; }
|
||||
|
||||
public string description { get; set; }
|
||||
|
||||
public string[] tags { get; set; }
|
||||
|
||||
public string operationId { get; set; }
|
||||
|
||||
public string[] consumes { get; set; }
|
||||
|
||||
public string[] produces { get; set; }
|
||||
|
||||
public SwaggerParam[] parameters { get; set; }
|
||||
|
||||
public Dictionary<string, SwaggerResponse> responses { get; set; }
|
||||
|
||||
public Dictionary<string, string[]>[] security { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerParam
|
||||
{
|
||||
public string @in { get; set; }
|
||||
|
||||
public string name { get; set; }
|
||||
|
||||
public string description { get; set; }
|
||||
|
||||
public bool required { get; set; }
|
||||
|
||||
public string type { get; set; }
|
||||
|
||||
public string collectionFormat { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerResponse
|
||||
{
|
||||
public string description { get; set; }
|
||||
|
||||
// ex. "$ref":"#/definitions/Pet"
|
||||
public Dictionary<string, string> schema { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerDefinition
|
||||
{
|
||||
public string type { get; set; }
|
||||
|
||||
public Dictionary<string, SwaggerProperty> properties { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerProperty
|
||||
{
|
||||
public string type { get; set; }
|
||||
|
||||
public string format { get; set; }
|
||||
|
||||
public string description { get; set; }
|
||||
|
||||
public string[] @enum { get; set; }
|
||||
|
||||
public string @default { get; set; }
|
||||
}
|
||||
|
||||
public class SwaggerService : IService, IRequiresRequest
|
||||
{
|
||||
private readonly IHttpServer _httpServer;
|
||||
private SwaggerSpec _spec;
|
||||
|
||||
public IRequest Request { get; set; }
|
||||
|
||||
public SwaggerService(IHttpServer httpServer)
|
||||
{
|
||||
_httpServer = httpServer;
|
||||
}
|
||||
|
||||
public object Get(GetSwaggerSpec request)
|
||||
{
|
||||
return _spec ?? (_spec = GetSpec());
|
||||
}
|
||||
|
||||
private SwaggerSpec GetSpec()
|
||||
{
|
||||
string host = null;
|
||||
Uri uri;
|
||||
if (Uri.TryCreate(Request.RawUrl, UriKind.Absolute, out uri))
|
||||
{
|
||||
host = uri.Host;
|
||||
}
|
||||
|
||||
var securitySchemes = new Dictionary<string, SwaggerSecurityScheme>();
|
||||
|
||||
securitySchemes["api_key"] = new SwaggerSecurityScheme
|
||||
{
|
||||
name = "api_key",
|
||||
type = "apiKey",
|
||||
@in = "query"
|
||||
};
|
||||
|
||||
var spec = new SwaggerSpec
|
||||
{
|
||||
schemes = new[] { "http" },
|
||||
tags = GetTags(),
|
||||
swagger = "2.0",
|
||||
info = new SwaggerInfo
|
||||
{
|
||||
title = "Jellyfin Server API",
|
||||
version = "1.0.0",
|
||||
description = "Explore the Jellyfin Server API",
|
||||
contact = new SwaggerConcactInfo
|
||||
{
|
||||
name = "Jellyfin Community",
|
||||
url = "https://jellyfin.readthedocs.io/en/latest/user-docs/getting-help/"
|
||||
}
|
||||
},
|
||||
paths = GetPaths(),
|
||||
definitions = GetDefinitions(),
|
||||
basePath = "/jellyfin",
|
||||
host = host,
|
||||
|
||||
components = new SwaggerComponents
|
||||
{
|
||||
securitySchemes = securitySchemes
|
||||
}
|
||||
};
|
||||
|
||||
return spec;
|
||||
}
|
||||
|
||||
|
||||
private SwaggerTag[] GetTags()
|
||||
{
|
||||
return Array.Empty<SwaggerTag>();
|
||||
}
|
||||
|
||||
private Dictionary<string, SwaggerDefinition> GetDefinitions()
|
||||
{
|
||||
return new Dictionary<string, SwaggerDefinition>();
|
||||
}
|
||||
|
||||
private IDictionary<string, Dictionary<string, SwaggerMethod>> GetPaths()
|
||||
{
|
||||
var paths = new SortedDictionary<string, Dictionary<string, SwaggerMethod>>();
|
||||
|
||||
// REVIEW: this can be done better
|
||||
var all = ((HttpListenerHost)_httpServer).ServiceController.RestPathMap.OrderBy(i => i.Key, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
|
||||
foreach (var current in all)
|
||||
{
|
||||
foreach (var info in current.Value)
|
||||
{
|
||||
if (info.IsHidden)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (info.Path.StartsWith("/mediabrowser", StringComparison.OrdinalIgnoreCase)
|
||||
|| info.Path.StartsWith("/jellyfin", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
paths[info.Path] = GetPathInfo(info);
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
private Dictionary<string, SwaggerMethod> GetPathInfo(RestPath info)
|
||||
{
|
||||
var result = new Dictionary<string, SwaggerMethod>();
|
||||
|
||||
foreach (var verb in info.Verbs)
|
||||
{
|
||||
var responses = new Dictionary<string, SwaggerResponse>
|
||||
{
|
||||
{ "200", new SwaggerResponse { description = "OK" } }
|
||||
};
|
||||
|
||||
var apiKeySecurity = new Dictionary<string, string[]>
|
||||
{
|
||||
{ "api_key", Array.Empty<string>() }
|
||||
};
|
||||
|
||||
result[verb.ToLowerInvariant()] = new SwaggerMethod
|
||||
{
|
||||
summary = info.Summary,
|
||||
description = info.Description,
|
||||
produces = new[] { "application/json" },
|
||||
consumes = new[] { "application/json" },
|
||||
operationId = info.RequestType.Name,
|
||||
tags = Array.Empty<string>(),
|
||||
|
||||
parameters = Array.Empty<SwaggerParam>(),
|
||||
|
||||
responses = responses,
|
||||
|
||||
security = new[] { apiKeySecurity }
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -848,8 +848,8 @@ namespace Emby.Server.Implementations.Session
|
|||
/// </summary>
|
||||
/// <param name="info">The info.</param>
|
||||
/// <returns>Task.</returns>
|
||||
/// <exception cref="ArgumentNullException">info</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">positionTicks</exception>
|
||||
/// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException"><c>info.PositionTicks</c> is <c>null</c> or negative.</exception>
|
||||
public async Task OnPlaybackStopped(PlaybackStopInfo info)
|
||||
{
|
||||
CheckDisposed();
|
||||
|
|
|
@ -93,7 +93,7 @@ namespace Emby.Server.Implementations.Session
|
|||
if (session != null)
|
||||
{
|
||||
EnsureController(session, e.Argument);
|
||||
await KeepAliveWebSocket(e.Argument);
|
||||
await KeepAliveWebSocket(e.Argument).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -177,7 +177,7 @@ namespace Emby.Server.Implementations.Session
|
|||
// Notify WebSocket about timeout
|
||||
try
|
||||
{
|
||||
await SendForceKeepAlive(webSocket);
|
||||
await SendForceKeepAlive(webSocket).ConfigureAwait(false);
|
||||
}
|
||||
catch (WebSocketException exception)
|
||||
{
|
||||
|
@ -233,6 +233,7 @@ namespace Emby.Server.Implementations.Session
|
|||
if (_keepAliveCancellationToken != null)
|
||||
{
|
||||
_keepAliveCancellationToken.Cancel();
|
||||
_keepAliveCancellationToken.Dispose();
|
||||
_keepAliveCancellationToken = null;
|
||||
}
|
||||
}
|
||||
|
@ -268,7 +269,7 @@ namespace Emby.Server.Implementations.Session
|
|||
lost = _webSockets.Where(i => (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds >= WebSocketLostTimeout).ToList();
|
||||
}
|
||||
|
||||
if (inactive.Any())
|
||||
if (inactive.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count);
|
||||
}
|
||||
|
@ -277,7 +278,7 @@ namespace Emby.Server.Implementations.Session
|
|||
{
|
||||
try
|
||||
{
|
||||
await SendForceKeepAlive(webSocket);
|
||||
await SendForceKeepAlive(webSocket).ConfigureAwait(false);
|
||||
}
|
||||
catch (WebSocketException exception)
|
||||
{
|
||||
|
@ -288,7 +289,7 @@ namespace Emby.Server.Implementations.Session
|
|||
|
||||
lock (_webSocketsLock)
|
||||
{
|
||||
if (lost.Any())
|
||||
if (lost.Count > 0)
|
||||
{
|
||||
_logger.LogInformation("Lost {0} WebSockets.", lost.Count);
|
||||
foreach (var webSocket in lost)
|
||||
|
@ -298,7 +299,7 @@ namespace Emby.Server.Implementations.Session
|
|||
}
|
||||
}
|
||||
|
||||
if (!_webSockets.Any())
|
||||
if (_webSockets.Count == 0)
|
||||
{
|
||||
StopKeepAlive();
|
||||
}
|
||||
|
@ -312,11 +313,13 @@ namespace Emby.Server.Implementations.Session
|
|||
/// <returns>Task.</returns>
|
||||
private Task SendForceKeepAlive(IWebSocketConnection webSocket)
|
||||
{
|
||||
return webSocket.SendAsync(new WebSocketMessage<int>
|
||||
{
|
||||
MessageType = "ForceKeepAlive",
|
||||
Data = WebSocketLostTimeout
|
||||
}, CancellationToken.None);
|
||||
return webSocket.SendAsync(
|
||||
new WebSocketMessage<int>
|
||||
{
|
||||
MessageType = "ForceKeepAlive",
|
||||
Data = WebSocketLostTimeout
|
||||
},
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -330,12 +333,11 @@ namespace Emby.Server.Implementations.Session
|
|||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await callback();
|
||||
Task task = Task.Delay(interval, cancellationToken);
|
||||
await callback().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
await task;
|
||||
await Task.Delay(interval, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
|
|
|
@ -154,8 +154,8 @@ namespace Emby.Server.Implementations.Sorting
|
|||
|
||||
private static int CompareEpisodes(Episode x, Episode y)
|
||||
{
|
||||
var xValue = (x.ParentIndexNumber ?? -1) * 1000 + (x.IndexNumber ?? -1);
|
||||
var yValue = (y.ParentIndexNumber ?? -1) * 1000 + (y.IndexNumber ?? -1);
|
||||
var xValue = ((x.ParentIndexNumber ?? -1) * 1000) + (x.IndexNumber ?? -1);
|
||||
var yValue = ((y.ParentIndexNumber ?? -1) * 1000) + (y.IndexNumber ?? -1);
|
||||
|
||||
return xValue.CompareTo(yValue);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -27,14 +28,17 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
/// All sessions will receive the message.
|
||||
/// </summary>
|
||||
AllGroup = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Only the specified session will receive the message.
|
||||
/// </summary>
|
||||
CurrentSession = 1,
|
||||
|
||||
/// <summary>
|
||||
/// All sessions, except the current one, will receive the message.
|
||||
/// </summary>
|
||||
AllExceptCurrentSession = 2,
|
||||
|
||||
/// <summary>
|
||||
/// Only sessions that are not buffering will receive the message.
|
||||
/// </summary>
|
||||
|
@ -56,15 +60,6 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
/// </summary>
|
||||
private readonly GroupInfo _group = new GroupInfo();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid GetGroupId() => _group.GroupId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid GetPlayingItemId() => _group.PlayingItem.Id;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsGroupEmpty() => _group.IsEmpty();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SyncPlayController" /> class.
|
||||
/// </summary>
|
||||
|
@ -78,6 +73,15 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
_syncPlayManager = syncPlayManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid GetGroupId() => _group.GroupId;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid GetPlayingItemId() => _group.PlayingItem.Id;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsGroupEmpty() => _group.IsEmpty();
|
||||
|
||||
/// <summary>
|
||||
/// Converts DateTime to UTC string.
|
||||
/// </summary>
|
||||
|
@ -85,7 +89,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
/// <value>The UTC string.</value>
|
||||
private string DateToUTCString(DateTime date)
|
||||
{
|
||||
return date.ToUniversalTime().ToString("o");
|
||||
return date.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -94,23 +98,23 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
/// <param name="from">The current session.</param>
|
||||
/// <param name="type">The filtering type.</param>
|
||||
/// <value>The array of sessions matching the filter.</value>
|
||||
private SessionInfo[] FilterSessions(SessionInfo from, BroadcastType type)
|
||||
private IEnumerable<SessionInfo> FilterSessions(SessionInfo from, BroadcastType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case BroadcastType.CurrentSession:
|
||||
return new SessionInfo[] { from };
|
||||
case BroadcastType.AllGroup:
|
||||
return _group.Participants.Values.Select(
|
||||
session => session.Session).ToArray();
|
||||
return _group.Participants.Values
|
||||
.Select(session => session.Session);
|
||||
case BroadcastType.AllExceptCurrentSession:
|
||||
return _group.Participants.Values.Select(
|
||||
session => session.Session).Where(
|
||||
session => !session.Id.Equals(from.Id)).ToArray();
|
||||
return _group.Participants.Values
|
||||
.Select(session => session.Session)
|
||||
.Where(session => !session.Id.Equals(from.Id, StringComparison.Ordinal));
|
||||
case BroadcastType.AllReady:
|
||||
return _group.Participants.Values.Where(
|
||||
session => !session.IsBuffering).Select(
|
||||
session => session.Session).ToArray();
|
||||
return _group.Participants.Values
|
||||
.Where(session => !session.IsBuffering)
|
||||
.Select(session => session.Session);
|
||||
default:
|
||||
return Array.Empty<SessionInfo>();
|
||||
}
|
||||
|
@ -128,10 +132,9 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
{
|
||||
IEnumerable<Task> GetTasks()
|
||||
{
|
||||
SessionInfo[] sessions = FilterSessions(from, type);
|
||||
foreach (var session in sessions)
|
||||
foreach (var session in FilterSessions(from, type))
|
||||
{
|
||||
yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), message, cancellationToken);
|
||||
yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id, message, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -150,10 +153,9 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
{
|
||||
IEnumerable<Task> GetTasks()
|
||||
{
|
||||
SessionInfo[] sessions = FilterSessions(from, type);
|
||||
foreach (var session in sessions)
|
||||
foreach (var session in FilterSessions(from, type))
|
||||
{
|
||||
yield return _sessionManager.SendSyncPlayCommand(session.Id.ToString(), message, cancellationToken);
|
||||
yield return _sessionManager.SendSyncPlayCommand(session.Id, message, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -236,9 +238,11 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||
}
|
||||
else
|
||||
{
|
||||
var playRequest = new PlayRequest();
|
||||
playRequest.ItemIds = new Guid[] { _group.PlayingItem.Id };
|
||||
playRequest.StartPositionTicks = _group.PositionTicks;
|
||||
var playRequest = new PlayRequest
|
||||
{
|
||||
ItemIds = new Guid[] { _group.PlayingItem.Id },
|
||||
StartPositionTicks = _group.PositionTicks
|
||||
};
|
||||
var update = NewSyncPlayGroupUpdate(GroupUpdateType.PrepareSession, playRequest);
|
||||
SendGroupUpdate(session, BroadcastType.CurrentSession, update, cancellationToken);
|
||||
}
|
||||
|
|
35
Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs
Normal file
35
Jellyfin.Api/Attributes/HttpSubscribeAttribute.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
|
||||
namespace Jellyfin.Api.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies an action that supports the HTTP GET method.
|
||||
/// </summary>
|
||||
public class HttpSubscribeAttribute : HttpMethodAttribute
|
||||
{
|
||||
private static readonly IEnumerable<string> _supportedMethods = new[] { "SUBSCRIBE" };
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class.
|
||||
/// </summary>
|
||||
public HttpSubscribeAttribute()
|
||||
: base(_supportedMethods)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="template">The route template. May not be null.</param>
|
||||
public HttpSubscribeAttribute(string template)
|
||||
: base(_supportedMethods, template)
|
||||
{
|
||||
if (template == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(template));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs
Normal file
35
Jellyfin.Api/Attributes/HttpUnsubscribeAttribute.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc.Routing;
|
||||
|
||||
namespace Jellyfin.Api.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifies an action that supports the HTTP GET method.
|
||||
/// </summary>
|
||||
public class HttpUnsubscribeAttribute : HttpMethodAttribute
|
||||
{
|
||||
private static readonly IEnumerable<string> _supportedMethods = new[] { "UNSUBSCRIBE" };
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class.
|
||||
/// </summary>
|
||||
public HttpUnsubscribeAttribute()
|
||||
: base(_supportedMethods)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="template">The route template. May not be null.</param>
|
||||
public HttpUnsubscribeAttribute(string template)
|
||||
: base(_supportedMethods, template)
|
||||
{
|
||||
if (template == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(template));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
103
Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
Normal file
103
Jellyfin.Api/Auth/BaseAuthorizationHandler.cs
Normal file
|
@ -0,0 +1,103 @@
|
|||
using System.Security.Claims;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Jellyfin.Api.Auth
|
||||
{
|
||||
/// <summary>
|
||||
/// Base authorization handler.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of Authorization Requirement.</typeparam>
|
||||
public abstract class BaseAuthorizationHandler<T> : AuthorizationHandler<T>
|
||||
where T : IAuthorizationRequirement
|
||||
{
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BaseAuthorizationHandler{T}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||
protected BaseAuthorizationHandler(
|
||||
IUserManager userManager,
|
||||
INetworkManager networkManager,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_networkManager = networkManager;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate authenticated claims.
|
||||
/// </summary>
|
||||
/// <param name="claimsPrincipal">Request claims.</param>
|
||||
/// <param name="ignoreSchedule">Whether to ignore parental control.</param>
|
||||
/// <param name="localAccessOnly">Whether access is to be allowed locally only.</param>
|
||||
/// <param name="requiredDownloadPermission">Whether validation requires download permission.</param>
|
||||
/// <returns>Validated claim status.</returns>
|
||||
protected bool ValidateClaims(
|
||||
ClaimsPrincipal claimsPrincipal,
|
||||
bool ignoreSchedule = false,
|
||||
bool localAccessOnly = false,
|
||||
bool requiredDownloadPermission = false)
|
||||
{
|
||||
// Ensure claim has userId.
|
||||
var userId = ClaimHelpers.GetUserId(claimsPrincipal);
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure userId links to a valid user.
|
||||
var user = _userManager.GetUserById(userId.Value);
|
||||
if (user == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure user is not disabled.
|
||||
if (user.HasPermission(PermissionKind.IsDisabled))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ip = RequestHelpers.NormalizeIp(_httpContextAccessor.HttpContext.Connection.RemoteIpAddress).ToString();
|
||||
var isInLocalNetwork = _networkManager.IsInLocalNetwork(ip);
|
||||
// User cannot access remotely and user is remote
|
||||
if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !isInLocalNetwork)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (localAccessOnly && !isInLocalNetwork)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// User attempting to access out of parental control hours.
|
||||
if (!ignoreSchedule
|
||||
&& !user.HasPermission(PermissionKind.IsAdministrator)
|
||||
&& !user.IsParentalScheduleAllowed())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// User attempting to download without permission.
|
||||
if (requiredDownloadPermission
|
||||
&& !user.HasPermission(PermissionKind.EnableContentDownloading))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
using System.Globalization;
|
||||
using System.Security.Authentication;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
|
@ -44,14 +45,24 @@ namespace Jellyfin.Api.Auth
|
|||
var authorizationInfo = _authService.Authenticate(Request);
|
||||
if (authorizationInfo == null)
|
||||
{
|
||||
return Task.FromResult(AuthenticateResult.Fail("Invalid user"));
|
||||
return Task.FromResult(AuthenticateResult.NoResult());
|
||||
// TODO return when legacy API is removed.
|
||||
// Don't spam the log with "Invalid User"
|
||||
// return Task.FromResult(AuthenticateResult.Fail("Invalid user"));
|
||||
}
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(ClaimTypes.Name, authorizationInfo.User.Username),
|
||||
new Claim(ClaimTypes.Role, authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User)
|
||||
new Claim(ClaimTypes.Role, authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User),
|
||||
new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)),
|
||||
new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId),
|
||||
new Claim(InternalClaimTypes.Device, authorizationInfo.Device),
|
||||
new Claim(InternalClaimTypes.Client, authorizationInfo.Client),
|
||||
new Claim(InternalClaimTypes.Version, authorizationInfo.Version),
|
||||
new Claim(InternalClaimTypes.Token, authorizationInfo.Token),
|
||||
};
|
||||
|
||||
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Default authorization handler.
|
||||
/// </summary>
|
||||
public class DefaultAuthorizationHandler : BaseAuthorizationHandler<DefaultAuthorizationRequirement>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DefaultAuthorizationHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||
public DefaultAuthorizationHandler(
|
||||
IUserManager userManager,
|
||||
INetworkManager networkManager,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
: base(userManager, networkManager, httpContextAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement)
|
||||
{
|
||||
var validated = ValidateClaims(context.User);
|
||||
if (!validated)
|
||||
{
|
||||
context.Fail();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
context.Succeed(requirement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// The default authorization requirement.
|
||||
/// </summary>
|
||||
public class DefaultAuthorizationRequirement : IAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
}
|
44
Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs
Normal file
44
Jellyfin.Api/Auth/DownloadPolicy/DownloadHandler.cs
Normal file
|
@ -0,0 +1,44 @@
|
|||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Jellyfin.Api.Auth.DownloadPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Download authorization handler.
|
||||
/// </summary>
|
||||
public class DownloadHandler : BaseAuthorizationHandler<DownloadRequirement>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DownloadHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||
public DownloadHandler(
|
||||
IUserManager userManager,
|
||||
INetworkManager networkManager,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
: base(userManager, networkManager, httpContextAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DownloadRequirement requirement)
|
||||
{
|
||||
var validated = ValidateClaims(context.User);
|
||||
if (validated)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Fail();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
11
Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs
Normal file
11
Jellyfin.Api/Auth/DownloadPolicy/DownloadRequirement.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Jellyfin.Api.Auth.DownloadPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// The download permission requirement.
|
||||
/// </summary>
|
||||
public class DownloadRequirement : IAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Ignore parental control schedule and allow before startup wizard has been completed.
|
||||
/// </summary>
|
||||
public class FirstTimeOrIgnoreParentalControlSetupHandler : BaseAuthorizationHandler<FirstTimeOrIgnoreParentalControlSetupRequirement>
|
||||
{
|
||||
private readonly IConfigurationManager _configurationManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FirstTimeOrIgnoreParentalControlSetupHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||
public FirstTimeOrIgnoreParentalControlSetupHandler(
|
||||
IUserManager userManager,
|
||||
INetworkManager networkManager,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IConfigurationManager configurationManager)
|
||||
: base(userManager, networkManager, httpContextAccessor)
|
||||
{
|
||||
_configurationManager = configurationManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeOrIgnoreParentalControlSetupRequirement requirement)
|
||||
{
|
||||
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var validated = ValidateClaims(context.User, ignoreSchedule: true);
|
||||
if (validated)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Fail();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// First time setup or ignore parental controls requirement.
|
||||
/// </summary>
|
||||
public class FirstTimeOrIgnoreParentalControlSetupRequirement : IAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Authorization handler for requiring first time setup or default privileges.
|
||||
/// </summary>
|
||||
public class FirstTimeSetupOrDefaultHandler : BaseAuthorizationHandler<FirstTimeSetupOrDefaultRequirement>
|
||||
{
|
||||
private readonly IConfigurationManager _configurationManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FirstTimeSetupOrDefaultHandler" /> class.
|
||||
/// </summary>
|
||||
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||
public FirstTimeSetupOrDefaultHandler(
|
||||
IConfigurationManager configurationManager,
|
||||
IUserManager userManager,
|
||||
INetworkManager networkManager,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
: base(userManager, networkManager, httpContextAccessor)
|
||||
{
|
||||
_configurationManager = configurationManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement firstTimeSetupOrDefaultRequirement)
|
||||
{
|
||||
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
|
||||
{
|
||||
context.Succeed(firstTimeSetupOrDefaultRequirement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var validated = ValidateClaims(context.User);
|
||||
if (validated)
|
||||
{
|
||||
context.Succeed(firstTimeSetupOrDefaultRequirement);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Fail();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// The authorization requirement, requiring incomplete first time setup or default privileges, for the authorization handler.
|
||||
/// </summary>
|
||||
public class FirstTimeSetupOrDefaultRequirement : IAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,22 +1,33 @@
|
|||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Authorization handler for requiring first time setup or elevated privileges.
|
||||
/// </summary>
|
||||
public class FirstTimeSetupOrElevatedHandler : AuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
|
||||
public class FirstTimeSetupOrElevatedHandler : BaseAuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
|
||||
{
|
||||
private readonly IConfigurationManager _configurationManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FirstTimeSetupOrElevatedHandler" /> class.
|
||||
/// </summary>
|
||||
/// <param name="configurationManager">The jellyfin configuration manager.</param>
|
||||
public FirstTimeSetupOrElevatedHandler(IConfigurationManager configurationManager)
|
||||
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||
public FirstTimeSetupOrElevatedHandler(
|
||||
IConfigurationManager configurationManager,
|
||||
IUserManager userManager,
|
||||
INetworkManager networkManager,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
: base(userManager, networkManager, httpContextAccessor)
|
||||
{
|
||||
_configurationManager = configurationManager;
|
||||
}
|
||||
|
@ -27,8 +38,11 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
|
|||
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
|
||||
{
|
||||
context.Succeed(firstTimeSetupOrElevatedRequirement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
else if (context.User.IsInRole(UserRoles.Administrator))
|
||||
|
||||
var validated = ValidateClaims(context.User);
|
||||
if (validated && context.User.IsInRole(UserRoles.Administrator))
|
||||
{
|
||||
context.Succeed(firstTimeSetupOrElevatedRequirement);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Escape schedule controls handler.
|
||||
/// </summary>
|
||||
public class IgnoreParentalControlHandler : BaseAuthorizationHandler<IgnoreParentalControlRequirement>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IgnoreParentalControlHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||
public IgnoreParentalControlHandler(
|
||||
IUserManager userManager,
|
||||
INetworkManager networkManager,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
: base(userManager, networkManager, httpContextAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement)
|
||||
{
|
||||
var validated = ValidateClaims(context.User, ignoreSchedule: true);
|
||||
if (!validated)
|
||||
{
|
||||
context.Fail();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
context.Succeed(requirement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Escape schedule controls requirement.
|
||||
/// </summary>
|
||||
public class IgnoreParentalControlRequirement : IAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Local access or require elevated privileges handler.
|
||||
/// </summary>
|
||||
public class LocalAccessOrRequiresElevationHandler : BaseAuthorizationHandler<LocalAccessOrRequiresElevationRequirement>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LocalAccessOrRequiresElevationHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||
public LocalAccessOrRequiresElevationHandler(
|
||||
IUserManager userManager,
|
||||
INetworkManager networkManager,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
: base(userManager, networkManager, httpContextAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessOrRequiresElevationRequirement requirement)
|
||||
{
|
||||
var validated = ValidateClaims(context.User, localAccessOnly: true);
|
||||
if (validated || context.User.IsInRole(UserRoles.Administrator))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Fail();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// The local access or elevated privileges authorization requirement.
|
||||
/// </summary>
|
||||
public class LocalAccessOrRequiresElevationRequirement : IAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
}
|
44
Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
Normal file
44
Jellyfin.Api/Auth/LocalAccessPolicy/LocalAccessHandler.cs
Normal file
|
@ -0,0 +1,44 @@
|
|||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Jellyfin.Api.Auth.LocalAccessPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Local access handler.
|
||||
/// </summary>
|
||||
public class LocalAccessHandler : BaseAuthorizationHandler<LocalAccessRequirement>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LocalAccessHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||
public LocalAccessHandler(
|
||||
IUserManager userManager,
|
||||
INetworkManager networkManager,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
: base(userManager, networkManager, httpContextAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessRequirement requirement)
|
||||
{
|
||||
var validated = ValidateClaims(context.User, localAccessOnly: true);
|
||||
if (!validated)
|
||||
{
|
||||
context.Fail();
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Jellyfin.Api.Auth.LocalAccessPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// The local access authorization requirement.
|
||||
/// </summary>
|
||||
public class LocalAccessRequirement : IAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,21 +1,43 @@
|
|||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Jellyfin.Api.Auth.RequiresElevationPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Authorization handler for requiring elevated privileges.
|
||||
/// </summary>
|
||||
public class RequiresElevationHandler : AuthorizationHandler<RequiresElevationRequirement>
|
||||
public class RequiresElevationHandler : BaseAuthorizationHandler<RequiresElevationRequirement>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RequiresElevationHandler"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||
public RequiresElevationHandler(
|
||||
IUserManager userManager,
|
||||
INetworkManager networkManager,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
: base(userManager, networkManager, httpContextAccessor)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequiresElevationRequirement requirement)
|
||||
{
|
||||
if (context.User.IsInRole(UserRoles.Administrator))
|
||||
var validated = ValidateClaims(context.User);
|
||||
if (validated && context.User.IsInRole(UserRoles.Administrator))
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Fail();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
|
38
Jellyfin.Api/Constants/InternalClaimTypes.cs
Normal file
38
Jellyfin.Api/Constants/InternalClaimTypes.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
namespace Jellyfin.Api.Constants
|
||||
{
|
||||
/// <summary>
|
||||
/// Internal claim types for authorization.
|
||||
/// </summary>
|
||||
public static class InternalClaimTypes
|
||||
{
|
||||
/// <summary>
|
||||
/// User Id.
|
||||
/// </summary>
|
||||
public const string UserId = "Jellyfin-UserId";
|
||||
|
||||
/// <summary>
|
||||
/// Device Id.
|
||||
/// </summary>
|
||||
public const string DeviceId = "Jellyfin-DeviceId";
|
||||
|
||||
/// <summary>
|
||||
/// Device.
|
||||
/// </summary>
|
||||
public const string Device = "Jellyfin-Device";
|
||||
|
||||
/// <summary>
|
||||
/// Client.
|
||||
/// </summary>
|
||||
public const string Client = "Jellyfin-Client";
|
||||
|
||||
/// <summary>
|
||||
/// Version.
|
||||
/// </summary>
|
||||
public const string Version = "Jellyfin-Version";
|
||||
|
||||
/// <summary>
|
||||
/// Token.
|
||||
/// </summary>
|
||||
public const string Token = "Jellyfin-Token";
|
||||
}
|
||||
}
|
|
@ -5,14 +5,49 @@ namespace Jellyfin.Api.Constants
|
|||
/// </summary>
|
||||
public static class Policies
|
||||
{
|
||||
/// <summary>
|
||||
/// Policy name for default authorization.
|
||||
/// </summary>
|
||||
public const string DefaultAuthorization = "DefaultAuthorization";
|
||||
|
||||
/// <summary>
|
||||
/// Policy name for requiring first time setup or elevated privileges.
|
||||
/// </summary>
|
||||
public const string FirstTimeSetupOrElevated = "FirstTimeOrElevated";
|
||||
public const string FirstTimeSetupOrElevated = "FirstTimeSetupOrElevated";
|
||||
|
||||
/// <summary>
|
||||
/// Policy name for requiring elevated privileges.
|
||||
/// </summary>
|
||||
public const string RequiresElevation = "RequiresElevation";
|
||||
|
||||
/// <summary>
|
||||
/// Policy name for allowing local access only.
|
||||
/// </summary>
|
||||
public const string LocalAccessOnly = "LocalAccessOnly";
|
||||
|
||||
/// <summary>
|
||||
/// Policy name for escaping schedule controls.
|
||||
/// </summary>
|
||||
public const string IgnoreParentalControl = "IgnoreParentalControl";
|
||||
|
||||
/// <summary>
|
||||
/// Policy name for requiring download permission.
|
||||
/// </summary>
|
||||
public const string Download = "Download";
|
||||
|
||||
/// <summary>
|
||||
/// Policy name for requiring first time setup or default permissions.
|
||||
/// </summary>
|
||||
public const string FirstTimeSetupOrDefault = "FirstTimeSetupOrDefault";
|
||||
|
||||
/// <summary>
|
||||
/// Policy name for requiring local access or elevated privileges.
|
||||
/// </summary>
|
||||
public const string LocalAccessOrRequiresElevation = "LocalAccessOrRequiresElevation";
|
||||
|
||||
/// <summary>
|
||||
/// Policy name for escaping schedule controls or requiring first time setup.
|
||||
/// </summary>
|
||||
public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl";
|
||||
}
|
||||
}
|
||||
|
|
57
Jellyfin.Api/Controllers/ActivityLogController.cs
Normal file
57
Jellyfin.Api/Controllers/ActivityLogController.cs
Normal file
|
@ -0,0 +1,57 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Model.Activity;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Activity log controller.
|
||||
/// </summary>
|
||||
[Route("System/ActivityLog")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
public class ActivityLogController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IActivityManager _activityManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ActivityLogController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="activityManager">Instance of <see cref="IActivityManager"/> interface.</param>
|
||||
public ActivityLogController(IActivityManager activityManager)
|
||||
{
|
||||
_activityManager = activityManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets activity log entries.
|
||||
/// </summary>
|
||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="minDate">Optional. The minimum date. Format = ISO.</param>
|
||||
/// <param name="hasUserId">Optional. Filter log entries if it has user id, or not.</param>
|
||||
/// <response code="200">Activity log returned.</response>
|
||||
/// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns>
|
||||
[HttpGet("Entries")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<ActivityLogEntry>> GetLogEntries(
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] DateTime? minDate,
|
||||
[FromQuery] bool? hasUserId)
|
||||
{
|
||||
var filterFunc = new Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>>(
|
||||
entries => entries.Where(entry => entry.DateCreated >= minDate
|
||||
&& (!hasUserId.HasValue || (hasUserId.Value
|
||||
? entry.UserId != Guid.Empty
|
||||
: entry.UserId == Guid.Empty))));
|
||||
|
||||
return _activityManager.GetPagedResult(filterFunc, startIndex, limit);
|
||||
}
|
||||
}
|
||||
}
|
134
Jellyfin.Api/Controllers/AlbumsController.cs
Normal file
134
Jellyfin.Api/Controllers/AlbumsController.cs
Normal file
|
@ -0,0 +1,134 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// The albums controller.
|
||||
/// </summary>
|
||||
[Route("")]
|
||||
public class AlbumsController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IDtoService _dtoService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AlbumsController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
||||
public AlbumsController(
|
||||
IUserManager userManager,
|
||||
ILibraryManager libraryManager,
|
||||
IDtoService dtoService)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_libraryManager = libraryManager;
|
||||
_dtoService = dtoService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds albums similar to a given album.
|
||||
/// </summary>
|
||||
/// <param name="albumId">The album id.</param>
|
||||
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||
/// <param name="excludeArtistIds">Optional. Ids of artists to exclude.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <response code="200">Similar albums returned.</response>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with similar albums.</returns>
|
||||
[HttpGet("Albums/{albumId}/Similar")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetSimilarAlbums(
|
||||
[FromRoute] string albumId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? excludeArtistIds,
|
||||
[FromQuery] int? limit)
|
||||
{
|
||||
var dtoOptions = new DtoOptions().AddClientFields(Request);
|
||||
|
||||
return SimilarItemsHelper.GetSimilarItemsResult(
|
||||
dtoOptions,
|
||||
_userManager,
|
||||
_libraryManager,
|
||||
_dtoService,
|
||||
userId,
|
||||
albumId,
|
||||
excludeArtistIds,
|
||||
limit,
|
||||
new[] { typeof(MusicAlbum) },
|
||||
GetAlbumSimilarityScore);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds artists similar to a given artist.
|
||||
/// </summary>
|
||||
/// <param name="artistId">The artist id.</param>
|
||||
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||
/// <param name="excludeArtistIds">Optional. Ids of artists to exclude.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <response code="200">Similar artists returned.</response>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with similar artists.</returns>
|
||||
[HttpGet("Artists/{artistId}/Similar")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetSimilarArtists(
|
||||
[FromRoute] string artistId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? excludeArtistIds,
|
||||
[FromQuery] int? limit)
|
||||
{
|
||||
var dtoOptions = new DtoOptions().AddClientFields(Request);
|
||||
|
||||
return SimilarItemsHelper.GetSimilarItemsResult(
|
||||
dtoOptions,
|
||||
_userManager,
|
||||
_libraryManager,
|
||||
_dtoService,
|
||||
userId,
|
||||
artistId,
|
||||
excludeArtistIds,
|
||||
limit,
|
||||
new[] { typeof(MusicArtist) },
|
||||
SimilarItemsHelper.GetSimiliarityScore);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a similairty score of two albums.
|
||||
/// </summary>
|
||||
/// <param name="item1">The first item.</param>
|
||||
/// <param name="item1People">The item1 people.</param>
|
||||
/// <param name="allPeople">All people.</param>
|
||||
/// <param name="item2">The second item.</param>
|
||||
/// <returns>System.Int32.</returns>
|
||||
private int GetAlbumSimilarityScore(BaseItem item1, List<PersonInfo> item1People, List<PersonInfo> allPeople, BaseItem item2)
|
||||
{
|
||||
var points = SimilarItemsHelper.GetSimiliarityScore(item1, item1People, allPeople, item2);
|
||||
|
||||
var album1 = (MusicAlbum)item1;
|
||||
var album2 = (MusicAlbum)item2;
|
||||
|
||||
var artists1 = album1
|
||||
.GetAllArtists()
|
||||
.DistinctNames()
|
||||
.ToList();
|
||||
|
||||
var artists2 = new HashSet<string>(
|
||||
album2.GetAllArtists().DistinctNames(),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
return points + artists1.Where(artists2.Contains).Sum(i => 5);
|
||||
}
|
||||
}
|
||||
}
|
97
Jellyfin.Api/Controllers/ApiKeyController.cs
Normal file
97
Jellyfin.Api/Controllers/ApiKeyController.cs
Normal file
|
@ -0,0 +1,97 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Security;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Authentication controller.
|
||||
/// </summary>
|
||||
[Route("Auth")]
|
||||
public class ApiKeyController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IAuthenticationRepository _authRepo;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ApiKeyController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
|
||||
/// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
|
||||
/// <param name="authRepo">Instance of <see cref="IAuthenticationRepository"/> interface.</param>
|
||||
public ApiKeyController(
|
||||
ISessionManager sessionManager,
|
||||
IServerApplicationHost appHost,
|
||||
IAuthenticationRepository authRepo)
|
||||
{
|
||||
_sessionManager = sessionManager;
|
||||
_appHost = appHost;
|
||||
_authRepo = authRepo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all keys.
|
||||
/// </summary>
|
||||
/// <response code="200">Api keys retrieved.</response>
|
||||
/// <returns>A <see cref="QueryResult{AuthenticationInfo}"/> with all keys.</returns>
|
||||
[HttpGet("Keys")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<AuthenticationInfo>> GetKeys()
|
||||
{
|
||||
var result = _authRepo.Get(new AuthenticationInfoQuery
|
||||
{
|
||||
HasUser = false
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new api key.
|
||||
/// </summary>
|
||||
/// <param name="app">Name of the app using the authentication key.</param>
|
||||
/// <response code="204">Api key created.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Keys")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult CreateKey([FromQuery, Required] string? app)
|
||||
{
|
||||
_authRepo.Create(new AuthenticationInfo
|
||||
{
|
||||
AppName = app,
|
||||
AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
|
||||
DateCreated = DateTime.UtcNow,
|
||||
DeviceId = _appHost.SystemId,
|
||||
DeviceName = _appHost.FriendlyName,
|
||||
AppVersion = _appHost.ApplicationVersionString
|
||||
});
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove an api key.
|
||||
/// </summary>
|
||||
/// <param name="key">The access token to delete.</param>
|
||||
/// <response code="204">Api key deleted.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpDelete("Keys/{key}")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult RevokeKey([FromRoute, Required] string? key)
|
||||
{
|
||||
_sessionManager.RevokeToken(key);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
488
Jellyfin.Api/Controllers/ArtistsController.cs
Normal file
488
Jellyfin.Api/Controllers/ArtistsController.cs
Normal file
|
@ -0,0 +1,488 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// The artists controller.
|
||||
/// </summary>
|
||||
[Route("Artists")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
public class ArtistsController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IDtoService _dtoService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ArtistsController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
||||
public ArtistsController(
|
||||
ILibraryManager libraryManager,
|
||||
IUserManager userManager,
|
||||
IDtoService dtoService)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
_dtoService = dtoService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all artists from a given item, folder, or the entire library.
|
||||
/// </summary>
|
||||
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
|
||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="searchTerm">Optional. Search term.</param>
|
||||
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
|
||||
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
|
||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
|
||||
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
|
||||
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
|
||||
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
|
||||
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
|
||||
/// <param name="enableUserData">Optional, include user data.</param>
|
||||
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
|
||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
|
||||
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param>
|
||||
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
|
||||
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="userId">User id.</param>
|
||||
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
|
||||
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
|
||||
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
|
||||
/// <param name="enableImages">Optional, include image information in output.</param>
|
||||
/// <param name="enableTotalRecordCount">Total record count.</param>
|
||||
/// <response code="200">Artists returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the artists.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetArtists(
|
||||
[FromQuery] double? minCommunityRating,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? searchTerm,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery] string? filters,
|
||||
[FromQuery] bool? isFavorite,
|
||||
[FromQuery] string? mediaTypes,
|
||||
[FromQuery] string? genres,
|
||||
[FromQuery] string? genreIds,
|
||||
[FromQuery] string? officialRatings,
|
||||
[FromQuery] string? tags,
|
||||
[FromQuery] string? years,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes,
|
||||
[FromQuery] string? person,
|
||||
[FromQuery] string? personIds,
|
||||
[FromQuery] string? personTypes,
|
||||
[FromQuery] string? studios,
|
||||
[FromQuery] string? studioIds,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
[FromQuery] string? nameLessThan,
|
||||
[FromQuery] bool? enableImages = true,
|
||||
[FromQuery] bool enableTotalRecordCount = true)
|
||||
{
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
User? user = null;
|
||||
BaseItem parentItem;
|
||||
|
||||
if (userId.HasValue && !userId.Equals(Guid.Empty))
|
||||
{
|
||||
user = _userManager.GetUserById(userId.Value);
|
||||
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
else
|
||||
{
|
||||
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
|
||||
var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
|
||||
var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
|
||||
var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
ExcludeItemTypes = excludeItemTypesArr,
|
||||
IncludeItemTypes = includeItemTypesArr,
|
||||
MediaTypes = mediaTypesArr,
|
||||
StartIndex = startIndex,
|
||||
Limit = limit,
|
||||
IsFavorite = isFavorite,
|
||||
NameLessThan = nameLessThan,
|
||||
NameStartsWith = nameStartsWith,
|
||||
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
||||
Tags = RequestHelpers.Split(tags, ',', true),
|
||||
OfficialRatings = RequestHelpers.Split(officialRatings, ',', true),
|
||||
Genres = RequestHelpers.Split(genres, ',', true),
|
||||
GenreIds = RequestHelpers.GetGuids(genreIds),
|
||||
StudioIds = RequestHelpers.GetGuids(studioIds),
|
||||
Person = person,
|
||||
PersonIds = RequestHelpers.GetGuids(personIds),
|
||||
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
|
||||
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
|
||||
MinCommunityRating = minCommunityRating,
|
||||
DtoOptions = dtoOptions,
|
||||
SearchTerm = searchTerm,
|
||||
EnableTotalRecordCount = enableTotalRecordCount
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(parentId))
|
||||
{
|
||||
if (parentItem is Folder)
|
||||
{
|
||||
query.AncestorIds = new[] { new Guid(parentId) };
|
||||
}
|
||||
else
|
||||
{
|
||||
query.ItemIds = new[] { new Guid(parentId) };
|
||||
}
|
||||
}
|
||||
|
||||
// Studios
|
||||
if (!string.IsNullOrEmpty(studios))
|
||||
{
|
||||
query.StudioIds = studios.Split('|').Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return _libraryManager.GetStudio(i);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}).Where(i => i != null).Select(i => i!.Id).ToArray();
|
||||
}
|
||||
|
||||
foreach (var filter in RequestHelpers.GetFilters(filters))
|
||||
{
|
||||
switch (filter)
|
||||
{
|
||||
case ItemFilter.Dislikes:
|
||||
query.IsLiked = false;
|
||||
break;
|
||||
case ItemFilter.IsFavorite:
|
||||
query.IsFavorite = true;
|
||||
break;
|
||||
case ItemFilter.IsFavoriteOrLikes:
|
||||
query.IsFavoriteOrLiked = true;
|
||||
break;
|
||||
case ItemFilter.IsFolder:
|
||||
query.IsFolder = true;
|
||||
break;
|
||||
case ItemFilter.IsNotFolder:
|
||||
query.IsFolder = false;
|
||||
break;
|
||||
case ItemFilter.IsPlayed:
|
||||
query.IsPlayed = true;
|
||||
break;
|
||||
case ItemFilter.IsResumable:
|
||||
query.IsResumable = true;
|
||||
break;
|
||||
case ItemFilter.IsUnplayed:
|
||||
query.IsPlayed = false;
|
||||
break;
|
||||
case ItemFilter.Likes:
|
||||
query.IsLiked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var result = _libraryManager.GetArtists(query);
|
||||
|
||||
var dtos = result.Items.Select(i =>
|
||||
{
|
||||
var (baseItem, itemCounts) = i;
|
||||
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(includeItemTypes))
|
||||
{
|
||||
dto.ChildCount = itemCounts.ItemCount;
|
||||
dto.ProgramCount = itemCounts.ProgramCount;
|
||||
dto.SeriesCount = itemCounts.SeriesCount;
|
||||
dto.EpisodeCount = itemCounts.EpisodeCount;
|
||||
dto.MovieCount = itemCounts.MovieCount;
|
||||
dto.TrailerCount = itemCounts.TrailerCount;
|
||||
dto.AlbumCount = itemCounts.AlbumCount;
|
||||
dto.SongCount = itemCounts.SongCount;
|
||||
dto.ArtistCount = itemCounts.ArtistCount;
|
||||
}
|
||||
|
||||
return dto;
|
||||
});
|
||||
|
||||
return new QueryResult<BaseItemDto>
|
||||
{
|
||||
Items = dtos.ToArray(),
|
||||
TotalRecordCount = result.TotalRecordCount
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all album artists from a given item, folder, or the entire library.
|
||||
/// </summary>
|
||||
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
|
||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="searchTerm">Optional. Search term.</param>
|
||||
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
|
||||
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
|
||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
|
||||
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
|
||||
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
|
||||
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
|
||||
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
|
||||
/// <param name="enableUserData">Optional, include user data.</param>
|
||||
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
|
||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
|
||||
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param>
|
||||
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
|
||||
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="userId">User id.</param>
|
||||
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
|
||||
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
|
||||
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
|
||||
/// <param name="enableImages">Optional, include image information in output.</param>
|
||||
/// <param name="enableTotalRecordCount">Total record count.</param>
|
||||
/// <response code="200">Album artists returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the album artists.</returns>
|
||||
[HttpGet("AlbumArtists")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetAlbumArtists(
|
||||
[FromQuery] double? minCommunityRating,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? searchTerm,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery] string? filters,
|
||||
[FromQuery] bool? isFavorite,
|
||||
[FromQuery] string? mediaTypes,
|
||||
[FromQuery] string? genres,
|
||||
[FromQuery] string? genreIds,
|
||||
[FromQuery] string? officialRatings,
|
||||
[FromQuery] string? tags,
|
||||
[FromQuery] string? years,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes,
|
||||
[FromQuery] string? person,
|
||||
[FromQuery] string? personIds,
|
||||
[FromQuery] string? personTypes,
|
||||
[FromQuery] string? studios,
|
||||
[FromQuery] string? studioIds,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
[FromQuery] string? nameLessThan,
|
||||
[FromQuery] bool? enableImages = true,
|
||||
[FromQuery] bool enableTotalRecordCount = true)
|
||||
{
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
User? user = null;
|
||||
BaseItem parentItem;
|
||||
|
||||
if (userId.HasValue && !userId.Equals(Guid.Empty))
|
||||
{
|
||||
user = _userManager.GetUserById(userId.Value);
|
||||
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
else
|
||||
{
|
||||
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
|
||||
var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
|
||||
var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
|
||||
var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
ExcludeItemTypes = excludeItemTypesArr,
|
||||
IncludeItemTypes = includeItemTypesArr,
|
||||
MediaTypes = mediaTypesArr,
|
||||
StartIndex = startIndex,
|
||||
Limit = limit,
|
||||
IsFavorite = isFavorite,
|
||||
NameLessThan = nameLessThan,
|
||||
NameStartsWith = nameStartsWith,
|
||||
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
||||
Tags = RequestHelpers.Split(tags, ',', true),
|
||||
OfficialRatings = RequestHelpers.Split(officialRatings, ',', true),
|
||||
Genres = RequestHelpers.Split(genres, ',', true),
|
||||
GenreIds = RequestHelpers.GetGuids(genreIds),
|
||||
StudioIds = RequestHelpers.GetGuids(studioIds),
|
||||
Person = person,
|
||||
PersonIds = RequestHelpers.GetGuids(personIds),
|
||||
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
|
||||
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
|
||||
MinCommunityRating = minCommunityRating,
|
||||
DtoOptions = dtoOptions,
|
||||
SearchTerm = searchTerm,
|
||||
EnableTotalRecordCount = enableTotalRecordCount
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(parentId))
|
||||
{
|
||||
if (parentItem is Folder)
|
||||
{
|
||||
query.AncestorIds = new[] { new Guid(parentId) };
|
||||
}
|
||||
else
|
||||
{
|
||||
query.ItemIds = new[] { new Guid(parentId) };
|
||||
}
|
||||
}
|
||||
|
||||
// Studios
|
||||
if (!string.IsNullOrEmpty(studios))
|
||||
{
|
||||
query.StudioIds = studios.Split('|').Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return _libraryManager.GetStudio(i);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}).Where(i => i != null).Select(i => i!.Id).ToArray();
|
||||
}
|
||||
|
||||
foreach (var filter in RequestHelpers.GetFilters(filters))
|
||||
{
|
||||
switch (filter)
|
||||
{
|
||||
case ItemFilter.Dislikes:
|
||||
query.IsLiked = false;
|
||||
break;
|
||||
case ItemFilter.IsFavorite:
|
||||
query.IsFavorite = true;
|
||||
break;
|
||||
case ItemFilter.IsFavoriteOrLikes:
|
||||
query.IsFavoriteOrLiked = true;
|
||||
break;
|
||||
case ItemFilter.IsFolder:
|
||||
query.IsFolder = true;
|
||||
break;
|
||||
case ItemFilter.IsNotFolder:
|
||||
query.IsFolder = false;
|
||||
break;
|
||||
case ItemFilter.IsPlayed:
|
||||
query.IsPlayed = true;
|
||||
break;
|
||||
case ItemFilter.IsResumable:
|
||||
query.IsResumable = true;
|
||||
break;
|
||||
case ItemFilter.IsUnplayed:
|
||||
query.IsPlayed = false;
|
||||
break;
|
||||
case ItemFilter.Likes:
|
||||
query.IsLiked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var result = _libraryManager.GetAlbumArtists(query);
|
||||
|
||||
var dtos = result.Items.Select(i =>
|
||||
{
|
||||
var (baseItem, itemCounts) = i;
|
||||
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(includeItemTypes))
|
||||
{
|
||||
dto.ChildCount = itemCounts.ItemCount;
|
||||
dto.ProgramCount = itemCounts.ProgramCount;
|
||||
dto.SeriesCount = itemCounts.SeriesCount;
|
||||
dto.EpisodeCount = itemCounts.EpisodeCount;
|
||||
dto.MovieCount = itemCounts.MovieCount;
|
||||
dto.TrailerCount = itemCounts.TrailerCount;
|
||||
dto.AlbumCount = itemCounts.AlbumCount;
|
||||
dto.SongCount = itemCounts.SongCount;
|
||||
dto.ArtistCount = itemCounts.ArtistCount;
|
||||
}
|
||||
|
||||
return dto;
|
||||
});
|
||||
|
||||
return new QueryResult<BaseItemDto>
|
||||
{
|
||||
Items = dtos.ToArray(),
|
||||
TotalRecordCount = result.TotalRecordCount
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an artist by name.
|
||||
/// </summary>
|
||||
/// <param name="name">Studio name.</param>
|
||||
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||
/// <response code="200">Artist returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the artist.</returns>
|
||||
[HttpGet("{name}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<BaseItemDto> GetArtistByName([FromRoute] string name, [FromQuery] Guid? userId)
|
||||
{
|
||||
var dtoOptions = new DtoOptions().AddClientFields(Request);
|
||||
|
||||
var item = _libraryManager.GetArtist(name, dtoOptions);
|
||||
|
||||
if (userId.HasValue && !userId.Equals(Guid.Empty))
|
||||
{
|
||||
var user = _userManager.GetUserById(userId.Value);
|
||||
|
||||
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
|
||||
}
|
||||
|
||||
return _dtoService.GetBaseItemDto(item, dtoOptions);
|
||||
}
|
||||
}
|
||||
}
|
353
Jellyfin.Api/Controllers/AudioController.cs
Normal file
353
Jellyfin.Api/Controllers/AudioController.cs
Normal file
|
@ -0,0 +1,353 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.Models.StreamingDtos;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// The audio controller.
|
||||
/// </summary>
|
||||
// TODO: In order to autheneticate this in the future, Dlna playback will require updating
|
||||
public class AudioController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IDlnaManager _dlnaManager;
|
||||
private readonly IAuthorizationContext _authContext;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ISubtitleEncoder _subtitleEncoder;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IDeviceManager _deviceManager;
|
||||
private readonly TranscodingJobHelper _transcodingJobHelper;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AudioController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
|
||||
/// <param name="userManger">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="authorizationContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="subtitleEncoder">Instance of the <see cref="ISubtitleEncoder"/> interface.</param>
|
||||
/// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
|
||||
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
|
||||
/// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param>
|
||||
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
|
||||
public AudioController(
|
||||
IDlnaManager dlnaManager,
|
||||
IUserManager userManger,
|
||||
IAuthorizationContext authorizationContext,
|
||||
ILibraryManager libraryManager,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
IMediaEncoder mediaEncoder,
|
||||
IFileSystem fileSystem,
|
||||
ISubtitleEncoder subtitleEncoder,
|
||||
IConfiguration configuration,
|
||||
IDeviceManager deviceManager,
|
||||
TranscodingJobHelper transcodingJobHelper,
|
||||
IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_dlnaManager = dlnaManager;
|
||||
_authContext = authorizationContext;
|
||||
_userManager = userManger;
|
||||
_libraryManager = libraryManager;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_fileSystem = fileSystem;
|
||||
_subtitleEncoder = subtitleEncoder;
|
||||
_configuration = configuration;
|
||||
_deviceManager = deviceManager;
|
||||
_transcodingJobHelper = transcodingJobHelper;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an audio stream.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="container">The audio container.</param>
|
||||
/// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
|
||||
/// <param name="params">The streaming parameters.</param>
|
||||
/// <param name="tag">The tag.</param>
|
||||
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
|
||||
/// <param name="playSessionId">The play session id.</param>
|
||||
/// <param name="segmentContainer">The segment container.</param>
|
||||
/// <param name="segmentLength">The segment lenght.</param>
|
||||
/// <param name="minSegments">The minimum number of segments.</param>
|
||||
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
|
||||
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
||||
/// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
|
||||
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
||||
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
||||
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
||||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
||||
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
|
||||
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
|
||||
/// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
|
||||
/// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
|
||||
/// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
|
||||
/// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
|
||||
/// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
|
||||
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
|
||||
/// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
|
||||
/// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
|
||||
/// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
|
||||
/// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
|
||||
/// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
|
||||
/// <param name="maxRefFrames">Optional.</param>
|
||||
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
|
||||
/// <param name="requireAvc">Optional. Whether to require avc.</param>
|
||||
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
|
||||
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
|
||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||
/// <param name="liveStreamId">The live stream id.</param>
|
||||
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
|
||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
|
||||
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
|
||||
/// <param name="transcodingReasons">Optional. The transcoding reason.</param>
|
||||
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
|
||||
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
|
||||
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
|
||||
/// <param name="streamOptions">Optional. The streaming options.</param>
|
||||
/// <response code="200">Audio stream returned.</response>
|
||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||
[HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetAudioStreamByContainer")]
|
||||
[HttpGet("{itemId}/stream", Name = "GetAudioStream")]
|
||||
[HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadAudioStreamByContainer")]
|
||||
[HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult> GetAudioStream(
|
||||
[FromRoute] Guid itemId,
|
||||
[FromRoute] string? container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
[FromQuery] bool? breakOnNonKeyFrames,
|
||||
[FromQuery] int? audioSampleRate,
|
||||
[FromQuery] int? maxAudioBitDepth,
|
||||
[FromQuery] int? audioBitRate,
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
[FromQuery] long? startTimeTicks,
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? videoBitRate,
|
||||
[FromQuery] int? subtitleStreamIndex,
|
||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
||||
[FromQuery] int? maxRefFrames,
|
||||
[FromQuery] int? maxVideoBitDepth,
|
||||
[FromQuery] bool? requireAvc,
|
||||
[FromQuery] bool? deInterlace,
|
||||
[FromQuery] bool? requireNonAnamorphic,
|
||||
[FromQuery] int? transcodingMaxAudioChannels,
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery] string? transcodingReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
[FromQuery] EncodingContext? context,
|
||||
[FromQuery] Dictionary<string, string>? streamOptions)
|
||||
{
|
||||
bool isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
|
||||
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
StreamingRequestDto streamingRequest = new StreamingRequestDto
|
||||
{
|
||||
Id = itemId,
|
||||
Container = container,
|
||||
Static = @static ?? true,
|
||||
Params = @params,
|
||||
Tag = tag,
|
||||
DeviceProfileId = deviceProfileId,
|
||||
PlaySessionId = playSessionId,
|
||||
SegmentContainer = segmentContainer,
|
||||
SegmentLength = segmentLength,
|
||||
MinSegments = minSegments,
|
||||
MediaSourceId = mediaSourceId,
|
||||
DeviceId = deviceId,
|
||||
AudioCodec = audioCodec,
|
||||
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
||||
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
||||
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
||||
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
||||
AudioSampleRate = audioSampleRate,
|
||||
MaxAudioChannels = maxAudioChannels,
|
||||
AudioBitRate = audioBitRate,
|
||||
MaxAudioBitDepth = maxAudioBitDepth,
|
||||
AudioChannels = audioChannels,
|
||||
Profile = profile,
|
||||
Level = level,
|
||||
Framerate = framerate,
|
||||
MaxFramerate = maxFramerate,
|
||||
CopyTimestamps = copyTimestamps ?? true,
|
||||
StartTimeTicks = startTimeTicks,
|
||||
Width = width,
|
||||
Height = height,
|
||||
VideoBitRate = videoBitRate,
|
||||
SubtitleStreamIndex = subtitleStreamIndex,
|
||||
SubtitleMethod = subtitleMethod,
|
||||
MaxRefFrames = maxRefFrames,
|
||||
MaxVideoBitDepth = maxVideoBitDepth,
|
||||
RequireAvc = requireAvc ?? true,
|
||||
DeInterlace = deInterlace ?? true,
|
||||
RequireNonAnamorphic = requireNonAnamorphic ?? true,
|
||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||
CpuCoreLimit = cpuCoreLimit,
|
||||
LiveStreamId = liveStreamId,
|
||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
|
||||
VideoCodec = videoCodec,
|
||||
SubtitleCodec = subtitleCodec,
|
||||
TranscodeReasons = transcodingReasons,
|
||||
AudioStreamIndex = audioStreamIndex,
|
||||
VideoStreamIndex = videoStreamIndex,
|
||||
Context = context ?? EncodingContext.Static,
|
||||
StreamOptions = streamOptions
|
||||
};
|
||||
|
||||
using var state = await StreamingHelpers.GetStreamingState(
|
||||
streamingRequest,
|
||||
Request,
|
||||
_authContext,
|
||||
_mediaSourceManager,
|
||||
_userManager,
|
||||
_libraryManager,
|
||||
_serverConfigurationManager,
|
||||
_mediaEncoder,
|
||||
_fileSystem,
|
||||
_subtitleEncoder,
|
||||
_configuration,
|
||||
_dlnaManager,
|
||||
_deviceManager,
|
||||
_transcodingJobHelper,
|
||||
_transcodingJobType,
|
||||
cancellationTokenSource.Token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (@static.HasValue && @static.Value && state.DirectStreamProvider != null)
|
||||
{
|
||||
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
|
||||
|
||||
await new ProgressiveFileCopier(state.DirectStreamProvider, null, _transcodingJobHelper, CancellationToken.None)
|
||||
{
|
||||
AllowEndOfFile = false
|
||||
}.WriteToAsync(Response.Body, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// TODO (moved from MediaBrowser.Api): Don't hardcode contentType
|
||||
return File(Response.Body, MimeTypes.GetMimeType("file.ts")!);
|
||||
}
|
||||
|
||||
// Static remote stream
|
||||
if (@static.HasValue && @static.Value && state.InputProtocol == MediaProtocol.Http)
|
||||
{
|
||||
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, true, startTimeTicks, Request, _dlnaManager);
|
||||
|
||||
using var httpClient = _httpClientFactory.CreateClient();
|
||||
return await FileStreamResponseHelpers.GetStaticRemoteStreamResult(state, isHeadRequest, this, httpClient).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (@static.HasValue && @static.Value && state.InputProtocol != MediaProtocol.File)
|
||||
{
|
||||
return BadRequest($"Input protocol {state.InputProtocol} cannot be streamed statically");
|
||||
}
|
||||
|
||||
var outputPath = state.OutputFilePath;
|
||||
var outputPathExists = System.IO.File.Exists(outputPath);
|
||||
|
||||
var transcodingJob = _transcodingJobHelper.GetTranscodingJob(outputPath, TranscodingJobType.Progressive);
|
||||
var isTranscodeCached = outputPathExists && transcodingJob != null;
|
||||
|
||||
StreamingHelpers.AddDlnaHeaders(state, Response.Headers, (@static.HasValue && @static.Value) || isTranscodeCached, startTimeTicks, Request, _dlnaManager);
|
||||
|
||||
// Static stream
|
||||
if (@static.HasValue && @static.Value)
|
||||
{
|
||||
var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
|
||||
|
||||
if (state.MediaSource.IsInfiniteStream)
|
||||
{
|
||||
await new ProgressiveFileCopier(state.MediaPath, null, _transcodingJobHelper, CancellationToken.None)
|
||||
{
|
||||
AllowEndOfFile = false
|
||||
}.WriteToAsync(Response.Body, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return File(Response.Body, contentType);
|
||||
}
|
||||
|
||||
return FileStreamResponseHelpers.GetStaticFileResult(
|
||||
state.MediaPath,
|
||||
contentType,
|
||||
isHeadRequest,
|
||||
this);
|
||||
}
|
||||
|
||||
// Need to start ffmpeg (because media can't be returned directly)
|
||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||
var encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
|
||||
var ffmpegCommandLineArguments = encodingHelper.GetProgressiveAudioFullCommandLine(state, encodingOptions, outputPath);
|
||||
return await FileStreamResponseHelpers.GetTranscodedFile(
|
||||
state,
|
||||
isHeadRequest,
|
||||
this,
|
||||
_transcodingJobHelper,
|
||||
ffmpegCommandLineArguments,
|
||||
Request,
|
||||
_transcodingJobType,
|
||||
cancellationTokenSource).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
57
Jellyfin.Api/Controllers/BrandingController.cs
Normal file
57
Jellyfin.Api/Controllers/BrandingController.cs
Normal file
|
@ -0,0 +1,57 @@
|
|||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Model.Branding;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Branding controller.
|
||||
/// </summary>
|
||||
public class BrandingController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BrandingController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
public BrandingController(IServerConfigurationManager serverConfigurationManager)
|
||||
{
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets branding configuration.
|
||||
/// </summary>
|
||||
/// <response code="200">Branding configuration returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the branding configuration.</returns>
|
||||
[HttpGet("Configuration")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<BrandingOptions> GetBrandingOptions()
|
||||
{
|
||||
return _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets branding css.
|
||||
/// </summary>
|
||||
/// <response code="200">Branding css returned.</response>
|
||||
/// <response code="204">No branding css configured.</response>
|
||||
/// <returns>
|
||||
/// An <see cref="OkResult"/> containing the branding css if exist,
|
||||
/// or a <see cref="NoContentResult"/> if the css is not configured.
|
||||
/// </returns>
|
||||
[HttpGet("Css")]
|
||||
[HttpGet("Css.css", Name = "GetBrandingCss_2")]
|
||||
[Produces("text/css")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult<string> GetBrandingCss()
|
||||
{
|
||||
var options = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
||||
return options.CustomCss ?? string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
256
Jellyfin.Api/Controllers/ChannelsController.cs
Normal file
256
Jellyfin.Api/Controllers/ChannelsController.cs
Normal file
|
@ -0,0 +1,256 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Channels;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Channels Controller.
|
||||
/// </summary>
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
public class ChannelsController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IChannelManager _channelManager;
|
||||
private readonly IUserManager _userManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChannelsController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="channelManager">Instance of the <see cref="IChannelManager"/> interface.</param>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
public ChannelsController(IChannelManager channelManager, IUserManager userManager)
|
||||
{
|
||||
_channelManager = channelManager;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets available channels.
|
||||
/// </summary>
|
||||
/// <param name="userId">User Id to filter by. Use <see cref="Guid.Empty"/> to not filter by user.</param>
|
||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="supportsLatestItems">Optional. Filter by channels that support getting latest items.</param>
|
||||
/// <param name="supportsMediaDeletion">Optional. Filter by channels that support media deletion.</param>
|
||||
/// <param name="isFavorite">Optional. Filter by channels that are favorite.</param>
|
||||
/// <response code="200">Channels returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the channels.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetChannels(
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] bool? supportsLatestItems,
|
||||
[FromQuery] bool? supportsMediaDeletion,
|
||||
[FromQuery] bool? isFavorite)
|
||||
{
|
||||
return _channelManager.GetChannels(new ChannelQuery
|
||||
{
|
||||
Limit = limit,
|
||||
StartIndex = startIndex,
|
||||
UserId = userId ?? Guid.Empty,
|
||||
SupportsLatestItems = supportsLatestItems,
|
||||
SupportsMediaDeletion = supportsMediaDeletion,
|
||||
IsFavorite = isFavorite
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all channel features.
|
||||
/// </summary>
|
||||
/// <response code="200">All channel features returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the channel features.</returns>
|
||||
[HttpGet("Features")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<ChannelFeatures>> GetAllChannelFeatures()
|
||||
{
|
||||
return _channelManager.GetAllChannelFeatures();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get channel features.
|
||||
/// </summary>
|
||||
/// <param name="channelId">Channel id.</param>
|
||||
/// <response code="200">Channel features returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the channel features.</returns>
|
||||
[HttpGet("{channelId}/Features")]
|
||||
public ActionResult<ChannelFeatures> GetChannelFeatures([FromRoute] string channelId)
|
||||
{
|
||||
return _channelManager.GetChannelFeatures(channelId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get channel items.
|
||||
/// </summary>
|
||||
/// <param name="channelId">Channel Id.</param>
|
||||
/// <param name="folderId">Optional. Folder Id.</param>
|
||||
/// <param name="userId">Optional. User Id.</param>
|
||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="sortOrder">Optional. Sort Order - Ascending,Descending.</param>
|
||||
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
|
||||
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
|
||||
/// <response code="200">Channel items returned.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="Task"/> representing the request to get the channel items.
|
||||
/// The task result contains an <see cref="OkResult"/> containing the channel items.
|
||||
/// </returns>
|
||||
[HttpGet("{channelId}/Items")]
|
||||
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetChannelItems(
|
||||
[FromRoute] Guid channelId,
|
||||
[FromQuery] Guid? folderId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? sortOrder,
|
||||
[FromQuery] string? filters,
|
||||
[FromQuery] string? sortBy,
|
||||
[FromQuery] string? fields)
|
||||
{
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
Limit = limit,
|
||||
StartIndex = startIndex,
|
||||
ChannelIds = new[] { channelId },
|
||||
ParentId = folderId ?? Guid.Empty,
|
||||
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
|
||||
DtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
};
|
||||
|
||||
foreach (var filter in RequestHelpers.GetFilters(filters))
|
||||
{
|
||||
switch (filter)
|
||||
{
|
||||
case ItemFilter.IsFolder:
|
||||
query.IsFolder = true;
|
||||
break;
|
||||
case ItemFilter.IsNotFolder:
|
||||
query.IsFolder = false;
|
||||
break;
|
||||
case ItemFilter.IsUnplayed:
|
||||
query.IsPlayed = false;
|
||||
break;
|
||||
case ItemFilter.IsPlayed:
|
||||
query.IsPlayed = true;
|
||||
break;
|
||||
case ItemFilter.IsFavorite:
|
||||
query.IsFavorite = true;
|
||||
break;
|
||||
case ItemFilter.IsResumable:
|
||||
query.IsResumable = true;
|
||||
break;
|
||||
case ItemFilter.Likes:
|
||||
query.IsLiked = true;
|
||||
break;
|
||||
case ItemFilter.Dislikes:
|
||||
query.IsLiked = false;
|
||||
break;
|
||||
case ItemFilter.IsFavoriteOrLikes:
|
||||
query.IsFavoriteOrLiked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return await _channelManager.GetChannelItems(query, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets latest channel items.
|
||||
/// </summary>
|
||||
/// <param name="userId">Optional. User Id.</param>
|
||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
|
||||
/// <param name="channelIds">Optional. Specify one or more channel id's, comma delimited.</param>
|
||||
/// <response code="200">Latest channel items returned.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="Task"/> representing the request to get the latest channel items.
|
||||
/// The task result contains an <see cref="OkResult"/> containing the latest channel items.
|
||||
/// </returns>
|
||||
[HttpGet("Items/Latest")]
|
||||
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLatestChannelItems(
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? filters,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery] string? channelIds)
|
||||
{
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
Limit = limit,
|
||||
StartIndex = startIndex,
|
||||
ChannelIds = (channelIds ?? string.Empty)
|
||||
.Split(',')
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Select(i => new Guid(i))
|
||||
.ToArray(),
|
||||
DtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
};
|
||||
|
||||
foreach (var filter in RequestHelpers.GetFilters(filters))
|
||||
{
|
||||
switch (filter)
|
||||
{
|
||||
case ItemFilter.IsFolder:
|
||||
query.IsFolder = true;
|
||||
break;
|
||||
case ItemFilter.IsNotFolder:
|
||||
query.IsFolder = false;
|
||||
break;
|
||||
case ItemFilter.IsUnplayed:
|
||||
query.IsPlayed = false;
|
||||
break;
|
||||
case ItemFilter.IsPlayed:
|
||||
query.IsPlayed = true;
|
||||
break;
|
||||
case ItemFilter.IsFavorite:
|
||||
query.IsFavorite = true;
|
||||
break;
|
||||
case ItemFilter.IsResumable:
|
||||
query.IsResumable = true;
|
||||
break;
|
||||
case ItemFilter.Likes:
|
||||
query.IsLiked = true;
|
||||
break;
|
||||
case ItemFilter.Dislikes:
|
||||
query.IsLiked = false;
|
||||
break;
|
||||
case ItemFilter.IsFavoriteOrLikes:
|
||||
query.IsFavoriteOrLiked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return await _channelManager.GetLatestChannelItems(query, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
111
Jellyfin.Api/Controllers/CollectionController.cs
Normal file
111
Jellyfin.Api/Controllers/CollectionController.cs
Normal file
|
@ -0,0 +1,111 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using MediaBrowser.Controller.Collections;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Collections;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// The collection controller.
|
||||
/// </summary>
|
||||
[Route("Collections")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
public class CollectionController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly ICollectionManager _collectionManager;
|
||||
private readonly IDtoService _dtoService;
|
||||
private readonly IAuthorizationContext _authContext;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CollectionController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="collectionManager">Instance of <see cref="ICollectionManager"/> interface.</param>
|
||||
/// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
|
||||
/// <param name="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param>
|
||||
public CollectionController(
|
||||
ICollectionManager collectionManager,
|
||||
IDtoService dtoService,
|
||||
IAuthorizationContext authContext)
|
||||
{
|
||||
_collectionManager = collectionManager;
|
||||
_dtoService = dtoService;
|
||||
_authContext = authContext;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new collection.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the collection.</param>
|
||||
/// <param name="ids">Item Ids to add to the collection.</param>
|
||||
/// <param name="parentId">Optional. Create the collection within a specific folder.</param>
|
||||
/// <param name="isLocked">Whether or not to lock the new collection.</param>
|
||||
/// <response code="200">Collection created.</response>
|
||||
/// <returns>A <see cref="CollectionCreationOptions"/> with information about the new collection.</returns>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<CollectionCreationResult> CreateCollection(
|
||||
[FromQuery] string? name,
|
||||
[FromQuery] string? ids,
|
||||
[FromQuery] Guid? parentId,
|
||||
[FromQuery] bool isLocked = false)
|
||||
{
|
||||
var userId = _authContext.GetAuthorizationInfo(Request).UserId;
|
||||
|
||||
var item = _collectionManager.CreateCollection(new CollectionCreationOptions
|
||||
{
|
||||
IsLocked = isLocked,
|
||||
Name = name,
|
||||
ParentId = parentId,
|
||||
ItemIdList = RequestHelpers.Split(ids, ',', true),
|
||||
UserIds = new[] { userId }
|
||||
});
|
||||
|
||||
var dtoOptions = new DtoOptions().AddClientFields(Request);
|
||||
|
||||
var dto = _dtoService.GetBaseItemDto(item, dtoOptions);
|
||||
|
||||
return new CollectionCreationResult
|
||||
{
|
||||
Id = dto.Id
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds items to a collection.
|
||||
/// </summary>
|
||||
/// <param name="collectionId">The collection id.</param>
|
||||
/// <param name="itemIds">Item ids, comma delimited.</param>
|
||||
/// <response code="204">Items added to collection.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpPost("{collectionId}/Items")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult AddToCollection([FromRoute] Guid collectionId, [FromQuery, Required] string? itemIds)
|
||||
{
|
||||
_collectionManager.AddToCollection(collectionId, RequestHelpers.Split(itemIds, ',', true));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes items from a collection.
|
||||
/// </summary>
|
||||
/// <param name="collectionId">The collection id.</param>
|
||||
/// <param name="itemIds">Item ids, comma delimited.</param>
|
||||
/// <response code="204">Items removed from collection.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpDelete("{collectionId}/Items")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult RemoveFromCollection([FromRoute] Guid collectionId, [FromQuery, Required] string? itemIds)
|
||||
{
|
||||
_collectionManager.RemoveFromCollection(collectionId, RequestHelpers.Split(itemIds, ',', true));
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
126
Jellyfin.Api/Controllers/ConfigurationController.cs
Normal file
126
Jellyfin.Api/Controllers/ConfigurationController.cs
Normal file
|
@ -0,0 +1,126 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Models.ConfigurationDtos;
|
||||
using MediaBrowser.Common.Json;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration Controller.
|
||||
/// </summary>
|
||||
[Route("System")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
public class ConfigurationController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IServerConfigurationManager _configurationManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
|
||||
private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigurationController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
||||
public ConfigurationController(
|
||||
IServerConfigurationManager configurationManager,
|
||||
IMediaEncoder mediaEncoder)
|
||||
{
|
||||
_configurationManager = configurationManager;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets application configuration.
|
||||
/// </summary>
|
||||
/// <response code="200">Application configuration returned.</response>
|
||||
/// <returns>Application configuration.</returns>
|
||||
[HttpGet("Configuration")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<ServerConfiguration> GetConfiguration()
|
||||
{
|
||||
return _configurationManager.Configuration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates application configuration.
|
||||
/// </summary>
|
||||
/// <param name="configuration">Configuration.</param>
|
||||
/// <response code="204">Configuration updated.</response>
|
||||
/// <returns>Update status.</returns>
|
||||
[HttpPost("Configuration")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult UpdateConfiguration([FromBody, Required] ServerConfiguration configuration)
|
||||
{
|
||||
_configurationManager.ReplaceConfiguration(configuration);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a named configuration.
|
||||
/// </summary>
|
||||
/// <param name="key">Configuration key.</param>
|
||||
/// <response code="200">Configuration returned.</response>
|
||||
/// <returns>Configuration.</returns>
|
||||
[HttpGet("Configuration/{key}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<object> GetNamedConfiguration([FromRoute] string? key)
|
||||
{
|
||||
return _configurationManager.GetConfiguration(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates named configuration.
|
||||
/// </summary>
|
||||
/// <param name="key">Configuration key.</param>
|
||||
/// <response code="204">Named configuration updated.</response>
|
||||
/// <returns>Update status.</returns>
|
||||
[HttpPost("Configuration/{key}")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> UpdateNamedConfiguration([FromRoute] string? key)
|
||||
{
|
||||
var configurationType = _configurationManager.GetConfigurationType(key);
|
||||
var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType, _serializerOptions).ConfigureAwait(false);
|
||||
_configurationManager.SaveConfiguration(key, configuration);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a default MetadataOptions object.
|
||||
/// </summary>
|
||||
/// <response code="200">Metadata options returned.</response>
|
||||
/// <returns>Default MetadataOptions.</returns>
|
||||
[HttpGet("Configuration/MetadataOptions/Default")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<MetadataOptions> GetDefaultMetadataOptions()
|
||||
{
|
||||
return new MetadataOptions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the path to the media encoder.
|
||||
/// </summary>
|
||||
/// <param name="mediaEncoderPath">Media encoder path form body.</param>
|
||||
/// <response code="204">Media encoder path updated.</response>
|
||||
/// <returns>Status.</returns>
|
||||
[HttpPost("MediaEncoder/Path")]
|
||||
[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult UpdateMediaEncoderPath([FromForm, Required] MediaEncoderPathDto mediaEncoderPath)
|
||||
{
|
||||
_mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
273
Jellyfin.Api/Controllers/DashboardController.cs
Normal file
273
Jellyfin.Api/Controllers/DashboardController.cs
Normal file
|
@ -0,0 +1,273 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Models;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Extensions;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Plugins;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// The dashboard controller.
|
||||
/// </summary>
|
||||
[Route("")]
|
||||
public class DashboardController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly ILogger<DashboardController> _logger;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IConfiguration _appConfig;
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly IResourceFileManager _resourceFileManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DashboardController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
|
||||
/// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
|
||||
/// <param name="appConfig">Instance of <see cref="IConfiguration"/> interface.</param>
|
||||
/// <param name="resourceFileManager">Instance of <see cref="IResourceFileManager"/> interface.</param>
|
||||
/// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
public DashboardController(
|
||||
ILogger<DashboardController> logger,
|
||||
IServerApplicationHost appHost,
|
||||
IConfiguration appConfig,
|
||||
IResourceFileManager resourceFileManager,
|
||||
IServerConfigurationManager serverConfigurationManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_appHost = appHost;
|
||||
_appConfig = appConfig;
|
||||
_resourceFileManager = resourceFileManager;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path of the directory containing the static web interface content, or null if the server is not
|
||||
/// hosting the web client.
|
||||
/// </summary>
|
||||
private string? WebClientUiPath => GetWebClientUiPath(_appConfig, _serverConfigurationManager);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration pages.
|
||||
/// </summary>
|
||||
/// <param name="enableInMainMenu">Whether to enable in the main menu.</param>
|
||||
/// <param name="pageType">The <see cref="ConfigurationPageInfo"/>.</param>
|
||||
/// <response code="200">ConfigurationPages returned.</response>
|
||||
/// <response code="404">Server still loading.</response>
|
||||
/// <returns>An <see cref="IEnumerable{ConfigurationPageInfo}"/> with infos about the plugins.</returns>
|
||||
[HttpGet("web/ConfigurationPages")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<IEnumerable<ConfigurationPageInfo?>> GetConfigurationPages(
|
||||
[FromQuery] bool? enableInMainMenu,
|
||||
[FromQuery] ConfigurationPageType? pageType)
|
||||
{
|
||||
const string unavailableMessage = "The server is still loading. Please try again momentarily.";
|
||||
|
||||
var pages = _appHost.GetExports<IPluginConfigurationPage>().ToList();
|
||||
|
||||
if (pages == null)
|
||||
{
|
||||
return NotFound(unavailableMessage);
|
||||
}
|
||||
|
||||
// Don't allow a failing plugin to fail them all
|
||||
var configPages = pages.Select(p =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return new ConfigurationPageInfo(p);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting plugin information from {Plugin}", p.GetType().Name);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.Where(i => i != null)
|
||||
.ToList();
|
||||
|
||||
configPages.AddRange(_appHost.Plugins.SelectMany(GetConfigPages));
|
||||
|
||||
if (pageType.HasValue)
|
||||
{
|
||||
configPages = configPages.Where(p => p!.ConfigurationPageType == pageType).ToList();
|
||||
}
|
||||
|
||||
if (enableInMainMenu.HasValue)
|
||||
{
|
||||
configPages = configPages.Where(p => p!.EnableInMainMenu == enableInMainMenu.Value).ToList();
|
||||
}
|
||||
|
||||
return configPages;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a dashboard configuration page.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the page.</param>
|
||||
/// <response code="200">ConfigurationPage returned.</response>
|
||||
/// <response code="404">Plugin configuration page not found.</response>
|
||||
/// <returns>The configuration page.</returns>
|
||||
[HttpGet("web/ConfigurationPage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult GetDashboardConfigurationPage([FromQuery] string? name)
|
||||
{
|
||||
IPlugin? plugin = null;
|
||||
Stream? stream = null;
|
||||
|
||||
var isJs = false;
|
||||
var isTemplate = false;
|
||||
|
||||
var page = _appHost.GetExports<IPluginConfigurationPage>().FirstOrDefault(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||
if (page != null)
|
||||
{
|
||||
plugin = page.Plugin;
|
||||
stream = page.GetHtmlStream();
|
||||
}
|
||||
|
||||
if (plugin == null)
|
||||
{
|
||||
var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||
if (altPage != null)
|
||||
{
|
||||
plugin = altPage.Item2;
|
||||
stream = plugin.GetType().Assembly.GetManifestResourceStream(altPage.Item1.EmbeddedResourcePath);
|
||||
|
||||
isJs = string.Equals(Path.GetExtension(altPage.Item1.EmbeddedResourcePath), ".js", StringComparison.OrdinalIgnoreCase);
|
||||
isTemplate = altPage.Item1.EmbeddedResourcePath.EndsWith(".template.html", StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
if (plugin != null && stream != null)
|
||||
{
|
||||
if (isJs)
|
||||
{
|
||||
return File(stream, MimeTypes.GetMimeType("page.js"));
|
||||
}
|
||||
|
||||
if (isTemplate)
|
||||
{
|
||||
return File(stream, MimeTypes.GetMimeType("page.html"));
|
||||
}
|
||||
|
||||
return File(stream, MimeTypes.GetMimeType("page.html"));
|
||||
}
|
||||
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the robots.txt.
|
||||
/// </summary>
|
||||
/// <response code="200">Robots.txt returned.</response>
|
||||
/// <returns>The robots.txt.</returns>
|
||||
[HttpGet("robots.txt")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public ActionResult GetRobotsTxt()
|
||||
{
|
||||
return GetWebClientResource("robots.txt");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a resource from the web client.
|
||||
/// </summary>
|
||||
/// <param name="resourceName">The resource name.</param>
|
||||
/// <response code="200">Web client returned.</response>
|
||||
/// <response code="404">Server does not host a web client.</response>
|
||||
/// <returns>The resource.</returns>
|
||||
[HttpGet("web/{*resourceName}")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult GetWebClientResource([FromRoute] string resourceName)
|
||||
{
|
||||
if (!_appConfig.HostWebClient() || WebClientUiPath == null)
|
||||
{
|
||||
return NotFound("Server does not host a web client.");
|
||||
}
|
||||
|
||||
var path = resourceName;
|
||||
var basePath = WebClientUiPath;
|
||||
|
||||
var requestPathAndQuery = Request.GetEncodedPathAndQuery();
|
||||
// Bounce them to the startup wizard if it hasn't been completed yet
|
||||
if (!_serverConfigurationManager.Configuration.IsStartupWizardCompleted
|
||||
&& !requestPathAndQuery.Contains("wizard", StringComparison.OrdinalIgnoreCase)
|
||||
&& requestPathAndQuery.Contains("index", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Redirect("index.html?start=wizard#!/wizardstart.html");
|
||||
}
|
||||
|
||||
var stream = new FileStream(_resourceFileManager.GetResourcePath(basePath, path), FileMode.Open, FileAccess.Read);
|
||||
return File(stream, MimeTypes.GetMimeType(path));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the favicon.
|
||||
/// </summary>
|
||||
/// <response code="200">Favicon.ico returned.</response>
|
||||
/// <returns>The favicon.</returns>
|
||||
[HttpGet("favicon.ico")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public ActionResult GetFavIcon()
|
||||
{
|
||||
return GetWebClientResource("favicon.ico");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path of the directory containing the static web interface content.
|
||||
/// </summary>
|
||||
/// <param name="appConfig">The app configuration.</param>
|
||||
/// <param name="serverConfigManager">The server configuration manager.</param>
|
||||
/// <returns>The directory path, or null if the server is not hosting the web client.</returns>
|
||||
public static string? GetWebClientUiPath(IConfiguration appConfig, IServerConfigurationManager serverConfigManager)
|
||||
{
|
||||
if (!appConfig.HostWebClient())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(serverConfigManager.Configuration.DashboardSourcePath))
|
||||
{
|
||||
return serverConfigManager.Configuration.DashboardSourcePath;
|
||||
}
|
||||
|
||||
return serverConfigManager.ApplicationPaths.WebPath;
|
||||
}
|
||||
|
||||
private IEnumerable<ConfigurationPageInfo> GetConfigPages(IPlugin plugin)
|
||||
{
|
||||
return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin, i.Item1));
|
||||
}
|
||||
|
||||
private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(IPlugin plugin)
|
||||
{
|
||||
if (!(plugin is IHasWebPages hasWebPages))
|
||||
{
|
||||
return new List<Tuple<PluginPageInfo, IPlugin>>();
|
||||
}
|
||||
|
||||
return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin));
|
||||
}
|
||||
|
||||
private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages()
|
||||
{
|
||||
return _appHost.Plugins.SelectMany(GetPluginPages);
|
||||
}
|
||||
}
|
||||
}
|
155
Jellyfin.Api/Controllers/DevicesController.cs
Normal file
155
Jellyfin.Api/Controllers/DevicesController.cs
Normal file
|
@ -0,0 +1,155 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Security;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Devices;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Devices Controller.
|
||||
/// </summary>
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
public class DevicesController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IDeviceManager _deviceManager;
|
||||
private readonly IAuthenticationRepository _authenticationRepository;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DevicesController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
|
||||
/// <param name="authenticationRepository">Instance of <see cref="IAuthenticationRepository"/> interface.</param>
|
||||
/// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
|
||||
public DevicesController(
|
||||
IDeviceManager deviceManager,
|
||||
IAuthenticationRepository authenticationRepository,
|
||||
ISessionManager sessionManager)
|
||||
{
|
||||
_deviceManager = deviceManager;
|
||||
_authenticationRepository = authenticationRepository;
|
||||
_sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get Devices.
|
||||
/// </summary>
|
||||
/// <param name="supportsSync">Gets or sets a value indicating whether [supports synchronize].</param>
|
||||
/// <param name="userId">Gets or sets the user identifier.</param>
|
||||
/// <response code="200">Devices retrieved.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
|
||||
[HttpGet]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery, Required] Guid? userId)
|
||||
{
|
||||
var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty };
|
||||
return _deviceManager.GetDevices(deviceQuery);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get info for a device.
|
||||
/// </summary>
|
||||
/// <param name="id">Device Id.</param>
|
||||
/// <response code="200">Device info retrieved.</response>
|
||||
/// <response code="404">Device not found.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
|
||||
[HttpGet("Info")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string? id)
|
||||
{
|
||||
var deviceInfo = _deviceManager.GetDevice(id);
|
||||
if (deviceInfo == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return deviceInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get options for a device.
|
||||
/// </summary>
|
||||
/// <param name="id">Device Id.</param>
|
||||
/// <response code="200">Device options retrieved.</response>
|
||||
/// <response code="404">Device not found.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
|
||||
[HttpGet("Options")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string? id)
|
||||
{
|
||||
var deviceInfo = _deviceManager.GetDeviceOptions(id);
|
||||
if (deviceInfo == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return deviceInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update device options.
|
||||
/// </summary>
|
||||
/// <param name="id">Device Id.</param>
|
||||
/// <param name="deviceOptions">Device Options.</param>
|
||||
/// <response code="204">Device options updated.</response>
|
||||
/// <response code="404">Device not found.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
|
||||
[HttpPost("Options")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult UpdateDeviceOptions(
|
||||
[FromQuery, Required] string? id,
|
||||
[FromBody, Required] DeviceOptions deviceOptions)
|
||||
{
|
||||
var existingDeviceOptions = _deviceManager.GetDeviceOptions(id);
|
||||
if (existingDeviceOptions == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
_deviceManager.UpdateDeviceOptions(id, deviceOptions);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a device.
|
||||
/// </summary>
|
||||
/// <param name="id">Device Id.</param>
|
||||
/// <response code="204">Device deleted.</response>
|
||||
/// <response code="404">Device not found.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
|
||||
[HttpDelete]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult DeleteDevice([FromQuery, Required] string? id)
|
||||
{
|
||||
var existingDevice = _deviceManager.GetDevice(id);
|
||||
if (existingDevice == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var sessions = _authenticationRepository.Get(new AuthenticationInfoQuery { DeviceId = id }).Items;
|
||||
|
||||
foreach (var session in sessions)
|
||||
{
|
||||
_sessionManager.Logout(session);
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
176
Jellyfin.Api/Controllers/DisplayPreferencesController.cs
Normal file
176
Jellyfin.Api/Controllers/DisplayPreferencesController.cs
Normal file
|
@ -0,0 +1,176 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Display Preferences Controller.
|
||||
/// </summary>
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
public class DisplayPreferencesController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IDisplayPreferencesManager _displayPreferencesManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="displayPreferencesManager">Instance of <see cref="IDisplayPreferencesManager"/> interface.</param>
|
||||
public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager)
|
||||
{
|
||||
_displayPreferencesManager = displayPreferencesManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get Display Preferences.
|
||||
/// </summary>
|
||||
/// <param name="displayPreferencesId">Display preferences id.</param>
|
||||
/// <param name="userId">User id.</param>
|
||||
/// <param name="client">Client.</param>
|
||||
/// <response code="200">Display preferences retrieved.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the display preferences on success, or a <see cref="NotFoundResult"/> if the display preferences could not be found.</returns>
|
||||
[HttpGet("{displayPreferencesId}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
|
||||
public ActionResult<DisplayPreferencesDto> GetDisplayPreferences(
|
||||
[FromRoute] string? displayPreferencesId,
|
||||
[FromQuery] [Required] Guid userId,
|
||||
[FromQuery] [Required] string? client)
|
||||
{
|
||||
var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, client);
|
||||
var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, Guid.Empty, displayPreferences.Client);
|
||||
|
||||
var dto = new DisplayPreferencesDto
|
||||
{
|
||||
Client = displayPreferences.Client,
|
||||
Id = displayPreferences.UserId.ToString(),
|
||||
ViewType = itemPreferences.ViewType.ToString(),
|
||||
SortBy = itemPreferences.SortBy,
|
||||
SortOrder = itemPreferences.SortOrder,
|
||||
IndexBy = displayPreferences.IndexBy?.ToString(),
|
||||
RememberIndexing = itemPreferences.RememberIndexing,
|
||||
RememberSorting = itemPreferences.RememberSorting,
|
||||
ScrollDirection = displayPreferences.ScrollDirection,
|
||||
ShowBackdrop = displayPreferences.ShowBackdrop,
|
||||
ShowSidebar = displayPreferences.ShowSidebar
|
||||
};
|
||||
|
||||
foreach (var homeSection in displayPreferences.HomeSections)
|
||||
{
|
||||
dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant();
|
||||
}
|
||||
|
||||
foreach (var itemDisplayPreferences in _displayPreferencesManager.ListItemDisplayPreferences(displayPreferences.UserId, displayPreferences.Client))
|
||||
{
|
||||
dto.CustomPrefs["landing-" + itemDisplayPreferences.ItemId] = itemDisplayPreferences.ViewType.ToString().ToLowerInvariant();
|
||||
}
|
||||
|
||||
dto.CustomPrefs["chromecastVersion"] = displayPreferences.ChromecastVersion.ToString().ToLowerInvariant();
|
||||
dto.CustomPrefs["skipForwardLength"] = displayPreferences.SkipForwardLength.ToString(CultureInfo.InvariantCulture);
|
||||
dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString(CultureInfo.InvariantCulture);
|
||||
dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture);
|
||||
dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update Display Preferences.
|
||||
/// </summary>
|
||||
/// <param name="displayPreferencesId">Display preferences id.</param>
|
||||
/// <param name="userId">User Id.</param>
|
||||
/// <param name="client">Client.</param>
|
||||
/// <param name="displayPreferences">New Display Preferences object.</param>
|
||||
/// <response code="204">Display preferences updated.</response>
|
||||
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
|
||||
[HttpPost("{displayPreferencesId}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
|
||||
public ActionResult UpdateDisplayPreferences(
|
||||
[FromRoute] string? displayPreferencesId,
|
||||
[FromQuery, Required] Guid userId,
|
||||
[FromQuery, Required] string? client,
|
||||
[FromBody, Required] DisplayPreferencesDto displayPreferences)
|
||||
{
|
||||
HomeSectionType[] defaults =
|
||||
{
|
||||
HomeSectionType.SmallLibraryTiles,
|
||||
HomeSectionType.Resume,
|
||||
HomeSectionType.ResumeAudio,
|
||||
HomeSectionType.LiveTv,
|
||||
HomeSectionType.NextUp,
|
||||
HomeSectionType.LatestMedia, HomeSectionType.None,
|
||||
};
|
||||
|
||||
var existingDisplayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, client);
|
||||
existingDisplayPreferences.IndexBy = Enum.TryParse<IndexingKind>(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null;
|
||||
existingDisplayPreferences.ShowBackdrop = displayPreferences.ShowBackdrop;
|
||||
existingDisplayPreferences.ShowSidebar = displayPreferences.ShowSidebar;
|
||||
|
||||
existingDisplayPreferences.ScrollDirection = displayPreferences.ScrollDirection;
|
||||
existingDisplayPreferences.ChromecastVersion = displayPreferences.CustomPrefs.TryGetValue("chromecastVersion", out var chromecastVersion)
|
||||
? Enum.Parse<ChromecastVersion>(chromecastVersion, true)
|
||||
: ChromecastVersion.Stable;
|
||||
existingDisplayPreferences.EnableNextVideoInfoOverlay = displayPreferences.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enableNextVideoInfoOverlay)
|
||||
? bool.Parse(enableNextVideoInfoOverlay)
|
||||
: true;
|
||||
existingDisplayPreferences.SkipBackwardLength = displayPreferences.CustomPrefs.TryGetValue("skipBackLength", out var skipBackLength)
|
||||
? int.Parse(skipBackLength, CultureInfo.InvariantCulture)
|
||||
: 10000;
|
||||
existingDisplayPreferences.SkipForwardLength = displayPreferences.CustomPrefs.TryGetValue("skipForwardLength", out var skipForwardLength)
|
||||
? int.Parse(skipForwardLength, CultureInfo.InvariantCulture)
|
||||
: 30000;
|
||||
existingDisplayPreferences.DashboardTheme = displayPreferences.CustomPrefs.TryGetValue("dashboardTheme", out var theme)
|
||||
? theme
|
||||
: string.Empty;
|
||||
existingDisplayPreferences.TvHome = displayPreferences.CustomPrefs.TryGetValue("tvhome", out var home)
|
||||
? home
|
||||
: string.Empty;
|
||||
existingDisplayPreferences.HomeSections.Clear();
|
||||
|
||||
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("homesection", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
var order = int.Parse(key.AsSpan().Slice("homesection".Length));
|
||||
if (!Enum.TryParse<HomeSectionType>(displayPreferences.CustomPrefs[key], true, out var type))
|
||||
{
|
||||
type = order < 7 ? defaults[order] : HomeSectionType.None;
|
||||
}
|
||||
|
||||
existingDisplayPreferences.HomeSections.Add(new HomeSection { Order = order, Type = type });
|
||||
}
|
||||
|
||||
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, Guid.Parse(key.Substring("landing-".Length)), existingDisplayPreferences.Client);
|
||||
itemPreferences.ViewType = Enum.Parse<ViewType>(displayPreferences.ViewType);
|
||||
_displayPreferencesManager.SaveChanges(itemPreferences);
|
||||
}
|
||||
|
||||
var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, Guid.Empty, existingDisplayPreferences.Client);
|
||||
itemPrefs.SortBy = displayPreferences.SortBy;
|
||||
itemPrefs.SortOrder = displayPreferences.SortOrder;
|
||||
itemPrefs.RememberIndexing = displayPreferences.RememberIndexing;
|
||||
itemPrefs.RememberSorting = displayPreferences.RememberSorting;
|
||||
|
||||
if (Enum.TryParse<ViewType>(displayPreferences.ViewType, true, out var viewType))
|
||||
{
|
||||
itemPrefs.ViewType = viewType;
|
||||
}
|
||||
|
||||
_displayPreferencesManager.SaveChanges(existingDisplayPreferences);
|
||||
_displayPreferencesManager.SaveChanges(itemPrefs);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
132
Jellyfin.Api/Controllers/DlnaController.cs
Normal file
132
Jellyfin.Api/Controllers/DlnaController.cs
Normal file
|
@ -0,0 +1,132 @@
|
|||
using System.Collections.Generic;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Dlna Controller.
|
||||
/// </summary>
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
public class DlnaController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IDlnaManager _dlnaManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DlnaController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
|
||||
public DlnaController(IDlnaManager dlnaManager)
|
||||
{
|
||||
_dlnaManager = dlnaManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get profile infos.
|
||||
/// </summary>
|
||||
/// <response code="200">Device profile infos returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the device profile infos.</returns>
|
||||
[HttpGet("ProfileInfos")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<DeviceProfileInfo>> GetProfileInfos()
|
||||
{
|
||||
return Ok(_dlnaManager.GetProfileInfos());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default profile.
|
||||
/// </summary>
|
||||
/// <response code="200">Default device profile returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the default profile.</returns>
|
||||
[HttpGet("Profiles/Default")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<DeviceProfile> GetDefaultProfile()
|
||||
{
|
||||
return _dlnaManager.GetDefaultProfile();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a single profile.
|
||||
/// </summary>
|
||||
/// <param name="profileId">Profile Id.</param>
|
||||
/// <response code="200">Device profile returned.</response>
|
||||
/// <response code="404">Device profile not found.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the profile on success, or a <see cref="NotFoundResult"/> if device profile not found.</returns>
|
||||
[HttpGet("Profiles/{profileId}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<DeviceProfile> GetProfile([FromRoute] string profileId)
|
||||
{
|
||||
var profile = _dlnaManager.GetProfile(profileId);
|
||||
if (profile == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a profile.
|
||||
/// </summary>
|
||||
/// <param name="profileId">Profile id.</param>
|
||||
/// <response code="204">Device profile deleted.</response>
|
||||
/// <response code="404">Device profile not found.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns>
|
||||
[HttpDelete("Profiles/{profileId}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult DeleteProfile([FromRoute] string profileId)
|
||||
{
|
||||
var existingDeviceProfile = _dlnaManager.GetProfile(profileId);
|
||||
if (existingDeviceProfile == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
_dlnaManager.DeleteProfile(profileId);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a profile.
|
||||
/// </summary>
|
||||
/// <param name="deviceProfile">Device profile.</param>
|
||||
/// <response code="204">Device profile created.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Profiles")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult CreateProfile([FromBody] DeviceProfile deviceProfile)
|
||||
{
|
||||
_dlnaManager.CreateProfile(deviceProfile);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a profile.
|
||||
/// </summary>
|
||||
/// <param name="profileId">Profile id.</param>
|
||||
/// <param name="deviceProfile">Device profile.</param>
|
||||
/// <response code="204">Device profile updated.</response>
|
||||
/// <response code="404">Device profile not found.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns>
|
||||
[HttpPost("Profiles/{profileId}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult UpdateProfile([FromRoute] string profileId, [FromBody] DeviceProfile deviceProfile)
|
||||
{
|
||||
var existingDeviceProfile = _dlnaManager.GetProfile(profileId);
|
||||
if (existingDeviceProfile == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
_dlnaManager.UpdateProfile(deviceProfile);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
257
Jellyfin.Api/Controllers/DlnaServerController.cs
Normal file
257
Jellyfin.Api/Controllers/DlnaServerController.cs
Normal file
|
@ -0,0 +1,257 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna;
|
||||
using Emby.Dlna.Main;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Dlna Server Controller.
|
||||
/// </summary>
|
||||
[Route("Dlna")]
|
||||
public class DlnaServerController : BaseJellyfinApiController
|
||||
{
|
||||
private const string XMLContentType = "text/xml; charset=UTF-8";
|
||||
|
||||
private readonly IDlnaManager _dlnaManager;
|
||||
private readonly IContentDirectory _contentDirectory;
|
||||
private readonly IConnectionManager _connectionManager;
|
||||
private readonly IMediaReceiverRegistrar _mediaReceiverRegistrar;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DlnaServerController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
|
||||
public DlnaServerController(IDlnaManager dlnaManager)
|
||||
{
|
||||
_dlnaManager = dlnaManager;
|
||||
_contentDirectory = DlnaEntryPoint.Current.ContentDirectory;
|
||||
_connectionManager = DlnaEntryPoint.Current.ConnectionManager;
|
||||
_mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get Description Xml.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <response code="200">Description xml returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the description xml.</returns>
|
||||
[HttpGet("{serverId}/description")]
|
||||
[HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")]
|
||||
[Produces(XMLContentType)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult GetDescriptionXml([FromRoute] string serverId)
|
||||
{
|
||||
var url = GetAbsoluteUri();
|
||||
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
|
||||
var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress);
|
||||
return Ok(xml);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets Dlna content directory xml.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <response code="200">Dlna content directory returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns>
|
||||
[HttpGet("{serverId}/ContentDirectory/ContentDirectory")]
|
||||
[HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_2")]
|
||||
[Produces(XMLContentType)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||
public ActionResult GetContentDirectory([FromRoute] string serverId)
|
||||
{
|
||||
return Ok(_contentDirectory.GetServiceXml());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets Dlna media receiver registrar xml.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <returns>Dlna media receiver registrar xml.</returns>
|
||||
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar")]
|
||||
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_2")]
|
||||
[Produces(XMLContentType)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||
public ActionResult GetMediaReceiverRegistrar([FromRoute] string serverId)
|
||||
{
|
||||
return Ok(_mediaReceiverRegistrar.GetServiceXml());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets Dlna media receiver registrar xml.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <returns>Dlna media receiver registrar xml.</returns>
|
||||
[HttpGet("{serverId}/ConnectionManager/ConnectionManager")]
|
||||
[HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_2")]
|
||||
[Produces(XMLContentType)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||
public ActionResult GetConnectionManager([FromRoute] string serverId)
|
||||
{
|
||||
return Ok(_connectionManager.GetServiceXml());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process a content directory control request.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <returns>Control response.</returns>
|
||||
[HttpPost("{serverId}/ContentDirectory/Control")]
|
||||
public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute] string serverId)
|
||||
{
|
||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process a connection manager control request.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <returns>Control response.</returns>
|
||||
[HttpPost("{serverId}/ConnectionManager/Control")]
|
||||
public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute] string serverId)
|
||||
{
|
||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process a media receiver registrar control request.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <returns>Control response.</returns>
|
||||
[HttpPost("{serverId}/MediaReceiverRegistrar/Control")]
|
||||
public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute] string serverId)
|
||||
{
|
||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes an event subscription request.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <returns>Event subscription response.</returns>
|
||||
[HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")]
|
||||
[HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||
public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId)
|
||||
{
|
||||
return ProcessEventRequest(_mediaReceiverRegistrar);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes an event subscription request.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <returns>Event subscription response.</returns>
|
||||
[HttpSubscribe("{serverId}/ContentDirectory/Events")]
|
||||
[HttpUnsubscribe("{serverId}/ContentDirectory/Events")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||
public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId)
|
||||
{
|
||||
return ProcessEventRequest(_contentDirectory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes an event subscription request.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <returns>Event subscription response.</returns>
|
||||
[HttpSubscribe("{serverId}/ConnectionManager/Events")]
|
||||
[HttpUnsubscribe("{serverId}/ConnectionManager/Events")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||
public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId)
|
||||
{
|
||||
return ProcessEventRequest(_connectionManager);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a server icon.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <param name="fileName">The icon filename.</param>
|
||||
/// <returns>Icon stream.</returns>
|
||||
[HttpGet("{serverId}/icons/{fileName}")]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||
public ActionResult GetIconId([FromRoute] string serverId, [FromRoute] string fileName)
|
||||
{
|
||||
return GetIconInternal(fileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a server icon.
|
||||
/// </summary>
|
||||
/// <param name="fileName">The icon filename.</param>
|
||||
/// <returns>Icon stream.</returns>
|
||||
[HttpGet("icons/{fileName}")]
|
||||
public ActionResult GetIcon([FromRoute] string fileName)
|
||||
{
|
||||
return GetIconInternal(fileName);
|
||||
}
|
||||
|
||||
private ActionResult GetIconInternal(string fileName)
|
||||
{
|
||||
var icon = _dlnaManager.GetIcon(fileName);
|
||||
if (icon == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var contentType = "image/" + Path.GetExtension(fileName)
|
||||
.TrimStart('.')
|
||||
.ToLowerInvariant();
|
||||
|
||||
return File(icon.Stream, contentType);
|
||||
}
|
||||
|
||||
private string GetAbsoluteUri()
|
||||
{
|
||||
return $"{Request.Scheme}://{Request.Host}{Request.Path}";
|
||||
}
|
||||
|
||||
private Task<ControlResponse> ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service)
|
||||
{
|
||||
return service.ProcessControlRequestAsync(new ControlRequest
|
||||
{
|
||||
Headers = Request.Headers,
|
||||
InputXml = requestStream,
|
||||
TargetServerUuId = id,
|
||||
RequestedUrl = GetAbsoluteUri()
|
||||
});
|
||||
}
|
||||
|
||||
private EventSubscriptionResponse ProcessEventRequest(IEventManager eventManager)
|
||||
{
|
||||
var subscriptionId = Request.Headers["SID"];
|
||||
if (string.Equals(Request.Method, "subscribe", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var notificationType = Request.Headers["NT"];
|
||||
var callback = Request.Headers["CALLBACK"];
|
||||
var timeoutString = Request.Headers["TIMEOUT"];
|
||||
|
||||
if (string.IsNullOrEmpty(notificationType))
|
||||
{
|
||||
return eventManager.RenewEventSubscription(
|
||||
subscriptionId,
|
||||
notificationType,
|
||||
timeoutString,
|
||||
callback);
|
||||
}
|
||||
|
||||
return eventManager.CreateEventSubscription(notificationType, timeoutString, callback);
|
||||
}
|
||||
|
||||
return eventManager.CancelEventSubscription(subscriptionId);
|
||||
}
|
||||
}
|
||||
}
|
2221
Jellyfin.Api/Controllers/DynamicHlsController.cs
Normal file
2221
Jellyfin.Api/Controllers/DynamicHlsController.cs
Normal file
File diff suppressed because it is too large
Load Diff
191
Jellyfin.Api/Controllers/EnvironmentController.cs
Normal file
191
Jellyfin.Api/Controllers/EnvironmentController.cs
Normal file
|
@ -0,0 +1,191 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Models.EnvironmentDtos;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Environment Controller.
|
||||
/// </summary>
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
public class EnvironmentController : BaseJellyfinApiController
|
||||
{
|
||||
private const char UncSeparator = '\\';
|
||||
private const string UncStartPrefix = @"\\";
|
||||
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILogger<EnvironmentController> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="EnvironmentController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{EnvironmentController}"/> interface.</param>
|
||||
public EnvironmentController(IFileSystem fileSystem, ILogger<EnvironmentController> logger)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the contents of a given directory in the file system.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <param name="includeFiles">An optional filter to include or exclude files from the results. true/false.</param>
|
||||
/// <param name="includeDirectories">An optional filter to include or exclude folders from the results. true/false.</param>
|
||||
/// <response code="200">Directory contents returned.</response>
|
||||
/// <returns>Directory contents.</returns>
|
||||
[HttpGet("DirectoryContents")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public IEnumerable<FileSystemEntryInfo> GetDirectoryContents(
|
||||
[FromQuery, Required] string path,
|
||||
[FromQuery] bool includeFiles = false,
|
||||
[FromQuery] bool includeDirectories = false)
|
||||
{
|
||||
if (path.StartsWith(UncStartPrefix, StringComparison.OrdinalIgnoreCase)
|
||||
&& path.LastIndexOf(UncSeparator) == 1)
|
||||
{
|
||||
return Array.Empty<FileSystemEntryInfo>();
|
||||
}
|
||||
|
||||
var entries =
|
||||
_fileSystem.GetFileSystemEntries(path)
|
||||
.Where(i => (i.IsDirectory && includeDirectories) || (!i.IsDirectory && includeFiles))
|
||||
.OrderBy(i => i.FullName);
|
||||
|
||||
return entries.Select(f => new FileSystemEntryInfo(f.Name, f.FullName, f.IsDirectory ? FileSystemEntryType.Directory : FileSystemEntryType.File));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates path.
|
||||
/// </summary>
|
||||
/// <param name="validatePathDto">Validate request object.</param>
|
||||
/// <response code="200">Path validated.</response>
|
||||
/// <response code="404">Path not found.</response>
|
||||
/// <returns>Validation status.</returns>
|
||||
[HttpPost("ValidatePath")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult ValidatePath([FromBody, Required] ValidatePathDto validatePathDto)
|
||||
{
|
||||
if (validatePathDto.IsFile.HasValue)
|
||||
{
|
||||
if (validatePathDto.IsFile.Value)
|
||||
{
|
||||
if (!System.IO.File.Exists(validatePathDto.Path))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Directory.Exists(validatePathDto.Path))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!System.IO.File.Exists(validatePathDto.Path) && !Directory.Exists(validatePathDto.Path))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (validatePathDto.ValidateWritable)
|
||||
{
|
||||
var file = Path.Combine(validatePathDto.Path, Guid.NewGuid().ToString());
|
||||
try
|
||||
{
|
||||
System.IO.File.WriteAllText(file, string.Empty);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (System.IO.File.Exists(file))
|
||||
{
|
||||
System.IO.File.Delete(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets network paths.
|
||||
/// </summary>
|
||||
/// <response code="200">Empty array returned.</response>
|
||||
/// <returns>List of entries.</returns>
|
||||
[Obsolete("This endpoint is obsolete.")]
|
||||
[HttpGet("NetworkShares")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<FileSystemEntryInfo>> GetNetworkShares()
|
||||
{
|
||||
_logger.LogWarning("Obsolete endpoint accessed: /Environment/NetworkShares");
|
||||
return Array.Empty<FileSystemEntryInfo>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets available drives from the server's file system.
|
||||
/// </summary>
|
||||
/// <response code="200">List of entries returned.</response>
|
||||
/// <returns>List of entries.</returns>
|
||||
[HttpGet("Drives")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public IEnumerable<FileSystemEntryInfo> GetDrives()
|
||||
{
|
||||
return _fileSystem.GetDrives().Select(d => new FileSystemEntryInfo(d.Name, d.FullName, FileSystemEntryType.Directory));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parent path of a given path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
/// <returns>Parent path.</returns>
|
||||
[HttpGet("ParentPath")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<string?> GetParentPath([FromQuery, Required] string path)
|
||||
{
|
||||
string? parent = Path.GetDirectoryName(path);
|
||||
if (string.IsNullOrEmpty(parent))
|
||||
{
|
||||
// Check if unc share
|
||||
var index = path.LastIndexOf(UncSeparator);
|
||||
|
||||
if (index != -1 && path.IndexOf(UncSeparator, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
parent = path.Substring(0, index);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(parent.TrimStart(UncSeparator)))
|
||||
{
|
||||
parent = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parent;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get Default directory browser.
|
||||
/// </summary>
|
||||
/// <response code="200">Default directory browser returned.</response>
|
||||
/// <returns>Default directory browser.</returns>
|
||||
[HttpGet("DefaultDirectoryBrowser")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<DefaultDirectoryBrowserInfoDto> GetDefaultDirectoryBrowser()
|
||||
{
|
||||
return new DefaultDirectoryBrowserInfoDto();
|
||||
}
|
||||
}
|
||||
}
|
218
Jellyfin.Api/Controllers/FilterController.cs
Normal file
218
Jellyfin.Api/Controllers/FilterController.cs
Normal file
|
@ -0,0 +1,218 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Filters controller.
|
||||
/// </summary>
|
||||
[Route("")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
public class FilterController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FilterController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
public FilterController(ILibraryManager libraryManager, IUserManager userManager)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets legacy query filters.
|
||||
/// </summary>
|
||||
/// <param name="userId">Optional. User id.</param>
|
||||
/// <param name="parentId">Optional. Parent id.</param>
|
||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
|
||||
/// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
|
||||
/// <response code="200">Legacy filters retrieved.</response>
|
||||
/// <returns>Legacy query filters.</returns>
|
||||
[HttpGet("Items/Filters")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy(
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery] string? mediaTypes)
|
||||
{
|
||||
var parentItem = string.IsNullOrEmpty(parentId)
|
||||
? null
|
||||
: _libraryManager.GetItemById(parentId);
|
||||
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
|
||||
if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
parentItem = null;
|
||||
}
|
||||
|
||||
var item = string.IsNullOrEmpty(parentId)
|
||||
? user == null
|
||||
? _libraryManager.RootFolder
|
||||
: _libraryManager.GetUserRootFolder()
|
||||
: parentItem;
|
||||
|
||||
var query = new InternalItemsQuery
|
||||
{
|
||||
User = user,
|
||||
MediaTypes = (mediaTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
|
||||
IncludeItemTypes = (includeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
|
||||
Recursive = true,
|
||||
EnableTotalRecordCount = false,
|
||||
DtoOptions = new DtoOptions
|
||||
{
|
||||
Fields = new[] { ItemFields.Genres, ItemFields.Tags },
|
||||
EnableImages = false,
|
||||
EnableUserData = false
|
||||
}
|
||||
};
|
||||
|
||||
var itemList = ((Folder)item!).GetItemList(query);
|
||||
return new QueryFiltersLegacy
|
||||
{
|
||||
Years = itemList.Select(i => i.ProductionYear ?? -1)
|
||||
.Where(i => i > 0)
|
||||
.Distinct()
|
||||
.OrderBy(i => i)
|
||||
.ToArray(),
|
||||
|
||||
Genres = itemList.SelectMany(i => i.Genres)
|
||||
.DistinctNames()
|
||||
.OrderBy(i => i)
|
||||
.ToArray(),
|
||||
|
||||
Tags = itemList
|
||||
.SelectMany(i => i.Tags)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(i => i)
|
||||
.ToArray(),
|
||||
|
||||
OfficialRatings = itemList
|
||||
.Select(i => i.OfficialRating)
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(i => i)
|
||||
.ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets query filters.
|
||||
/// </summary>
|
||||
/// <param name="userId">Optional. User id.</param>
|
||||
/// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
|
||||
/// <param name="isAiring">Optional. Is item airing.</param>
|
||||
/// <param name="isMovie">Optional. Is item movie.</param>
|
||||
/// <param name="isSports">Optional. Is item sports.</param>
|
||||
/// <param name="isKids">Optional. Is item kids.</param>
|
||||
/// <param name="isNews">Optional. Is item news.</param>
|
||||
/// <param name="isSeries">Optional. Is item series.</param>
|
||||
/// <param name="recursive">Optional. Search recursive.</param>
|
||||
/// <response code="200">Filters retrieved.</response>
|
||||
/// <returns>Query filters.</returns>
|
||||
[HttpGet("Items/Filters2")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryFilters> GetQueryFilters(
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery] bool? isAiring,
|
||||
[FromQuery] bool? isMovie,
|
||||
[FromQuery] bool? isSports,
|
||||
[FromQuery] bool? isKids,
|
||||
[FromQuery] bool? isNews,
|
||||
[FromQuery] bool? isSeries,
|
||||
[FromQuery] bool? recursive)
|
||||
{
|
||||
var parentItem = string.IsNullOrEmpty(parentId)
|
||||
? null
|
||||
: _libraryManager.GetItemById(parentId);
|
||||
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
|
||||
if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
parentItem = null;
|
||||
}
|
||||
|
||||
var filters = new QueryFilters();
|
||||
var genreQuery = new InternalItemsQuery(user)
|
||||
{
|
||||
IncludeItemTypes =
|
||||
(includeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
|
||||
DtoOptions = new DtoOptions
|
||||
{
|
||||
Fields = Array.Empty<ItemFields>(),
|
||||
EnableImages = false,
|
||||
EnableUserData = false
|
||||
},
|
||||
IsAiring = isAiring,
|
||||
IsMovie = isMovie,
|
||||
IsSports = isSports,
|
||||
IsKids = isKids,
|
||||
IsNews = isNews,
|
||||
IsSeries = isSeries
|
||||
};
|
||||
|
||||
if ((recursive ?? true) || parentItem is UserView || parentItem is ICollectionFolder)
|
||||
{
|
||||
genreQuery.AncestorIds = parentItem == null ? Array.Empty<Guid>() : new[] { parentItem.Id };
|
||||
}
|
||||
else
|
||||
{
|
||||
genreQuery.Parent = parentItem;
|
||||
}
|
||||
|
||||
if (string.Equals(includeItemTypes, nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, nameof(MusicVideo), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, nameof(MusicArtist), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, nameof(Audio), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair
|
||||
{
|
||||
Name = i.Item1.Name,
|
||||
Id = i.Item1.Id
|
||||
}).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
filters.Genres = _libraryManager.GetGenres(genreQuery).Items.Select(i => new NameGuidPair
|
||||
{
|
||||
Name = i.Item1.Name,
|
||||
Id = i.Item1.Id
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
}
|
||||
}
|
320
Jellyfin.Api/Controllers/GenresController.cs
Normal file
320
Jellyfin.Api/Controllers/GenresController.cs
Normal file
|
@ -0,0 +1,320 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Genre = MediaBrowser.Controller.Entities.Genre;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// The genres controller.
|
||||
/// </summary>
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
public class GenresController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IDtoService _dtoService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GenresController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
||||
public GenresController(
|
||||
IUserManager userManager,
|
||||
ILibraryManager libraryManager,
|
||||
IDtoService dtoService)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_libraryManager = libraryManager;
|
||||
_dtoService = dtoService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all genres from a given item, folder, or the entire library.
|
||||
/// </summary>
|
||||
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
|
||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="searchTerm">The search term.</param>
|
||||
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
|
||||
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
|
||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
|
||||
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
|
||||
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
|
||||
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
|
||||
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
|
||||
/// <param name="enableUserData">Optional, include user data.</param>
|
||||
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
|
||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
|
||||
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
|
||||
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
|
||||
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="userId">User id.</param>
|
||||
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
|
||||
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
|
||||
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
|
||||
/// <param name="enableImages">Optional, include image information in output.</param>
|
||||
/// <param name="enableTotalRecordCount">Optional. Include total record count.</param>
|
||||
/// <response code="200">Genres returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the queryresult of genres.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetGenres(
|
||||
[FromQuery] double? minCommunityRating,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? searchTerm,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery] string? filters,
|
||||
[FromQuery] bool? isFavorite,
|
||||
[FromQuery] string? mediaTypes,
|
||||
[FromQuery] string? genres,
|
||||
[FromQuery] string? genreIds,
|
||||
[FromQuery] string? officialRatings,
|
||||
[FromQuery] string? tags,
|
||||
[FromQuery] string? years,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes,
|
||||
[FromQuery] string? person,
|
||||
[FromQuery] string? personIds,
|
||||
[FromQuery] string? personTypes,
|
||||
[FromQuery] string? studios,
|
||||
[FromQuery] string? studioIds,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
[FromQuery] string? nameLessThan,
|
||||
[FromQuery] bool? enableImages = true,
|
||||
[FromQuery] bool enableTotalRecordCount = true)
|
||||
{
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
User? user = null;
|
||||
BaseItem parentItem;
|
||||
|
||||
if (userId.HasValue && !userId.Equals(Guid.Empty))
|
||||
{
|
||||
user = _userManager.GetUserById(userId.Value);
|
||||
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
else
|
||||
{
|
||||
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
|
||||
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
|
||||
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
|
||||
StartIndex = startIndex,
|
||||
Limit = limit,
|
||||
IsFavorite = isFavorite,
|
||||
NameLessThan = nameLessThan,
|
||||
NameStartsWith = nameStartsWith,
|
||||
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
||||
Tags = RequestHelpers.Split(tags, '|', true),
|
||||
OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
|
||||
Genres = RequestHelpers.Split(genres, '|', true),
|
||||
GenreIds = RequestHelpers.GetGuids(genreIds),
|
||||
StudioIds = RequestHelpers.GetGuids(studioIds),
|
||||
Person = person,
|
||||
PersonIds = RequestHelpers.GetGuids(personIds),
|
||||
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
|
||||
Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(),
|
||||
MinCommunityRating = minCommunityRating,
|
||||
DtoOptions = dtoOptions,
|
||||
SearchTerm = searchTerm,
|
||||
EnableTotalRecordCount = enableTotalRecordCount
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(parentId))
|
||||
{
|
||||
if (parentItem is Folder)
|
||||
{
|
||||
query.AncestorIds = new[] { new Guid(parentId) };
|
||||
}
|
||||
else
|
||||
{
|
||||
query.ItemIds = new[] { new Guid(parentId) };
|
||||
}
|
||||
}
|
||||
|
||||
// Studios
|
||||
if (!string.IsNullOrEmpty(studios))
|
||||
{
|
||||
query.StudioIds = studios.Split('|')
|
||||
.Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return _libraryManager.GetStudio(i);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}).Where(i => i != null)
|
||||
.Select(i => i!.Id)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
foreach (var filter in RequestHelpers.GetFilters(filters))
|
||||
{
|
||||
switch (filter)
|
||||
{
|
||||
case ItemFilter.Dislikes:
|
||||
query.IsLiked = false;
|
||||
break;
|
||||
case ItemFilter.IsFavorite:
|
||||
query.IsFavorite = true;
|
||||
break;
|
||||
case ItemFilter.IsFavoriteOrLikes:
|
||||
query.IsFavoriteOrLiked = true;
|
||||
break;
|
||||
case ItemFilter.IsFolder:
|
||||
query.IsFolder = true;
|
||||
break;
|
||||
case ItemFilter.IsNotFolder:
|
||||
query.IsFolder = false;
|
||||
break;
|
||||
case ItemFilter.IsPlayed:
|
||||
query.IsPlayed = true;
|
||||
break;
|
||||
case ItemFilter.IsResumable:
|
||||
query.IsResumable = true;
|
||||
break;
|
||||
case ItemFilter.IsUnplayed:
|
||||
query.IsPlayed = false;
|
||||
break;
|
||||
case ItemFilter.Likes:
|
||||
query.IsLiked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var result = new QueryResult<(BaseItem, ItemCounts)>();
|
||||
|
||||
var dtos = result.Items.Select(i =>
|
||||
{
|
||||
var (baseItem, counts) = i;
|
||||
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(includeItemTypes))
|
||||
{
|
||||
dto.ChildCount = counts.ItemCount;
|
||||
dto.ProgramCount = counts.ProgramCount;
|
||||
dto.SeriesCount = counts.SeriesCount;
|
||||
dto.EpisodeCount = counts.EpisodeCount;
|
||||
dto.MovieCount = counts.MovieCount;
|
||||
dto.TrailerCount = counts.TrailerCount;
|
||||
dto.AlbumCount = counts.AlbumCount;
|
||||
dto.SongCount = counts.SongCount;
|
||||
dto.ArtistCount = counts.ArtistCount;
|
||||
}
|
||||
|
||||
return dto;
|
||||
});
|
||||
|
||||
return new QueryResult<BaseItemDto>
|
||||
{
|
||||
Items = dtos.ToArray(),
|
||||
TotalRecordCount = result.TotalRecordCount
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a genre, by name.
|
||||
/// </summary>
|
||||
/// <param name="genreName">The genre name.</param>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <response code="200">Genres returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the genre.</returns>
|
||||
[HttpGet("{genreName}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<BaseItemDto> GetGenre([FromRoute] string genreName, [FromQuery] Guid? userId)
|
||||
{
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddClientFields(Request);
|
||||
|
||||
Genre item = new Genre();
|
||||
if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
var result = GetItemFromSlugName<Genre>(_libraryManager, genreName, dtoOptions);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
item = result;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
item = _libraryManager.GetGenre(genreName);
|
||||
}
|
||||
|
||||
if (userId.HasValue && !userId.Equals(Guid.Empty))
|
||||
{
|
||||
var user = _userManager.GetUserById(userId.Value);
|
||||
|
||||
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
|
||||
}
|
||||
|
||||
return _dtoService.GetBaseItemDto(item, dtoOptions);
|
||||
}
|
||||
|
||||
private T GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
|
||||
where T : BaseItem, new()
|
||||
{
|
||||
var result = libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
Name = name.Replace(BaseItem.SlugChar, '&'),
|
||||
IncludeItemTypes = new[] { typeof(T).Name },
|
||||
DtoOptions = dtoOptions
|
||||
}).OfType<T>().FirstOrDefault();
|
||||
|
||||
result ??= libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
Name = name.Replace(BaseItem.SlugChar, '/'),
|
||||
IncludeItemTypes = new[] { typeof(T).Name },
|
||||
DtoOptions = dtoOptions
|
||||
}).OfType<T>().FirstOrDefault();
|
||||
|
||||
result ??= libraryManager.GetItemList(new InternalItemsQuery
|
||||
{
|
||||
Name = name.Replace(BaseItem.SlugChar, '?'),
|
||||
IncludeItemTypes = new[] { typeof(T).Name },
|
||||
DtoOptions = dtoOptions
|
||||
}).OfType<T>().FirstOrDefault();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
154
Jellyfin.Api/Controllers/HlsSegmentController.cs
Normal file
154
Jellyfin.Api/Controllers/HlsSegmentController.cs
Normal file
|
@ -0,0 +1,154 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Net;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// The hls segment controller.
|
||||
/// </summary>
|
||||
[Route("")]
|
||||
public class HlsSegmentController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly TranscodingJobHelper _transcodingJobHelper;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HlsSegmentController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <param name="transcodingJobHelper">Initialized instance of the <see cref="TranscodingJobHelper"/>.</param>
|
||||
public HlsSegmentController(
|
||||
IFileSystem fileSystem,
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
TranscodingJobHelper transcodingJobHelper)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_transcodingJobHelper = transcodingJobHelper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the specified audio segment for an audio item.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="segmentId">The segment id.</param>
|
||||
/// <response code="200">Hls audio segment returned.</response>
|
||||
/// <returns>A <see cref="FileStreamResult"/> containing the audio stream.</returns>
|
||||
// Can't require authentication just yet due to seeing some requests come from Chrome without full query string
|
||||
// [Authenticated]
|
||||
[HttpGet("Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")]
|
||||
[HttpGet("Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
||||
public ActionResult GetHlsAudioSegmentLegacy([FromRoute] string itemId, [FromRoute] string segmentId)
|
||||
{
|
||||
// TODO: Deprecate with new iOS app
|
||||
var file = segmentId + Path.GetExtension(Request.Path);
|
||||
file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
|
||||
|
||||
return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a hls video playlist.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The video id.</param>
|
||||
/// <param name="playlistId">The playlist id.</param>
|
||||
/// <response code="200">Hls video playlist returned.</response>
|
||||
/// <returns>A <see cref="FileStreamResult"/> containing the playlist.</returns>
|
||||
[HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
||||
public ActionResult GetHlsPlaylistLegacy([FromRoute] string itemId, [FromRoute] string playlistId)
|
||||
{
|
||||
var file = playlistId + Path.GetExtension(Request.Path);
|
||||
file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
|
||||
|
||||
return GetFileResult(file, file);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops an active encoding.
|
||||
/// </summary>
|
||||
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
||||
/// <param name="playSessionId">The play session id.</param>
|
||||
/// <response code="204">Encoding stopped successfully.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpDelete("Videos/ActiveEncodings")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult StopEncodingProcess([FromQuery] string deviceId, [FromQuery] string playSessionId)
|
||||
{
|
||||
_transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a hls video segment.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="playlistId">The playlist id.</param>
|
||||
/// <param name="segmentId">The segment id.</param>
|
||||
/// <param name="segmentContainer">The segment container.</param>
|
||||
/// <response code="200">Hls video segment returned.</response>
|
||||
/// <returns>A <see cref="FileStreamResult"/> containing the video segment.</returns>
|
||||
// Can't require authentication just yet due to seeing some requests come from Chrome without full query string
|
||||
// [Authenticated]
|
||||
[HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
||||
public ActionResult GetHlsVideoSegmentLegacy(
|
||||
[FromRoute] string itemId,
|
||||
[FromRoute] string playlistId,
|
||||
[FromRoute] string segmentId,
|
||||
[FromRoute] string segmentContainer)
|
||||
{
|
||||
var file = segmentId + Path.GetExtension(Request.Path);
|
||||
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
|
||||
|
||||
file = Path.Combine(transcodeFolderPath, file);
|
||||
|
||||
var normalizedPlaylistId = playlistId;
|
||||
|
||||
var playlistPath = _fileSystem.GetFilePaths(transcodeFolderPath)
|
||||
.FirstOrDefault(i =>
|
||||
string.Equals(Path.GetExtension(i), ".m3u8", StringComparison.OrdinalIgnoreCase)
|
||||
&& i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1);
|
||||
|
||||
return GetFileResult(file, playlistPath);
|
||||
}
|
||||
|
||||
private ActionResult GetFileResult(string path, string playlistPath)
|
||||
{
|
||||
var transcodingJob = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
|
||||
|
||||
Response.OnCompleted(() =>
|
||||
{
|
||||
if (transcodingJob != null)
|
||||
{
|
||||
_transcodingJobHelper.OnTranscodeEndRequest(transcodingJob);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path)!, false, this);
|
||||
}
|
||||
}
|
||||
}
|
231
Jellyfin.Api/Controllers/ImageByNameController.cs
Normal file
231
Jellyfin.Api/Controllers/ImageByNameController.cs
Normal file
|
@ -0,0 +1,231 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Net;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Images By Name Controller.
|
||||
/// </summary>
|
||||
[Route("Images")]
|
||||
public class ImageByNameController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IServerApplicationPaths _applicationPaths;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageByNameController" /> class.
|
||||
/// </summary>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager" /> interface.</param>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem" /> interface.</param>
|
||||
public ImageByNameController(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
IFileSystem fileSystem)
|
||||
{
|
||||
_applicationPaths = serverConfigurationManager.ApplicationPaths;
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all general images.
|
||||
/// </summary>
|
||||
/// <response code="200">Retrieved list of images.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the list of images.</returns>
|
||||
[HttpGet("General")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<ImageByNameInfo>> GetGeneralImages()
|
||||
{
|
||||
return GetImageList(_applicationPaths.GeneralPath, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get General Image.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the image.</param>
|
||||
/// <param name="type">Image Type (primary, backdrop, logo, etc).</param>
|
||||
/// <response code="200">Image stream retrieved.</response>
|
||||
/// <response code="404">Image not found.</response>
|
||||
/// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
|
||||
[HttpGet("General/{name}/{type}")]
|
||||
[AllowAnonymous]
|
||||
[Produces(MediaTypeNames.Application.Octet)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<FileStreamResult> GetGeneralImage([FromRoute, Required] string? name, [FromRoute, Required] string? type)
|
||||
{
|
||||
var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase)
|
||||
? "folder"
|
||||
: type;
|
||||
|
||||
var path = BaseItem.SupportedImageExtensions
|
||||
.Select(i => Path.Combine(_applicationPaths.GeneralPath, name, filename + i))
|
||||
.FirstOrDefault(System.IO.File.Exists);
|
||||
|
||||
if (path == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var contentType = MimeTypes.GetMimeType(path);
|
||||
return File(System.IO.File.OpenRead(path), contentType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all general images.
|
||||
/// </summary>
|
||||
/// <response code="200">Retrieved list of images.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the list of images.</returns>
|
||||
[HttpGet("Ratings")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<ImageByNameInfo>> GetRatingImages()
|
||||
{
|
||||
return GetImageList(_applicationPaths.RatingsPath, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get rating image.
|
||||
/// </summary>
|
||||
/// <param name="theme">The theme to get the image from.</param>
|
||||
/// <param name="name">The name of the image.</param>
|
||||
/// <response code="200">Image stream retrieved.</response>
|
||||
/// <response code="404">Image not found.</response>
|
||||
/// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
|
||||
[HttpGet("Ratings/{theme}/{name}")]
|
||||
[AllowAnonymous]
|
||||
[Produces(MediaTypeNames.Application.Octet)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<FileStreamResult> GetRatingImage(
|
||||
[FromRoute, Required] string? theme,
|
||||
[FromRoute, Required] string? name)
|
||||
{
|
||||
return GetImageFile(_applicationPaths.RatingsPath, theme, name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all media info images.
|
||||
/// </summary>
|
||||
/// <response code="200">Image list retrieved.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the list of images.</returns>
|
||||
[HttpGet("MediaInfo")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<ImageByNameInfo>> GetMediaInfoImages()
|
||||
{
|
||||
return GetImageList(_applicationPaths.MediaInfoImagesPath, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get media info image.
|
||||
/// </summary>
|
||||
/// <param name="theme">The theme to get the image from.</param>
|
||||
/// <param name="name">The name of the image.</param>
|
||||
/// <response code="200">Image stream retrieved.</response>
|
||||
/// <response code="404">Image not found.</response>
|
||||
/// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
|
||||
[HttpGet("MediaInfo/{theme}/{name}")]
|
||||
[AllowAnonymous]
|
||||
[Produces(MediaTypeNames.Application.Octet)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<FileStreamResult> GetMediaInfoImage(
|
||||
[FromRoute, Required] string? theme,
|
||||
[FromRoute, Required] string? name)
|
||||
{
|
||||
return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal FileHelper.
|
||||
/// </summary>
|
||||
/// <param name="basePath">Path to begin search.</param>
|
||||
/// <param name="theme">Theme to search.</param>
|
||||
/// <param name="name">File name to search for.</param>
|
||||
/// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
|
||||
private ActionResult<FileStreamResult> GetImageFile(string basePath, string? theme, string? name)
|
||||
{
|
||||
var themeFolder = Path.Combine(basePath, theme);
|
||||
if (Directory.Exists(themeFolder))
|
||||
{
|
||||
var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, name + i))
|
||||
.FirstOrDefault(System.IO.File.Exists);
|
||||
|
||||
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
|
||||
{
|
||||
var contentType = MimeTypes.GetMimeType(path);
|
||||
return File(System.IO.File.OpenRead(path), contentType);
|
||||
}
|
||||
}
|
||||
|
||||
var allFolder = Path.Combine(basePath, "all");
|
||||
if (Directory.Exists(allFolder))
|
||||
{
|
||||
var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, name + i))
|
||||
.FirstOrDefault(System.IO.File.Exists);
|
||||
|
||||
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
|
||||
{
|
||||
var contentType = MimeTypes.GetMimeType(path);
|
||||
return File(System.IO.File.OpenRead(path), contentType);
|
||||
}
|
||||
}
|
||||
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
private List<ImageByNameInfo> GetImageList(string path, bool supportsThemes)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _fileSystem.GetFiles(path, BaseItem.SupportedImageExtensions, false, true)
|
||||
.Select(i => new ImageByNameInfo
|
||||
{
|
||||
Name = _fileSystem.GetFileNameWithoutExtension(i),
|
||||
FileLength = i.Length,
|
||||
|
||||
// For themeable images, use the Theme property
|
||||
// For general images, the same object structure is fine,
|
||||
// but it's not owned by a theme, so call it Context
|
||||
Theme = supportsThemes ? GetThemeName(i.FullName, path) : null,
|
||||
Context = supportsThemes ? null : GetThemeName(i.FullName, path),
|
||||
Format = i.Extension.ToLowerInvariant().TrimStart('.')
|
||||
})
|
||||
.OrderBy(i => i.Name)
|
||||
.ToList();
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return new List<ImageByNameInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
private string? GetThemeName(string path, string rootImagePath)
|
||||
{
|
||||
var parentName = Path.GetDirectoryName(path);
|
||||
|
||||
if (string.Equals(parentName, rootImagePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
parentName = Path.GetFileName(parentName);
|
||||
|
||||
return string.Equals(parentName, "all", StringComparison.OrdinalIgnoreCase) ? null : parentName;
|
||||
}
|
||||
}
|
||||
}
|
1304
Jellyfin.Api/Controllers/ImageController.cs
Normal file
1304
Jellyfin.Api/Controllers/ImageController.cs
Normal file
File diff suppressed because it is too large
Load Diff
330
Jellyfin.Api/Controllers/InstantMixController.cs
Normal file
330
Jellyfin.Api/Controllers/InstantMixController.cs
Normal file
|
@ -0,0 +1,330 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// The instant mix controller.
|
||||
/// </summary>
|
||||
[Route("")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
public class InstantMixController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IDtoService _dtoService;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IMusicManager _musicManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InstantMixController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
||||
/// <param name="musicManager">Instance of the <see cref="IMusicManager"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
public InstantMixController(
|
||||
IUserManager userManager,
|
||||
IDtoService dtoService,
|
||||
IMusicManager musicManager,
|
||||
ILibraryManager libraryManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_dtoService = dtoService;
|
||||
_musicManager = musicManager;
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instant playlist based on a given song.
|
||||
/// </summary>
|
||||
/// <param name="id">The item id.</param>
|
||||
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
|
||||
/// <param name="enableImages">Optional. Include image information in output.</param>
|
||||
/// <param name="enableUserData">Optional. Include user data.</param>
|
||||
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
|
||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <response code="200">Instant playlist returned.</response>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
||||
[HttpGet("Songs/{id}/InstantMix")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromSong(
|
||||
[FromRoute] Guid id,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(id);
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||
return GetResult(items, user, limit, dtoOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instant playlist based on a given song.
|
||||
/// </summary>
|
||||
/// <param name="id">The item id.</param>
|
||||
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
|
||||
/// <param name="enableImages">Optional. Include image information in output.</param>
|
||||
/// <param name="enableUserData">Optional. Include user data.</param>
|
||||
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
|
||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <response code="200">Instant playlist returned.</response>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
||||
[HttpGet("Albums/{id}/InstantMix")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromAlbum(
|
||||
[FromRoute] Guid id,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes)
|
||||
{
|
||||
var album = _libraryManager.GetItemById(id);
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
|
||||
return GetResult(items, user, limit, dtoOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instant playlist based on a given song.
|
||||
/// </summary>
|
||||
/// <param name="id">The item id.</param>
|
||||
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
|
||||
/// <param name="enableImages">Optional. Include image information in output.</param>
|
||||
/// <param name="enableUserData">Optional. Include user data.</param>
|
||||
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
|
||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <response code="200">Instant playlist returned.</response>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
||||
[HttpGet("Playlists/{id}/InstantMix")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromPlaylist(
|
||||
[FromRoute] Guid id,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes)
|
||||
{
|
||||
var playlist = (Playlist)_libraryManager.GetItemById(id);
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
|
||||
return GetResult(items, user, limit, dtoOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instant playlist based on a given song.
|
||||
/// </summary>
|
||||
/// <param name="name">The genre name.</param>
|
||||
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
|
||||
/// <param name="enableImages">Optional. Include image information in output.</param>
|
||||
/// <param name="enableUserData">Optional. Include user data.</param>
|
||||
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
|
||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <response code="200">Instant playlist returned.</response>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
||||
[HttpGet("MusicGenres/{name}/InstantMix")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenre(
|
||||
[FromRoute, Required] string? name,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes)
|
||||
{
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions);
|
||||
return GetResult(items, user, limit, dtoOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instant playlist based on a given song.
|
||||
/// </summary>
|
||||
/// <param name="id">The item id.</param>
|
||||
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
|
||||
/// <param name="enableImages">Optional. Include image information in output.</param>
|
||||
/// <param name="enableUserData">Optional. Include user data.</param>
|
||||
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
|
||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <response code="200">Instant playlist returned.</response>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
||||
[HttpGet("Artists/InstantMix")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
|
||||
[FromRoute] Guid id,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(id);
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||
return GetResult(items, user, limit, dtoOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instant playlist based on a given song.
|
||||
/// </summary>
|
||||
/// <param name="id">The item id.</param>
|
||||
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
|
||||
/// <param name="enableImages">Optional. Include image information in output.</param>
|
||||
/// <param name="enableUserData">Optional. Include user data.</param>
|
||||
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
|
||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <response code="200">Instant playlist returned.</response>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
||||
[HttpGet("MusicGenres/InstantMix")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenres(
|
||||
[FromRoute] Guid id,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(id);
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||
return GetResult(items, user, limit, dtoOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instant playlist based on a given song.
|
||||
/// </summary>
|
||||
/// <param name="id">The item id.</param>
|
||||
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
|
||||
/// <param name="enableImages">Optional. Include image information in output.</param>
|
||||
/// <param name="enableUserData">Optional. Include user data.</param>
|
||||
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
|
||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <response code="200">Instant playlist returned.</response>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
||||
[HttpGet("Items/{id}/InstantMix")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromItem(
|
||||
[FromRoute] Guid id,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(id);
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||
return GetResult(items, user, limit, dtoOptions);
|
||||
}
|
||||
|
||||
private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
|
||||
{
|
||||
var list = items;
|
||||
|
||||
var result = new QueryResult<BaseItemDto>
|
||||
{
|
||||
TotalRecordCount = list.Count
|
||||
};
|
||||
|
||||
if (limit.HasValue)
|
||||
{
|
||||
list = list.Take(limit.Value).ToList();
|
||||
}
|
||||
|
||||
var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user);
|
||||
|
||||
result.Items = returnList;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
364
Jellyfin.Api/Controllers/ItemLookupController.cs
Normal file
364
Jellyfin.Api/Controllers/ItemLookupController.cs
Normal file
|
@ -0,0 +1,364 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Item lookup controller.
|
||||
/// </summary>
|
||||
[Route("")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
public class ItemLookupController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly IServerApplicationPaths _appPaths;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILogger<ItemLookupController> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ItemLookupController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{ItemLookupController}"/> interface.</param>
|
||||
public ItemLookupController(
|
||||
IProviderManager providerManager,
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
ILogger<ItemLookupController> logger)
|
||||
{
|
||||
_providerManager = providerManager;
|
||||
_appPaths = serverConfigurationManager.ApplicationPaths;
|
||||
_fileSystem = fileSystem;
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the item's external id info.
|
||||
/// </summary>
|
||||
/// <param name="itemId">Item id.</param>
|
||||
/// <response code="200">External id info retrieved.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>List of external id info.</returns>
|
||||
[HttpGet("Items/{itemId}/ExternalIdInfos")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<IEnumerable<ExternalIdInfo>> GetExternalIdInfos([FromRoute] Guid itemId)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
if (item == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(_providerManager.GetExternalIdInfos(item));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get movie remote search.
|
||||
/// </summary>
|
||||
/// <param name="query">Remote search query.</param>
|
||||
/// <response code="200">Movie remote search executed.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
||||
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
||||
/// </returns>
|
||||
[HttpPost("Items/RemoteSearch/Movie")]
|
||||
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMovieRemoteSearchResults([FromBody, Required] RemoteSearchQuery<MovieInfo> query)
|
||||
{
|
||||
var results = await _providerManager.GetRemoteSearchResults<Movie, MovieInfo>(query, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get trailer remote search.
|
||||
/// </summary>
|
||||
/// <param name="query">Remote search query.</param>
|
||||
/// <response code="200">Trailer remote search executed.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
||||
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
||||
/// </returns>
|
||||
[HttpPost("Items/RemoteSearch/Trailer")]
|
||||
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetTrailerRemoteSearchResults([FromBody, Required] RemoteSearchQuery<TrailerInfo> query)
|
||||
{
|
||||
var results = await _providerManager.GetRemoteSearchResults<Trailer, TrailerInfo>(query, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get music video remote search.
|
||||
/// </summary>
|
||||
/// <param name="query">Remote search query.</param>
|
||||
/// <response code="200">Music video remote search executed.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
||||
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
||||
/// </returns>
|
||||
[HttpPost("Items/RemoteSearch/MusicVideo")]
|
||||
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicVideoRemoteSearchResults([FromBody, Required] RemoteSearchQuery<MusicVideoInfo> query)
|
||||
{
|
||||
var results = await _providerManager.GetRemoteSearchResults<MusicVideo, MusicVideoInfo>(query, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get series remote search.
|
||||
/// </summary>
|
||||
/// <param name="query">Remote search query.</param>
|
||||
/// <response code="200">Series remote search executed.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
||||
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
||||
/// </returns>
|
||||
[HttpPost("Items/RemoteSearch/Series")]
|
||||
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetSeriesRemoteSearchResults([FromBody, Required] RemoteSearchQuery<SeriesInfo> query)
|
||||
{
|
||||
var results = await _providerManager.GetRemoteSearchResults<Series, SeriesInfo>(query, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get box set remote search.
|
||||
/// </summary>
|
||||
/// <param name="query">Remote search query.</param>
|
||||
/// <response code="200">Box set remote search executed.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
||||
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
||||
/// </returns>
|
||||
[HttpPost("Items/RemoteSearch/BoxSet")]
|
||||
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBoxSetRemoteSearchResults([FromBody, Required] RemoteSearchQuery<BoxSetInfo> query)
|
||||
{
|
||||
var results = await _providerManager.GetRemoteSearchResults<BoxSet, BoxSetInfo>(query, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get music artist remote search.
|
||||
/// </summary>
|
||||
/// <param name="query">Remote search query.</param>
|
||||
/// <response code="200">Music artist remote search executed.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
||||
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
||||
/// </returns>
|
||||
[HttpPost("Items/RemoteSearch/MusicArtist")]
|
||||
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicArtistRemoteSearchResults([FromBody, Required] RemoteSearchQuery<ArtistInfo> query)
|
||||
{
|
||||
var results = await _providerManager.GetRemoteSearchResults<MusicArtist, ArtistInfo>(query, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get music album remote search.
|
||||
/// </summary>
|
||||
/// <param name="query">Remote search query.</param>
|
||||
/// <response code="200">Music album remote search executed.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
||||
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
||||
/// </returns>
|
||||
[HttpPost("Items/RemoteSearch/MusicAlbum")]
|
||||
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicAlbumRemoteSearchResults([FromBody, Required] RemoteSearchQuery<AlbumInfo> query)
|
||||
{
|
||||
var results = await _providerManager.GetRemoteSearchResults<MusicAlbum, AlbumInfo>(query, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get person remote search.
|
||||
/// </summary>
|
||||
/// <param name="query">Remote search query.</param>
|
||||
/// <response code="200">Person remote search executed.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
||||
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
||||
/// </returns>
|
||||
[HttpPost("Items/RemoteSearch/Person")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetPersonRemoteSearchResults([FromBody, Required] RemoteSearchQuery<PersonLookupInfo> query)
|
||||
{
|
||||
var results = await _providerManager.GetRemoteSearchResults<Person, PersonLookupInfo>(query, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get book remote search.
|
||||
/// </summary>
|
||||
/// <param name="query">Remote search query.</param>
|
||||
/// <response code="200">Book remote search executed.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
||||
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
||||
/// </returns>
|
||||
[HttpPost("Items/RemoteSearch/Book")]
|
||||
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBookRemoteSearchResults([FromBody, Required] RemoteSearchQuery<BookInfo> query)
|
||||
{
|
||||
var results = await _providerManager.GetRemoteSearchResults<Book, BookInfo>(query, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a remote image.
|
||||
/// </summary>
|
||||
/// <param name="imageUrl">The image url.</param>
|
||||
/// <param name="providerName">The provider name.</param>
|
||||
/// <response code="200">Remote image retrieved.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
||||
/// The task result contains an <see cref="FileStreamResult"/> containing the images file stream.
|
||||
/// </returns>
|
||||
[HttpGet("Items/RemoteSearch/Image")]
|
||||
public async Task<ActionResult> GetRemoteSearchImage(
|
||||
[FromQuery, Required] string imageUrl,
|
||||
[FromQuery, Required] string providerName)
|
||||
{
|
||||
var urlHash = imageUrl.GetMD5();
|
||||
var pointerCachePath = GetFullCachePath(urlHash.ToString());
|
||||
|
||||
try
|
||||
{
|
||||
var contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
|
||||
if (System.IO.File.Exists(contentPath))
|
||||
{
|
||||
await using var fileStreamExisting = System.IO.File.OpenRead(pointerCachePath);
|
||||
return new FileStreamResult(fileStreamExisting, MediaTypeNames.Application.Octet);
|
||||
}
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
// Means the file isn't cached yet
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Means the file isn't cached yet
|
||||
}
|
||||
|
||||
await DownloadImage(providerName, imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
|
||||
|
||||
// Read the pointer file again
|
||||
await using var fileStream = System.IO.File.OpenRead(pointerCachePath);
|
||||
return new FileStreamResult(fileStream, MediaTypeNames.Application.Octet);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies search criteria to an item and refreshes metadata.
|
||||
/// </summary>
|
||||
/// <param name="itemId">Item id.</param>
|
||||
/// <param name="searchResult">The remote search result.</param>
|
||||
/// <param name="replaceAllImages">Optional. Whether or not to replace all images. Default: True.</param>
|
||||
/// <response code="204">Item metadata refreshed.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
||||
/// The task result contains an <see cref="NoContentResult"/>.
|
||||
/// </returns>
|
||||
[HttpPost("Items/RemoteSearch/Apply/{id}")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
public async Task<ActionResult> ApplySearchCriteria(
|
||||
[FromRoute] Guid itemId,
|
||||
[FromBody, Required] RemoteSearchResult searchResult,
|
||||
[FromQuery] bool replaceAllImages = true)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
_logger.LogInformation(
|
||||
"Setting provider id's to item {0}-{1}: {2}",
|
||||
item.Id,
|
||||
item.Name,
|
||||
JsonSerializer.Serialize(searchResult.ProviderIds));
|
||||
|
||||
// Since the refresh process won't erase provider Ids, we need to set this explicitly now.
|
||||
item.ProviderIds = searchResult.ProviderIds;
|
||||
await _providerManager.RefreshFullItem(
|
||||
item,
|
||||
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
||||
{
|
||||
MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
|
||||
ImageRefreshMode = MetadataRefreshMode.FullRefresh,
|
||||
ReplaceAllMetadata = true,
|
||||
ReplaceAllImages = replaceAllImages,
|
||||
SearchResult = searchResult
|
||||
}, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads the image.
|
||||
/// </summary>
|
||||
/// <param name="providerName">Name of the provider.</param>
|
||||
/// <param name="url">The URL.</param>
|
||||
/// <param name="urlHash">The URL hash.</param>
|
||||
/// <param name="pointerCachePath">The pointer cache path.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath)
|
||||
{
|
||||
var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false);
|
||||
var ext = result.ContentType.Split('/').Last();
|
||||
var fullCachePath = GetFullCachePath(urlHash + "." + ext);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
|
||||
await using (var stream = result.Content)
|
||||
{
|
||||
await using var fileStream = new FileStream(
|
||||
fullCachePath,
|
||||
FileMode.Create,
|
||||
FileAccess.Write,
|
||||
FileShare.Read,
|
||||
IODefaults.FileStreamBufferSize,
|
||||
true);
|
||||
|
||||
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
|
||||
await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full cache path.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private string GetFullCachePath(string filename)
|
||||
=> Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
|
||||
}
|
||||
}
|
85
Jellyfin.Api/Controllers/ItemRefreshController.cs
Normal file
85
Jellyfin.Api/Controllers/ItemRefreshController.cs
Normal file
|
@ -0,0 +1,85 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Item Refresh Controller.
|
||||
/// </summary>
|
||||
[Route("Items")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
public class ItemRefreshController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ItemRefreshController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param>
|
||||
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
|
||||
public ItemRefreshController(
|
||||
ILibraryManager libraryManager,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_providerManager = providerManager;
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes metadata for an item.
|
||||
/// </summary>
|
||||
/// <param name="itemId">Item id.</param>
|
||||
/// <param name="metadataRefreshMode">(Optional) Specifies the metadata refresh mode.</param>
|
||||
/// <param name="imageRefreshMode">(Optional) Specifies the image refresh mode.</param>
|
||||
/// <param name="replaceAllMetadata">(Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh.</param>
|
||||
/// <param name="replaceAllImages">(Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh.</param>
|
||||
/// <response code="204">Item metadata refresh queued.</response>
|
||||
/// <response code="404">Item to refresh not found.</response>
|
||||
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
|
||||
[HttpPost("{itemId}/Refresh")]
|
||||
[Description("Refreshes metadata for an item.")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult Post(
|
||||
[FromRoute] Guid itemId,
|
||||
[FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None,
|
||||
[FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None,
|
||||
[FromQuery] bool replaceAllMetadata = false,
|
||||
[FromQuery] bool replaceAllImages = false)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
if (item == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var refreshOptions = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
||||
{
|
||||
MetadataRefreshMode = metadataRefreshMode,
|
||||
ImageRefreshMode = imageRefreshMode,
|
||||
ReplaceAllImages = replaceAllImages,
|
||||
ReplaceAllMetadata = replaceAllMetadata,
|
||||
ForceSave = metadataRefreshMode == MetadataRefreshMode.FullRefresh
|
||||
|| imageRefreshMode == MetadataRefreshMode.FullRefresh
|
||||
|| replaceAllImages
|
||||
|| replaceAllMetadata,
|
||||
IsAutomated = false
|
||||
};
|
||||
|
||||
_providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,215 +1,100 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace MediaBrowser.Api
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
[Route("/Items/{ItemId}", "POST", Summary = "Updates an item")]
|
||||
public class UpdateItem : BaseItemDto, IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "ItemId", Description = "The id of the item", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
public string ItemId { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Items/{ItemId}/MetadataEditor", "GET", Summary = "Gets metadata editor info for an item")]
|
||||
public class GetMetadataEditorInfo : IReturn<MetadataEditorInfo>
|
||||
{
|
||||
[ApiMember(Name = "ItemId", Description = "The id of the item", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")]
|
||||
public string ItemId { get; set; }
|
||||
}
|
||||
|
||||
[Route("/Items/{ItemId}/ContentType", "POST", Summary = "Updates an item's content type")]
|
||||
public class UpdateItemContentType : IReturnVoid
|
||||
{
|
||||
[ApiMember(Name = "ItemId", Description = "The id of the item", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")]
|
||||
public Guid ItemId { get; set; }
|
||||
|
||||
[ApiMember(Name = "ContentType", Description = "The content type of the item", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")]
|
||||
public string ContentType { get; set; }
|
||||
}
|
||||
|
||||
[Authenticated(Roles = "admin")]
|
||||
public class ItemUpdateService : BaseApiService
|
||||
/// <summary>
|
||||
/// Item update controller.
|
||||
/// </summary>
|
||||
[Route("")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
public class ItemUpdateController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly ILocalizationManager _localizationManager;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
|
||||
public ItemUpdateService(
|
||||
ILogger<ItemUpdateService> logger,
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
IHttpResultFactory httpResultFactory,
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ItemUpdateController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||
/// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
public ItemUpdateController(
|
||||
IFileSystem fileSystem,
|
||||
ILibraryManager libraryManager,
|
||||
IProviderManager providerManager,
|
||||
ILocalizationManager localizationManager)
|
||||
: base(logger, serverConfigurationManager, httpResultFactory)
|
||||
ILocalizationManager localizationManager,
|
||||
IServerConfigurationManager serverConfigurationManager)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_providerManager = providerManager;
|
||||
_localizationManager = localizationManager;
|
||||
_fileSystem = fileSystem;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
}
|
||||
|
||||
public object Get(GetMetadataEditorInfo request)
|
||||
/// <summary>
|
||||
/// Updates an item.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="request">The new item properties.</param>
|
||||
/// <response code="204">Item updated.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
|
||||
[HttpPost("Items/{itemId}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult UpdateItem([FromRoute] Guid itemId, [FromBody, Required] BaseItemDto request)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(request.ItemId);
|
||||
|
||||
var info = new MetadataEditorInfo
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
if (item == null)
|
||||
{
|
||||
ParentalRatingOptions = _localizationManager.GetParentalRatings().ToArray(),
|
||||
ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(),
|
||||
Countries = _localizationManager.GetCountries().ToArray(),
|
||||
Cultures = _localizationManager.GetCultures().ToArray()
|
||||
};
|
||||
|
||||
if (!item.IsVirtualItem && !(item is ICollectionFolder) && !(item is UserView) && !(item is AggregateFolder) && !(item is LiveTvChannel) && !(item is IItemByName) &&
|
||||
item.SourceType == SourceType.Library)
|
||||
{
|
||||
var inheritedContentType = _libraryManager.GetInheritedContentType(item);
|
||||
var configuredContentType = _libraryManager.GetConfiguredContentType(item);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(inheritedContentType) || !string.IsNullOrWhiteSpace(configuredContentType))
|
||||
{
|
||||
info.ContentTypeOptions = GetContentTypeOptions(true).ToArray();
|
||||
info.ContentType = configuredContentType;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(inheritedContentType) || string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
info.ContentTypeOptions = info.ContentTypeOptions
|
||||
.Where(i => string.IsNullOrWhiteSpace(i.Value) || string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return ToOptimizedResult(info);
|
||||
}
|
||||
|
||||
public void Post(UpdateItemContentType request)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(request.ItemId);
|
||||
var path = item.ContainingFolderPath;
|
||||
|
||||
var types = ServerConfigurationManager.Configuration.ContentTypes
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i.Name))
|
||||
.Where(i => !string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.ContentType))
|
||||
{
|
||||
types.Add(new NameValuePair
|
||||
{
|
||||
Name = path,
|
||||
Value = request.ContentType
|
||||
});
|
||||
}
|
||||
|
||||
ServerConfigurationManager.Configuration.ContentTypes = types.ToArray();
|
||||
ServerConfigurationManager.SaveConfiguration();
|
||||
}
|
||||
|
||||
private List<NameValuePair> GetContentTypeOptions(bool isForItem)
|
||||
{
|
||||
var list = new List<NameValuePair>();
|
||||
|
||||
if (isForItem)
|
||||
{
|
||||
list.Add(new NameValuePair
|
||||
{
|
||||
Name = "Inherit",
|
||||
Value = ""
|
||||
});
|
||||
}
|
||||
|
||||
list.Add(new NameValuePair
|
||||
{
|
||||
Name = "Movies",
|
||||
Value = "movies"
|
||||
});
|
||||
list.Add(new NameValuePair
|
||||
{
|
||||
Name = "Music",
|
||||
Value = "music"
|
||||
});
|
||||
list.Add(new NameValuePair
|
||||
{
|
||||
Name = "Shows",
|
||||
Value = "tvshows"
|
||||
});
|
||||
|
||||
if (!isForItem)
|
||||
{
|
||||
list.Add(new NameValuePair
|
||||
{
|
||||
Name = "Books",
|
||||
Value = "books"
|
||||
});
|
||||
}
|
||||
|
||||
list.Add(new NameValuePair
|
||||
{
|
||||
Name = "HomeVideos",
|
||||
Value = "homevideos"
|
||||
});
|
||||
list.Add(new NameValuePair
|
||||
{
|
||||
Name = "MusicVideos",
|
||||
Value = "musicvideos"
|
||||
});
|
||||
list.Add(new NameValuePair
|
||||
{
|
||||
Name = "Photos",
|
||||
Value = "photos"
|
||||
});
|
||||
|
||||
if (!isForItem)
|
||||
{
|
||||
list.Add(new NameValuePair
|
||||
{
|
||||
Name = "MixedContent",
|
||||
Value = ""
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var val in list)
|
||||
{
|
||||
val.Name = _localizationManager.GetLocalizedString(val.Name);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public void Post(UpdateItem request)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(request.ItemId);
|
||||
|
||||
var newLockData = request.LockData ?? false;
|
||||
var isLockedChanged = item.IsLocked != newLockData;
|
||||
|
||||
var series = item as Series;
|
||||
var displayOrderChanged = series != null && !string.Equals(series.DisplayOrder ?? string.Empty, request.DisplayOrder ?? string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
var displayOrderChanged = series != null && !string.Equals(
|
||||
series.DisplayOrder ?? string.Empty,
|
||||
request.DisplayOrder ?? string.Empty,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// Do this first so that metadata savers can pull the updates from the database.
|
||||
if (request.People != null)
|
||||
{
|
||||
_libraryManager.UpdatePeople(item, request.People.Select(x => new PersonInfo { Name = x.Name, Role = x.Role, Type = x.Type }).ToList());
|
||||
_libraryManager.UpdatePeople(
|
||||
item,
|
||||
request.People.Select(x => new PersonInfo
|
||||
{
|
||||
Name = x.Name,
|
||||
Role = x.Role,
|
||||
Type = x.Type
|
||||
}).ToList());
|
||||
}
|
||||
|
||||
UpdateItem(request, item);
|
||||
|
@ -232,7 +117,7 @@ namespace MediaBrowser.Api
|
|||
if (displayOrderChanged)
|
||||
{
|
||||
_providerManager.QueueRefresh(
|
||||
series.Id,
|
||||
series!.Id,
|
||||
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
||||
{
|
||||
MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
|
||||
|
@ -241,11 +126,101 @@ namespace MediaBrowser.Api
|
|||
},
|
||||
RefreshPriority.High);
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private DateTime NormalizeDateTime(DateTime val)
|
||||
/// <summary>
|
||||
/// Gets metadata editor info for an item.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <response code="200">Item metadata editor returned.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>An <see cref="OkResult"/> on success containing the metadata editor, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
|
||||
[HttpGet("Items/{itemId}/MetadataEditor")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<MetadataEditorInfo> GetMetadataEditorInfo([FromRoute] Guid itemId)
|
||||
{
|
||||
return DateTime.SpecifyKind(val, DateTimeKind.Utc);
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
|
||||
var info = new MetadataEditorInfo
|
||||
{
|
||||
ParentalRatingOptions = _localizationManager.GetParentalRatings().ToArray(),
|
||||
ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(),
|
||||
Countries = _localizationManager.GetCountries().ToArray(),
|
||||
Cultures = _localizationManager.GetCultures().ToArray()
|
||||
};
|
||||
|
||||
if (!item.IsVirtualItem
|
||||
&& !(item is ICollectionFolder)
|
||||
&& !(item is UserView)
|
||||
&& !(item is AggregateFolder)
|
||||
&& !(item is LiveTvChannel)
|
||||
&& !(item is IItemByName)
|
||||
&& item.SourceType == SourceType.Library)
|
||||
{
|
||||
var inheritedContentType = _libraryManager.GetInheritedContentType(item);
|
||||
var configuredContentType = _libraryManager.GetConfiguredContentType(item);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(inheritedContentType) ||
|
||||
!string.IsNullOrWhiteSpace(configuredContentType))
|
||||
{
|
||||
info.ContentTypeOptions = GetContentTypeOptions(true).ToArray();
|
||||
info.ContentType = configuredContentType;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(inheritedContentType)
|
||||
|| string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
info.ContentTypeOptions = info.ContentTypeOptions
|
||||
.Where(i => string.IsNullOrWhiteSpace(i.Value)
|
||||
|| string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an item's content type.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="contentType">The content type of the item.</param>
|
||||
/// <response code="204">Item content type updated.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
|
||||
[HttpPost("Items/{itemId}/ContentType")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, Required] string? contentType)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
if (item == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var path = item.ContainingFolderPath;
|
||||
|
||||
var types = _serverConfigurationManager.Configuration.ContentTypes
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i.Name))
|
||||
.Where(i => !string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(contentType))
|
||||
{
|
||||
types.Add(new NameValuePair
|
||||
{
|
||||
Name = path,
|
||||
Value = contentType
|
||||
});
|
||||
}
|
||||
|
||||
_serverConfigurationManager.Configuration.ContentTypes = types.ToArray();
|
||||
_serverConfigurationManager.SaveConfiguration();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private void UpdateItem(BaseItemDto request, BaseItem item)
|
||||
|
@ -361,24 +336,25 @@ namespace MediaBrowser.Api
|
|||
}
|
||||
}
|
||||
|
||||
if (item is Audio song)
|
||||
switch (item)
|
||||
{
|
||||
song.Album = request.Album;
|
||||
}
|
||||
|
||||
if (item is MusicVideo musicVideo)
|
||||
{
|
||||
musicVideo.Album = request.Album;
|
||||
}
|
||||
|
||||
if (item is Series series)
|
||||
{
|
||||
series.Status = GetSeriesStatus(request);
|
||||
|
||||
if (request.AirDays != null)
|
||||
case Audio song:
|
||||
song.Album = request.Album;
|
||||
break;
|
||||
case MusicVideo musicVideo:
|
||||
musicVideo.Album = request.Album;
|
||||
break;
|
||||
case Series series:
|
||||
{
|
||||
series.AirDays = request.AirDays;
|
||||
series.AirTime = request.AirTime;
|
||||
series.Status = GetSeriesStatus(request);
|
||||
|
||||
if (request.AirDays != null)
|
||||
{
|
||||
series.AirDays = request.AirDays;
|
||||
series.AirTime = request.AirTime;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -392,5 +368,81 @@ namespace MediaBrowser.Api
|
|||
|
||||
return (SeriesStatus)Enum.Parse(typeof(SeriesStatus), item.Status, true);
|
||||
}
|
||||
|
||||
private DateTime NormalizeDateTime(DateTime val)
|
||||
{
|
||||
return DateTime.SpecifyKind(val, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
private List<NameValuePair> GetContentTypeOptions(bool isForItem)
|
||||
{
|
||||
var list = new List<NameValuePair>();
|
||||
|
||||
if (isForItem)
|
||||
{
|
||||
list.Add(new NameValuePair
|
||||
{
|
||||
Name = "Inherit",
|
||||
Value = string.Empty
|
||||
});
|
||||
}
|
||||
|
||||
list.Add(new NameValuePair
|
||||
{
|
||||
Name = "Movies",
|
||||
Value = "movies"
|
||||
});
|
||||
list.Add(new NameValuePair
|
||||
{
|
||||
Name = "Music",
|
||||
Value = "music"
|
||||
});
|
||||
list.Add(new NameValuePair
|
||||
{
|
||||
Name = "Shows",
|
||||
Value = "tvshows"
|
||||
});
|
||||
|
||||
if (!isForItem)
|
||||
{
|
||||
list.Add(new NameValuePair
|
||||
{
|
||||
Name = "Books",
|
||||
Value = "books"
|
||||
});
|
||||
}
|
||||
|
||||
list.Add(new NameValuePair
|
||||
{
|
||||
Name = "HomeVideos",
|
||||
Value = "homevideos"
|
||||
});
|
||||
list.Add(new NameValuePair
|
||||
{
|
||||
Name = "MusicVideos",
|
||||
Value = "musicvideos"
|
||||
});
|
||||
list.Add(new NameValuePair
|
||||
{
|
||||
Name = "Photos",
|
||||
Value = "photos"
|
||||
});
|
||||
|
||||
if (!isForItem)
|
||||
{
|
||||
list.Add(new NameValuePair
|
||||
{
|
||||
Name = "MixedContent",
|
||||
Value = string.Empty
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var val in list)
|
||||
{
|
||||
val.Name = _localizationManager.GetLocalizedString(val.Name);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
593
Jellyfin.Api/Controllers/ItemsController.cs
Normal file
593
Jellyfin.Api/Controllers/ItemsController.cs
Normal file
|
@ -0,0 +1,593 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// The items controller.
|
||||
/// </summary>
|
||||
[Route("")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
public class ItemsController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly IDtoService _dtoService;
|
||||
private readonly ILogger<ItemsController> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ItemsController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
|
||||
public ItemsController(
|
||||
IUserManager userManager,
|
||||
ILibraryManager libraryManager,
|
||||
ILocalizationManager localization,
|
||||
IDtoService dtoService,
|
||||
ILogger<ItemsController> logger)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_libraryManager = libraryManager;
|
||||
_localization = localization;
|
||||
_dtoService = dtoService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets items based on a query.
|
||||
/// </summary>
|
||||
/// <param name="uId">The user id supplied in the /Users/{uid}/Items.</param>
|
||||
/// <param name="userId">The user id supplied as query parameter.</param>
|
||||
/// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
|
||||
/// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
|
||||
/// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
|
||||
/// <param name="hasSubtitles">Optional filter by items with subtitles.</param>
|
||||
/// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
|
||||
/// <param name="hasTrailer">Optional filter by items with trailers.</param>
|
||||
/// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
|
||||
/// <param name="parentIndexNumber">Optional filter by parent index number.</param>
|
||||
/// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
|
||||
/// <param name="isHd">Optional filter by items that are HD or not.</param>
|
||||
/// <param name="is4K">Optional filter by items that are 4K or not.</param>
|
||||
/// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
|
||||
/// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
|
||||
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
|
||||
/// <param name="minCriticRating">Optional filter by minimum critic rating.</param>
|
||||
/// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param>
|
||||
/// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param>
|
||||
/// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
|
||||
/// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
|
||||
/// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
|
||||
/// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
|
||||
/// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
|
||||
/// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
|
||||
/// <param name="excludeItemIds">Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
|
||||
/// <param name="searchTerm">Optional. Filter based on a search term.</param>
|
||||
/// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
|
||||
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
|
||||
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
|
||||
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
|
||||
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
|
||||
/// <param name="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
|
||||
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
|
||||
/// <param name="isPlayed">Optional filter by items that are played, or not.</param>
|
||||
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="enableUserData">Optional, include user data.</param>
|
||||
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
|
||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
|
||||
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
|
||||
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
|
||||
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param>
|
||||
/// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param>
|
||||
/// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param>
|
||||
/// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
|
||||
/// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.</param>
|
||||
/// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
|
||||
/// <param name="isLocked">Optional filter by items that are locked.</param>
|
||||
/// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
|
||||
/// <param name="hasOfficialRating">Optional filter by items that have official ratings.</param>
|
||||
/// <param name="collapseBoxSetItems">Whether or not to hide items behind their boxsets.</param>
|
||||
/// <param name="minWidth">Optional. Filter by the minimum width of the item.</param>
|
||||
/// <param name="minHeight">Optional. Filter by the minimum height of the item.</param>
|
||||
/// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
|
||||
/// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
|
||||
/// <param name="is3D">Optional filter by items that are 3D, or not.</param>
|
||||
/// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimeted.</param>
|
||||
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
|
||||
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
|
||||
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
|
||||
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
|
||||
/// <param name="enableImages">Optional, include image information in output.</param>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
|
||||
[HttpGet("Items")]
|
||||
[HttpGet("Users/{uId}/Items", Name = "GetItems_2")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetItems(
|
||||
[FromRoute] Guid? uId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? maxOfficialRating,
|
||||
[FromQuery] bool? hasThemeSong,
|
||||
[FromQuery] bool? hasThemeVideo,
|
||||
[FromQuery] bool? hasSubtitles,
|
||||
[FromQuery] bool? hasSpecialFeature,
|
||||
[FromQuery] bool? hasTrailer,
|
||||
[FromQuery] string? adjacentTo,
|
||||
[FromQuery] int? parentIndexNumber,
|
||||
[FromQuery] bool? hasParentalRating,
|
||||
[FromQuery] bool? isHd,
|
||||
[FromQuery] bool? is4K,
|
||||
[FromQuery] string? locationTypes,
|
||||
[FromQuery] string? excludeLocationTypes,
|
||||
[FromQuery] bool? isMissing,
|
||||
[FromQuery] bool? isUnaired,
|
||||
[FromQuery] double? minCommunityRating,
|
||||
[FromQuery] double? minCriticRating,
|
||||
[FromQuery] DateTime? minPremiereDate,
|
||||
[FromQuery] DateTime? minDateLastSaved,
|
||||
[FromQuery] DateTime? minDateLastSavedForUser,
|
||||
[FromQuery] DateTime? maxPremiereDate,
|
||||
[FromQuery] bool? hasOverview,
|
||||
[FromQuery] bool? hasImdbId,
|
||||
[FromQuery] bool? hasTmdbId,
|
||||
[FromQuery] bool? hasTvdbId,
|
||||
[FromQuery] string? excludeItemIds,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] bool? recursive,
|
||||
[FromQuery] string? searchTerm,
|
||||
[FromQuery] string? sortOrder,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery] string? filters,
|
||||
[FromQuery] bool? isFavorite,
|
||||
[FromQuery] string? mediaTypes,
|
||||
[FromQuery] string? imageTypes,
|
||||
[FromQuery] string? sortBy,
|
||||
[FromQuery] bool? isPlayed,
|
||||
[FromQuery] string? genres,
|
||||
[FromQuery] string? officialRatings,
|
||||
[FromQuery] string? tags,
|
||||
[FromQuery] string? years,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes,
|
||||
[FromQuery] string? person,
|
||||
[FromQuery] string? personIds,
|
||||
[FromQuery] string? personTypes,
|
||||
[FromQuery] string? studios,
|
||||
[FromQuery] string? artists,
|
||||
[FromQuery] string? excludeArtistIds,
|
||||
[FromQuery] string? artistIds,
|
||||
[FromQuery] string? albumArtistIds,
|
||||
[FromQuery] string? contributingArtistIds,
|
||||
[FromQuery] string? albums,
|
||||
[FromQuery] string? albumIds,
|
||||
[FromQuery] string? ids,
|
||||
[FromQuery] string? videoTypes,
|
||||
[FromQuery] string? minOfficialRating,
|
||||
[FromQuery] bool? isLocked,
|
||||
[FromQuery] bool? isPlaceHolder,
|
||||
[FromQuery] bool? hasOfficialRating,
|
||||
[FromQuery] bool? collapseBoxSetItems,
|
||||
[FromQuery] int? minWidth,
|
||||
[FromQuery] int? minHeight,
|
||||
[FromQuery] int? maxWidth,
|
||||
[FromQuery] int? maxHeight,
|
||||
[FromQuery] bool? is3D,
|
||||
[FromQuery] string? seriesStatus,
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
[FromQuery] string? nameLessThan,
|
||||
[FromQuery] string? studioIds,
|
||||
[FromQuery] string? genreIds,
|
||||
[FromQuery] bool enableTotalRecordCount = true,
|
||||
[FromQuery] bool? enableImages = true)
|
||||
{
|
||||
// use user id route parameter over query parameter
|
||||
userId = uId ?? userId;
|
||||
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
if (string.Equals(includeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
parentId = null;
|
||||
}
|
||||
|
||||
BaseItem? item = null;
|
||||
QueryResult<BaseItem> result;
|
||||
if (!string.IsNullOrEmpty(parentId))
|
||||
{
|
||||
item = _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
|
||||
item ??= _libraryManager.GetUserRootFolder();
|
||||
|
||||
if (!(item is Folder folder))
|
||||
{
|
||||
folder = _libraryManager.GetUserRootFolder();
|
||||
}
|
||||
|
||||
if (folder is IHasCollectionType hasCollectionType
|
||||
&& string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
recursive = true;
|
||||
includeItemTypes = "Playlist";
|
||||
}
|
||||
|
||||
bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id)
|
||||
// Assume all folders inside an EnabledChannel are enabled
|
||||
|| user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id);
|
||||
|
||||
var collectionFolders = _libraryManager.GetCollectionFolders(item);
|
||||
foreach (var collectionFolder in collectionFolders)
|
||||
{
|
||||
if (user.GetPreference(PreferenceKind.EnabledFolders).Contains(
|
||||
collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
|
||||
StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
isInEnabledFolder = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(item is UserRootFolder)
|
||||
&& !isInEnabledFolder
|
||||
&& !user.HasPermission(PermissionKind.EnableAllFolders)
|
||||
&& !user.HasPermission(PermissionKind.EnableAllChannels))
|
||||
{
|
||||
_logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name);
|
||||
return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
|
||||
}
|
||||
|
||||
if ((recursive.HasValue && recursive.Value) || !string.IsNullOrEmpty(ids) || !(item is UserRootFolder))
|
||||
{
|
||||
var query = new InternalItemsQuery(user!)
|
||||
{
|
||||
IsPlayed = isPlayed,
|
||||
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
|
||||
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
|
||||
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
|
||||
Recursive = recursive ?? false,
|
||||
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
|
||||
IsFavorite = isFavorite,
|
||||
Limit = limit,
|
||||
StartIndex = startIndex,
|
||||
IsMissing = isMissing,
|
||||
IsUnaired = isUnaired,
|
||||
CollapseBoxSetItems = collapseBoxSetItems,
|
||||
NameLessThan = nameLessThan,
|
||||
NameStartsWith = nameStartsWith,
|
||||
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
||||
HasImdbId = hasImdbId,
|
||||
IsPlaceHolder = isPlaceHolder,
|
||||
IsLocked = isLocked,
|
||||
MinWidth = minWidth,
|
||||
MinHeight = minHeight,
|
||||
MaxWidth = maxWidth,
|
||||
MaxHeight = maxHeight,
|
||||
Is3D = is3D,
|
||||
HasTvdbId = hasTvdbId,
|
||||
HasTmdbId = hasTmdbId,
|
||||
HasOverview = hasOverview,
|
||||
HasOfficialRating = hasOfficialRating,
|
||||
HasParentalRating = hasParentalRating,
|
||||
HasSpecialFeature = hasSpecialFeature,
|
||||
HasSubtitles = hasSubtitles,
|
||||
HasThemeSong = hasThemeSong,
|
||||
HasThemeVideo = hasThemeVideo,
|
||||
HasTrailer = hasTrailer,
|
||||
IsHD = isHd,
|
||||
Is4K = is4K,
|
||||
Tags = RequestHelpers.Split(tags, '|', true),
|
||||
OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
|
||||
Genres = RequestHelpers.Split(genres, '|', true),
|
||||
ArtistIds = RequestHelpers.GetGuids(artistIds),
|
||||
AlbumArtistIds = RequestHelpers.GetGuids(albumArtistIds),
|
||||
ContributingArtistIds = RequestHelpers.GetGuids(contributingArtistIds),
|
||||
GenreIds = RequestHelpers.GetGuids(genreIds),
|
||||
StudioIds = RequestHelpers.GetGuids(studioIds),
|
||||
Person = person,
|
||||
PersonIds = RequestHelpers.GetGuids(personIds),
|
||||
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
|
||||
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
|
||||
ImageTypes = RequestHelpers.Split(imageTypes, ',', true).Select(v => Enum.Parse<ImageType>(v, true)).ToArray(),
|
||||
VideoTypes = RequestHelpers.Split(videoTypes, ',', true).Select(v => Enum.Parse<VideoType>(v, true)).ToArray(),
|
||||
AdjacentTo = adjacentTo,
|
||||
ItemIds = RequestHelpers.GetGuids(ids),
|
||||
MinCommunityRating = minCommunityRating,
|
||||
MinCriticRating = minCriticRating,
|
||||
ParentId = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId),
|
||||
ParentIndexNumber = parentIndexNumber,
|
||||
EnableTotalRecordCount = enableTotalRecordCount,
|
||||
ExcludeItemIds = RequestHelpers.GetGuids(excludeItemIds),
|
||||
DtoOptions = dtoOptions,
|
||||
SearchTerm = searchTerm,
|
||||
MinDateLastSaved = minDateLastSaved?.ToUniversalTime(),
|
||||
MinDateLastSavedForUser = minDateLastSavedForUser?.ToUniversalTime(),
|
||||
MinPremiereDate = minPremiereDate?.ToUniversalTime(),
|
||||
MaxPremiereDate = maxPremiereDate?.ToUniversalTime(),
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ids) || !string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
query.CollapseBoxSetItems = false;
|
||||
}
|
||||
|
||||
foreach (var filter in RequestHelpers.GetFilters(filters!))
|
||||
{
|
||||
switch (filter)
|
||||
{
|
||||
case ItemFilter.Dislikes:
|
||||
query.IsLiked = false;
|
||||
break;
|
||||
case ItemFilter.IsFavorite:
|
||||
query.IsFavorite = true;
|
||||
break;
|
||||
case ItemFilter.IsFavoriteOrLikes:
|
||||
query.IsFavoriteOrLiked = true;
|
||||
break;
|
||||
case ItemFilter.IsFolder:
|
||||
query.IsFolder = true;
|
||||
break;
|
||||
case ItemFilter.IsNotFolder:
|
||||
query.IsFolder = false;
|
||||
break;
|
||||
case ItemFilter.IsPlayed:
|
||||
query.IsPlayed = true;
|
||||
break;
|
||||
case ItemFilter.IsResumable:
|
||||
query.IsResumable = true;
|
||||
break;
|
||||
case ItemFilter.IsUnplayed:
|
||||
query.IsPlayed = false;
|
||||
break;
|
||||
case ItemFilter.Likes:
|
||||
query.IsLiked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by Series Status
|
||||
if (!string.IsNullOrEmpty(seriesStatus))
|
||||
{
|
||||
query.SeriesStatuses = seriesStatus.Split(',').Select(d => (SeriesStatus)Enum.Parse(typeof(SeriesStatus), d, true)).ToArray();
|
||||
}
|
||||
|
||||
// ExcludeLocationTypes
|
||||
if (!string.IsNullOrEmpty(excludeLocationTypes))
|
||||
{
|
||||
if (excludeLocationTypes.Split(',').Select(d => (LocationType)Enum.Parse(typeof(LocationType), d, true)).ToArray().Contains(LocationType.Virtual))
|
||||
{
|
||||
query.IsVirtualItem = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(locationTypes))
|
||||
{
|
||||
var requestedLocationTypes = locationTypes.Split(',');
|
||||
if (requestedLocationTypes.Length > 0 && requestedLocationTypes.Length < 4)
|
||||
{
|
||||
query.IsVirtualItem = requestedLocationTypes.Contains(LocationType.Virtual.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
// Min official rating
|
||||
if (!string.IsNullOrWhiteSpace(minOfficialRating))
|
||||
{
|
||||
query.MinParentalRating = _localization.GetRatingLevel(minOfficialRating);
|
||||
}
|
||||
|
||||
// Max official rating
|
||||
if (!string.IsNullOrWhiteSpace(maxOfficialRating))
|
||||
{
|
||||
query.MaxParentalRating = _localization.GetRatingLevel(maxOfficialRating);
|
||||
}
|
||||
|
||||
// Artists
|
||||
if (!string.IsNullOrEmpty(artists))
|
||||
{
|
||||
query.ArtistIds = artists.Split('|').Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return _libraryManager.GetArtist(i, new DtoOptions(false));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}).Where(i => i != null).Select(i => i!.Id).ToArray();
|
||||
}
|
||||
|
||||
// ExcludeArtistIds
|
||||
if (!string.IsNullOrWhiteSpace(excludeArtistIds))
|
||||
{
|
||||
query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(albumIds))
|
||||
{
|
||||
query.AlbumIds = RequestHelpers.GetGuids(albumIds);
|
||||
}
|
||||
|
||||
// Albums
|
||||
if (!string.IsNullOrEmpty(albums))
|
||||
{
|
||||
query.AlbumIds = albums.Split('|').SelectMany(i =>
|
||||
{
|
||||
return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { nameof(MusicAlbum) }, Name = i, Limit = 1 });
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
// Studios
|
||||
if (!string.IsNullOrEmpty(studios))
|
||||
{
|
||||
query.StudioIds = studios.Split('|').Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return _libraryManager.GetStudio(i);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}).Where(i => i != null).Select(i => i!.Id).ToArray();
|
||||
}
|
||||
|
||||
// Apply default sorting if none requested
|
||||
if (query.OrderBy.Count == 0)
|
||||
{
|
||||
// Albums by artist
|
||||
if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], "MusicAlbum", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
query.OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.ProductionYear, SortOrder.Descending), new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) };
|
||||
}
|
||||
}
|
||||
|
||||
result = folder.GetItems(query);
|
||||
}
|
||||
else
|
||||
{
|
||||
var itemsArray = folder.GetChildren(user, true);
|
||||
result = new QueryResult<BaseItem> { Items = itemsArray, TotalRecordCount = itemsArray.Count, StartIndex = 0 };
|
||||
}
|
||||
|
||||
return new QueryResult<BaseItemDto> { StartIndex = startIndex.GetValueOrDefault(), TotalRecordCount = result.TotalRecordCount, Items = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user) };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets items based on a query.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <param name="startIndex">The start index.</param>
|
||||
/// <param name="limit">The item limit.</param>
|
||||
/// <param name="searchTerm">The search term.</param>
|
||||
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
|
||||
/// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
|
||||
/// <param name="enableUserData">Optional. Include user data.</param>
|
||||
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
|
||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
|
||||
/// <param name="enableImages">Optional. Include image information in output.</param>
|
||||
/// <response code="200">Items returned.</response>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items that are resumable.</returns>
|
||||
[HttpGet("Users/{userId}/Items/Resume")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetResumeItems(
|
||||
[FromRoute] Guid userId,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? searchTerm,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery] string? mediaTypes,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery] bool enableTotalRecordCount = true,
|
||||
[FromQuery] bool? enableImages = true)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
var ancestorIds = Array.Empty<Guid>();
|
||||
|
||||
var excludeFolderIds = user.GetPreference(PreferenceKind.LatestItemExcludes);
|
||||
if (parentIdGuid.Equals(Guid.Empty) && excludeFolderIds.Length > 0)
|
||||
{
|
||||
ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true)
|
||||
.Where(i => i is Folder)
|
||||
.Where(i => !excludeFolderIds.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
|
||||
.Select(i => i.Id)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
|
||||
{
|
||||
OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) },
|
||||
IsResumable = true,
|
||||
StartIndex = startIndex,
|
||||
Limit = limit,
|
||||
ParentId = parentIdGuid,
|
||||
Recursive = true,
|
||||
DtoOptions = dtoOptions,
|
||||
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
|
||||
IsVirtualItem = false,
|
||||
CollapseBoxSetItems = false,
|
||||
EnableTotalRecordCount = enableTotalRecordCount,
|
||||
AncestorIds = ancestorIds,
|
||||
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
|
||||
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
|
||||
SearchTerm = searchTerm
|
||||
});
|
||||
|
||||
var returnItems = _dtoService.GetBaseItemDtos(itemsResult.Items, dtoOptions, user);
|
||||
|
||||
return new QueryResult<BaseItemDto>
|
||||
{
|
||||
StartIndex = startIndex.GetValueOrDefault(),
|
||||
TotalRecordCount = itemsResult.TotalRecordCount,
|
||||
Items = returnItems
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
1036
Jellyfin.Api/Controllers/LibraryController.cs
Normal file
1036
Jellyfin.Api/Controllers/LibraryController.cs
Normal file
File diff suppressed because it is too large
Load Diff
331
Jellyfin.Api/Controllers/LibraryStructureController.cs
Normal file
331
Jellyfin.Api/Controllers/LibraryStructureController.cs
Normal file
|
@ -0,0 +1,331 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Models.LibraryStructureDto;
|
||||
using MediaBrowser.Common.Progress;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// The library structure controller.
|
||||
/// </summary>
|
||||
[Route("Library/VirtualFolders")]
|
||||
[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
|
||||
public class LibraryStructureController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IServerApplicationPaths _appPaths;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILibraryMonitor _libraryMonitor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LibraryStructureController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="libraryMonitor">Instance of <see cref="ILibraryMonitor"/> interface.</param>
|
||||
public LibraryStructureController(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILibraryManager libraryManager,
|
||||
ILibraryMonitor libraryMonitor)
|
||||
{
|
||||
_appPaths = serverConfigurationManager.ApplicationPaths;
|
||||
_libraryManager = libraryManager;
|
||||
_libraryMonitor = libraryMonitor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all virtual folders.
|
||||
/// </summary>
|
||||
/// <response code="200">Virtual folders retrieved.</response>
|
||||
/// <returns>An <see cref="IEnumerable{VirtualFolderInfo}"/> with the virtual folders.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<VirtualFolderInfo>> GetVirtualFolders()
|
||||
{
|
||||
return _libraryManager.GetVirtualFolders(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a virtual folder.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the virtual folder.</param>
|
||||
/// <param name="collectionType">The type of the collection.</param>
|
||||
/// <param name="paths">The paths of the virtual folder.</param>
|
||||
/// <param name="libraryOptionsDto">The library options.</param>
|
||||
/// <param name="refreshLibrary">Whether to refresh the library.</param>
|
||||
/// <response code="204">Folder added.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> AddVirtualFolder(
|
||||
[FromQuery] string? name,
|
||||
[FromQuery] string? collectionType,
|
||||
[FromQuery] string[] paths,
|
||||
[FromBody] LibraryOptionsDto? libraryOptionsDto,
|
||||
[FromQuery] bool refreshLibrary = false)
|
||||
{
|
||||
var libraryOptions = libraryOptionsDto?.LibraryOptions ?? new LibraryOptions();
|
||||
|
||||
if (paths != null && paths.Length > 0)
|
||||
{
|
||||
libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo { Path = i }).ToArray();
|
||||
}
|
||||
|
||||
await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a virtual folder.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the folder.</param>
|
||||
/// <param name="refreshLibrary">Whether to refresh the library.</param>
|
||||
/// <response code="204">Folder removed.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpDelete]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> RemoveVirtualFolder(
|
||||
[FromQuery] string? name,
|
||||
[FromQuery] bool refreshLibrary = false)
|
||||
{
|
||||
await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renames a virtual folder.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the virtual folder.</param>
|
||||
/// <param name="newName">The new name.</param>
|
||||
/// <param name="refreshLibrary">Whether to refresh the library.</param>
|
||||
/// <response code="204">Folder renamed.</response>
|
||||
/// <response code="404">Library doesn't exist.</response>
|
||||
/// <response code="409">Library already exists.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/> on success, a <see cref="NotFoundResult"/> if the library doesn't exist, a <see cref="ConflictResult"/> if the new name is already taken.</returns>
|
||||
/// <exception cref="ArgumentNullException">The new name may not be null.</exception>
|
||||
[HttpPost("Name")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public ActionResult RenameVirtualFolder(
|
||||
[FromQuery] string? name,
|
||||
[FromQuery] string? newName,
|
||||
[FromQuery] bool refreshLibrary = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(newName))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(newName));
|
||||
}
|
||||
|
||||
var rootFolderPath = _appPaths.DefaultUserViewsPath;
|
||||
|
||||
var currentPath = Path.Combine(rootFolderPath, name);
|
||||
var newPath = Path.Combine(rootFolderPath, newName);
|
||||
|
||||
if (!Directory.Exists(currentPath))
|
||||
{
|
||||
return NotFound("The media collection does not exist.");
|
||||
}
|
||||
|
||||
if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && Directory.Exists(newPath))
|
||||
{
|
||||
return Conflict($"The media library already exists at {newPath}.");
|
||||
}
|
||||
|
||||
_libraryMonitor.Stop();
|
||||
|
||||
try
|
||||
{
|
||||
// Changing capitalization. Handle windows case insensitivity
|
||||
if (string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var tempPath = Path.Combine(
|
||||
rootFolderPath,
|
||||
Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture));
|
||||
Directory.Move(currentPath, tempPath);
|
||||
currentPath = tempPath;
|
||||
}
|
||||
|
||||
Directory.Move(currentPath, newPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CollectionFolder.OnCollectionFolderChange();
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
// No need to start if scanning the library because it will handle it
|
||||
if (refreshLibrary)
|
||||
{
|
||||
await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Need to add a delay here or directory watchers may still pick up the changes
|
||||
// Have to block here to allow exceptions to bubble
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
_libraryMonitor.Start();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a media path to a library.
|
||||
/// </summary>
|
||||
/// <param name="mediaPathDto">The media path dto.</param>
|
||||
/// <param name="refreshLibrary">Whether to refresh the library.</param>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
/// <response code="204">Media path added.</response>
|
||||
/// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
|
||||
[HttpPost("Paths")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult AddMediaPath(
|
||||
[FromBody, Required] MediaPathDto mediaPathDto,
|
||||
[FromQuery] bool refreshLibrary = false)
|
||||
{
|
||||
_libraryMonitor.Stop();
|
||||
|
||||
try
|
||||
{
|
||||
var mediaPath = mediaPathDto.PathInfo ?? new MediaPathInfo { Path = mediaPathDto.Path };
|
||||
|
||||
_libraryManager.AddMediaPath(mediaPathDto.Name, mediaPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
// No need to start if scanning the library because it will handle it
|
||||
if (refreshLibrary)
|
||||
{
|
||||
await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Need to add a delay here or directory watchers may still pick up the changes
|
||||
// Have to block here to allow exceptions to bubble
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
_libraryMonitor.Start();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates a media path.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the library.</param>
|
||||
/// <param name="pathInfo">The path info.</param>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
/// <response code="204">Media path updated.</response>
|
||||
/// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
|
||||
[HttpPost("Paths/Update")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult UpdateMediaPath(
|
||||
[FromQuery] string? name,
|
||||
[FromBody] MediaPathInfo? pathInfo)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
_libraryManager.UpdateMediaPath(name, pathInfo);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a media path.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the library.</param>
|
||||
/// <param name="path">The path to remove.</param>
|
||||
/// <param name="refreshLibrary">Whether to refresh the library.</param>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
/// <response code="204">Media path removed.</response>
|
||||
/// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
|
||||
[HttpDelete("Paths")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult RemoveMediaPath(
|
||||
[FromQuery] string? name,
|
||||
[FromQuery] string? path,
|
||||
[FromQuery] bool refreshLibrary = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
_libraryMonitor.Stop();
|
||||
|
||||
try
|
||||
{
|
||||
_libraryManager.RemoveMediaPath(name, path);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
// No need to start if scanning the library because it will handle it
|
||||
if (refreshLibrary)
|
||||
{
|
||||
await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Need to add a delay here or directory watchers may still pick up the changes
|
||||
// Have to block here to allow exceptions to bubble
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
_libraryMonitor.Start();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update library options.
|
||||
/// </summary>
|
||||
/// <param name="id">The library name.</param>
|
||||
/// <param name="libraryOptions">The library options.</param>
|
||||
/// <response code="204">Library updated.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("LibraryOptions")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult UpdateLibraryOptions(
|
||||
[FromQuery] string? id,
|
||||
[FromBody] LibraryOptions? libraryOptions)
|
||||
{
|
||||
var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(id);
|
||||
|
||||
collectionFolder.UpdateLibraryOptions(libraryOptions);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
1238
Jellyfin.Api/Controllers/LiveTvController.cs
Normal file
1238
Jellyfin.Api/Controllers/LiveTvController.cs
Normal file
File diff suppressed because it is too large
Load Diff
76
Jellyfin.Api/Controllers/LocalizationController.cs
Normal file
76
Jellyfin.Api/Controllers/LocalizationController.cs
Normal file
|
@ -0,0 +1,76 @@
|
|||
using System.Collections.Generic;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Localization controller.
|
||||
/// </summary>
|
||||
[Authorize(Policy = Policies.FirstTimeSetupOrDefault)]
|
||||
public class LocalizationController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly ILocalizationManager _localization;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LocalizationController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||
public LocalizationController(ILocalizationManager localization)
|
||||
{
|
||||
_localization = localization;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets known cultures.
|
||||
/// </summary>
|
||||
/// <response code="200">Known cultures returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the list of cultures.</returns>
|
||||
[HttpGet("Cultures")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<CultureDto>> GetCultures()
|
||||
{
|
||||
return Ok(_localization.GetCultures());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets known countries.
|
||||
/// </summary>
|
||||
/// <response code="200">Known countries returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the list of countries.</returns>
|
||||
[HttpGet("Countries")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<CountryInfo>> GetCountries()
|
||||
{
|
||||
return Ok(_localization.GetCountries());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets known parental ratings.
|
||||
/// </summary>
|
||||
/// <response code="200">Known parental ratings returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the list of parental ratings.</returns>
|
||||
[HttpGet("ParentalRatings")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<ParentalRating>> GetParentalRatings()
|
||||
{
|
||||
return Ok(_localization.GetParentalRatings());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets localization options.
|
||||
/// </summary>
|
||||
/// <response code="200">Localization options returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the list of localization options.</returns>
|
||||
[HttpGet("Options")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<LocalizationOption>> GetLocalizationOptions()
|
||||
{
|
||||
return Ok(_localization.GetLocalizationOptions());
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user