commit
2d6699fd47
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Models.NotificationDtos;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Notifications;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Notifications;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// The notification controller.
|
||||
/// </summary>
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
public class NotificationsController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly INotificationManager _notificationManager;
|
||||
private readonly IUserManager _userManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NotificationsController" /> class.
|
||||
/// </summary>
|
||||
/// <param name="notificationManager">The notification manager.</param>
|
||||
/// <param name="userManager">The user manager.</param>
|
||||
public NotificationsController(INotificationManager notificationManager, IUserManager userManager)
|
||||
{
|
||||
_notificationManager = notificationManager;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a user's notifications.
|
||||
/// </summary>
|
||||
/// <response code="200">Notifications returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing a list of notifications.</returns>
|
||||
[HttpGet("{userId}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<NotificationResultDto> GetNotifications()
|
||||
{
|
||||
return new NotificationResultDto();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a user's notification summary.
|
||||
/// </summary>
|
||||
/// <response code="200">Summary of user's notifications returned.</response>
|
||||
/// <returns>An <cref see="OkResult"/> containing a summary of the users notifications.</returns>
|
||||
[HttpGet("{userId}/Summary")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<NotificationsSummaryDto> GetNotificationsSummary()
|
||||
{
|
||||
return new NotificationsSummaryDto();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets notification types.
|
||||
/// </summary>
|
||||
/// <response code="200">All notification types returned.</response>
|
||||
/// <returns>An <cref see="OkResult"/> containing a list of all notification types.</returns>
|
||||
[HttpGet("Types")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public IEnumerable<NotificationTypeInfo> GetNotificationTypes()
|
||||
{
|
||||
return _notificationManager.GetNotificationTypes();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets notification services.
|
||||
/// </summary>
|
||||
/// <response code="200">All notification services returned.</response>
|
||||
/// <returns>An <cref see="OkResult"/> containing a list of all notification services.</returns>
|
||||
[HttpGet("Services")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public IEnumerable<NameIdPair> GetNotificationServices()
|
||||
{
|
||||
return _notificationManager.GetNotificationServices();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a notification to all admins.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL of the notification.</param>
|
||||
/// <param name="level">The level of the notification.</param>
|
||||
/// <param name="name">The name of the notification.</param>
|
||||
/// <param name="description">The description of the notification.</param>
|
||||
/// <response code="204">Notification sent.</response>
|
||||
/// <returns>A <cref see="NoContentResult"/>.</returns>
|
||||
[HttpPost("Admin")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult CreateAdminNotification(
|
||||
[FromQuery] string? url,
|
||||
[FromQuery] NotificationLevel? level,
|
||||
[FromQuery] string name = "",
|
||||
[FromQuery] string description = "")
|
||||
{
|
||||
var notification = new NotificationRequest
|
||||
{
|
||||
Name = name,
|
||||
Description = description,
|
||||
Url = url,
|
||||
Level = level ?? NotificationLevel.Normal,
|
||||
UserIds = _userManager.Users
|
||||
.Where(user => user.HasPermission(PermissionKind.IsAdministrator))
|
||||
.Select(user => user.Id)
|
||||
.ToArray(),
|
||||
Date = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
_notificationManager.SendNotification(notification, CancellationToken.None);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets notifications as read.
|
||||
/// </summary>
|
||||
/// <response code="204">Notifications set as read.</response>
|
||||
/// <returns>A <cref see="NoContentResult"/>.</returns>
|
||||
[HttpPost("{userId}/Read")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult SetRead()
|
||||
{
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets notifications as unread.
|
||||
/// </summary>
|
||||
/// <response code="204">Notifications set as unread.</response>
|
||||
/// <returns>A <cref see="NoContentResult"/>.</returns>
|
||||
[HttpPost("{userId}/Unread")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult SetUnread()
|
||||
{
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Common.Updates;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Model.Updates;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Package Controller.
|
||||
/// </summary>
|
||||
[Route("")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
public class PackageController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IInstallationManager _installationManager;
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PackageController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
public PackageController(IInstallationManager installationManager, IServerConfigurationManager serverConfigurationManager)
|
||||
{
|
||||
_installationManager = installationManager;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a package by name or assembly GUID.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the package.</param>
|
||||
/// <param name="assemblyGuid">The GUID of the associated assembly.</param>
|
||||
/// <response code="200">Package retrieved.</response>
|
||||
/// <returns>A <see cref="PackageInfo"/> containing package information.</returns>
|
||||
[HttpGet("Packages/{name}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PackageInfo>> GetPackageInfo(
|
||||
[FromRoute] [Required] string? name,
|
||||
[FromQuery] string? assemblyGuid)
|
||||
{
|
||||
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
|
||||
var result = _installationManager.FilterPackages(
|
||||
packages,
|
||||
name,
|
||||
string.IsNullOrEmpty(assemblyGuid) ? default : Guid.Parse(assemblyGuid)).FirstOrDefault();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets available packages.
|
||||
/// </summary>
|
||||
/// <response code="200">Available packages returned.</response>
|
||||
/// <returns>An <see cref="PackageInfo"/> containing available packages information.</returns>
|
||||
[HttpGet("Packages")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<IEnumerable<PackageInfo>> GetPackages()
|
||||
{
|
||||
IEnumerable<PackageInfo> packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
|
||||
|
||||
return packages;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installs a package.
|
||||
/// </summary>
|
||||
/// <param name="name">Package name.</param>
|
||||
/// <param name="assemblyGuid">GUID of the associated assembly.</param>
|
||||
/// <param name="version">Optional version. Defaults to latest version.</param>
|
||||
/// <response code="204">Package found.</response>
|
||||
/// <response code="404">Package not found.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the package could not be found.</returns>
|
||||
[HttpPost("Packages/Installed/{name}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
public async Task<ActionResult> InstallPackage(
|
||||
[FromRoute] [Required] string? name,
|
||||
[FromQuery] string? assemblyGuid,
|
||||
[FromQuery] string? version)
|
||||
{
|
||||
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
|
||||
var package = _installationManager.GetCompatibleVersions(
|
||||
packages,
|
||||
name,
|
||||
string.IsNullOrEmpty(assemblyGuid) ? Guid.Empty : Guid.Parse(assemblyGuid),
|
||||
string.IsNullOrEmpty(version) ? null : Version.Parse(version)).FirstOrDefault();
|
||||
|
||||
if (package == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
await _installationManager.InstallPackage(package).ConfigureAwait(false);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels a package installation.
|
||||
/// </summary>
|
||||
/// <param name="packageId">Installation Id.</param>
|
||||
/// <response code="204">Installation cancelled.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/> on successfully cancelling a package installation.</returns>
|
||||
[HttpDelete("Packages/Installing/{packageId}")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult CancelPackageInstallation(
|
||||
[FromRoute] [Required] Guid packageId)
|
||||
{
|
||||
_installationManager.CancelInstallation(packageId);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all package repositories.
|
||||
/// </summary>
|
||||
/// <response code="200">Package repositories returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the list of package repositories.</returns>
|
||||
[HttpGet("Repositories")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<RepositoryInfo>> GetRepositories()
|
||||
{
|
||||
return _serverConfigurationManager.Configuration.PluginRepositories;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the enabled and existing package repositories.
|
||||
/// </summary>
|
||||
/// <param name="repositoryInfos">The list of package repositories.</param>
|
||||
/// <response code="204">Package repositories saved.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpOptions("Repositories")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult SetRepositories([FromBody] List<RepositoryInfo> repositoryInfos)
|
||||
{
|
||||
_serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos;
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,268 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Remote Images Controller.
|
||||
/// </summary>
|
||||
[Route("")]
|
||||
public class RemoteImageController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly IServerApplicationPaths _applicationPaths;
|
||||
private readonly IHttpClient _httpClient;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RemoteImageController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
|
||||
/// <param name="httpClient">Instance of the <see cref="IHttpClient"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
public RemoteImageController(
|
||||
IProviderManager providerManager,
|
||||
IServerApplicationPaths applicationPaths,
|
||||
IHttpClient httpClient,
|
||||
ILibraryManager libraryManager)
|
||||
{
|
||||
_providerManager = providerManager;
|
||||
_applicationPaths = applicationPaths;
|
||||
_httpClient = httpClient;
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets available remote images for an item.
|
||||
/// </summary>
|
||||
/// <param name="itemId">Item Id.</param>
|
||||
/// <param name="type">The image type.</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="providerName">Optional. The image provider to use.</param>
|
||||
/// <param name="includeAllLanguages">Optional. Include all languages.</param>
|
||||
/// <response code="200">Remote Images returned.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>Remote Image Result.</returns>
|
||||
[HttpGet("Items/{itemId}/RemoteImages")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<RemoteImageResult>> GetRemoteImages(
|
||||
[FromRoute] Guid itemId,
|
||||
[FromQuery] ImageType? type,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? providerName,
|
||||
[FromQuery] bool includeAllLanguages = false)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
if (item == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var images = await _providerManager.GetAvailableRemoteImages(
|
||||
item,
|
||||
new RemoteImageQuery(providerName ?? string.Empty)
|
||||
{
|
||||
IncludeAllLanguages = includeAllLanguages,
|
||||
IncludeDisabledProviders = true,
|
||||
ImageType = type
|
||||
}, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var imageArray = images.ToArray();
|
||||
var allProviders = _providerManager.GetRemoteImageProviderInfo(item);
|
||||
if (type.HasValue)
|
||||
{
|
||||
allProviders = allProviders.Where(o => o.SupportedImages.Contains(type.Value));
|
||||
}
|
||||
|
||||
var result = new RemoteImageResult
|
||||
{
|
||||
TotalRecordCount = imageArray.Length,
|
||||
Providers = allProviders.Select(o => o.Name)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray()
|
||||
};
|
||||
|
||||
if (startIndex.HasValue)
|
||||
{
|
||||
imageArray = imageArray.Skip(startIndex.Value).ToArray();
|
||||
}
|
||||
|
||||
if (limit.HasValue)
|
||||
{
|
||||
imageArray = imageArray.Take(limit.Value).ToArray();
|
||||
}
|
||||
|
||||
result.Images = imageArray;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets available remote image providers for an item.
|
||||
/// </summary>
|
||||
/// <param name="itemId">Item Id.</param>
|
||||
/// <response code="200">Returned remote image providers.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>List of remote image providers.</returns>
|
||||
[HttpGet("Items/{itemId}/RemoteImages/Providers")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<IEnumerable<ImageProviderInfo>> GetRemoteImageProviders([FromRoute] Guid itemId)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
if (item == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(_providerManager.GetRemoteImageProviderInfo(item));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a remote image.
|
||||
/// </summary>
|
||||
/// <param name="imageUrl">The image url.</param>
|
||||
/// <response code="200">Remote image returned.</response>
|
||||
/// <response code="404">Remote image not found.</response>
|
||||
/// <returns>Image Stream.</returns>
|
||||
[HttpGet("Images/Remote")]
|
||||
[Produces(MediaTypeNames.Application.Octet)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> GetRemoteImage([FromQuery, Required] string imageUrl)
|
||||
{
|
||||
var urlHash = imageUrl.GetMD5();
|
||||
var pointerCachePath = GetFullCachePath(urlHash.ToString());
|
||||
|
||||
string? contentPath = null;
|
||||
var hasFile = false;
|
||||
|
||||
try
|
||||
{
|
||||
contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
|
||||
if (System.IO.File.Exists(contentPath))
|
||||
{
|
||||
hasFile = true;
|
||||
}
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
// The file isn't cached yet
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// The file isn't cached yet
|
||||
}
|
||||
|
||||
if (!hasFile)
|
||||
{
|
||||
await DownloadImage(imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
|
||||
contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(contentPath))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var contentType = MimeTypes.GetMimeType(contentPath);
|
||||
return File(System.IO.File.OpenRead(contentPath), contentType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a remote image for an item.
|
||||
/// </summary>
|
||||
/// <param name="itemId">Item Id.</param>
|
||||
/// <param name="type">The image type.</param>
|
||||
/// <param name="imageUrl">The image url.</param>
|
||||
/// <response code="204">Remote image downloaded.</response>
|
||||
/// <response code="404">Remote image not found.</response>
|
||||
/// <returns>Download status.</returns>
|
||||
[HttpPost("Items/{itemId}/RemoteImages/Download")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> DownloadRemoteImage(
|
||||
[FromRoute] Guid itemId,
|
||||
[FromQuery, Required] ImageType type,
|
||||
[FromQuery] string? imageUrl)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
if (item == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
await _providerManager.SaveImage(item, imageUrl, type, null, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full cache path.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private string GetFullCachePath(string filename)
|
||||
{
|
||||
return Path.Combine(_applicationPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads the image.
|
||||
/// </summary>
|
||||
/// <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 url, Guid urlHash, string pointerCachePath)
|
||||
{
|
||||
using var result = await _httpClient.GetResponse(new HttpRequestOptions
|
||||
{
|
||||
Url = url,
|
||||
BufferContent = false
|
||||
}).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, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,161 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Scheduled Tasks Controller.
|
||||
/// </summary>
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
public class ScheduledTasksController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly ITaskManager _taskManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ScheduledTasksController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="taskManager">Instance of the <see cref="ITaskManager"/> interface.</param>
|
||||
public ScheduledTasksController(ITaskManager taskManager)
|
||||
{
|
||||
_taskManager = taskManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get tasks.
|
||||
/// </summary>
|
||||
/// <param name="isHidden">Optional filter tasks that are hidden, or not.</param>
|
||||
/// <param name="isEnabled">Optional filter tasks that are enabled, or not.</param>
|
||||
/// <response code="200">Scheduled tasks retrieved.</response>
|
||||
/// <returns>The list of scheduled tasks.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public IEnumerable<IScheduledTaskWorker> GetTasks(
|
||||
[FromQuery] bool? isHidden,
|
||||
[FromQuery] bool? isEnabled)
|
||||
{
|
||||
IEnumerable<IScheduledTaskWorker> tasks = _taskManager.ScheduledTasks.OrderBy(o => o.Name);
|
||||
|
||||
foreach (var task in tasks)
|
||||
{
|
||||
if (task.ScheduledTask is IConfigurableScheduledTask scheduledTask)
|
||||
{
|
||||
if (isHidden.HasValue && isHidden.Value != scheduledTask.IsHidden)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isEnabled.HasValue && isEnabled.Value != scheduledTask.IsEnabled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
yield return task;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get task by id.
|
||||
/// </summary>
|
||||
/// <param name="taskId">Task Id.</param>
|
||||
/// <response code="200">Task retrieved.</response>
|
||||
/// <response code="404">Task not found.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the task on success, or a <see cref="NotFoundResult"/> if the task could not be found.</returns>
|
||||
[HttpGet("{taskId}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<TaskInfo> GetTask([FromRoute, Required] string? taskId)
|
||||
{
|
||||
var task = _taskManager.ScheduledTasks.FirstOrDefault(i =>
|
||||
string.Equals(i.Id, taskId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (task == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return ScheduledTaskHelpers.GetTaskInfo(task);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start specified task.
|
||||
/// </summary>
|
||||
/// <param name="taskId">Task Id.</param>
|
||||
/// <response code="204">Task started.</response>
|
||||
/// <response code="404">Task not found.</response>
|
||||
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
|
||||
[HttpPost("Running/{taskId}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult StartTask([FromRoute] string? taskId)
|
||||
{
|
||||
var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
|
||||
o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (task == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
_taskManager.Execute(task, new TaskOptions());
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop specified task.
|
||||
/// </summary>
|
||||
/// <param name="taskId">Task Id.</param>
|
||||
/// <response code="204">Task stopped.</response>
|
||||
/// <response code="404">Task not found.</response>
|
||||
/// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
|
||||
[HttpDelete("Running/{taskId}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult StopTask([FromRoute, Required] string? taskId)
|
||||
{
|
||||
var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
|
||||
o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (task == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
_taskManager.Cancel(task);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update specified task triggers.
|
||||
/// </summary>
|
||||
/// <param name="taskId">Task Id.</param>
|
||||
/// <param name="triggerInfos">Triggers.</param>
|
||||
/// <response code="204">Task triggers updated.</response>
|
||||
/// <response code="404">Task not found.</response>
|
||||
/// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
|
||||
[HttpPost("{taskId}/Triggers")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult UpdateTask(
|
||||
[FromRoute, Required] string? taskId,
|
||||
[FromBody, Required] TaskTriggerInfo[] triggerInfos)
|
||||
{
|
||||
var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
|
||||
o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
|
||||
if (task == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
task.Triggers = triggerInfos;
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,269 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Search;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Search controller.
|
||||
/// </summary>
|
||||
[Route("Search/Hints")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
public class SearchController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly ISearchEngine _searchEngine;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IDtoService _dtoService;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SearchController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="searchEngine">Instance of <see cref="ISearchEngine"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
|
||||
/// <param name="imageProcessor">Instance of <see cref="IImageProcessor"/> interface.</param>
|
||||
public SearchController(
|
||||
ISearchEngine searchEngine,
|
||||
ILibraryManager libraryManager,
|
||||
IDtoService dtoService,
|
||||
IImageProcessor imageProcessor)
|
||||
{
|
||||
_searchEngine = searchEngine;
|
||||
_libraryManager = libraryManager;
|
||||
_dtoService = dtoService;
|
||||
_imageProcessor = imageProcessor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the search hint result.
|
||||
/// </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="userId">Optional. Supply a user id to search within a user's library or omit to search all.</param>
|
||||
/// <param name="searchTerm">The search term to filter on.</param>
|
||||
/// <param name="includeItemTypes">If specified, only results with the specified item types are returned. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="excludeItemTypes">If specified, results with these item types are filtered out. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="mediaTypes">If specified, only results with the specified media types are returned. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="parentId">If specified, only children of the parent are returned.</param>
|
||||
/// <param name="isMovie">Optional filter for movies.</param>
|
||||
/// <param name="isSeries">Optional filter for series.</param>
|
||||
/// <param name="isNews">Optional filter for news.</param>
|
||||
/// <param name="isKids">Optional filter for kids.</param>
|
||||
/// <param name="isSports">Optional filter for sports.</param>
|
||||
/// <param name="includePeople">Optional filter whether to include people.</param>
|
||||
/// <param name="includeMedia">Optional filter whether to include media.</param>
|
||||
/// <param name="includeGenres">Optional filter whether to include genres.</param>
|
||||
/// <param name="includeStudios">Optional filter whether to include studios.</param>
|
||||
/// <param name="includeArtists">Optional filter whether to include artists.</param>
|
||||
/// <response code="200">Search hint returned.</response>
|
||||
/// <returns>An <see cref="SearchHintResult"/> with the results of the search.</returns>
|
||||
[HttpGet]
|
||||
[Description("Gets search hints based on a search term")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<SearchHintResult> Get(
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery, Required] string? searchTerm,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? mediaTypes,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery] bool? isMovie,
|
||||
[FromQuery] bool? isSeries,
|
||||
[FromQuery] bool? isNews,
|
||||
[FromQuery] bool? isKids,
|
||||
[FromQuery] bool? isSports,
|
||||
[FromQuery] bool includePeople = true,
|
||||
[FromQuery] bool includeMedia = true,
|
||||
[FromQuery] bool includeGenres = true,
|
||||
[FromQuery] bool includeStudios = true,
|
||||
[FromQuery] bool includeArtists = true)
|
||||
{
|
||||
var result = _searchEngine.GetSearchHints(new SearchQuery
|
||||
{
|
||||
Limit = limit,
|
||||
SearchTerm = searchTerm,
|
||||
IncludeArtists = includeArtists,
|
||||
IncludeGenres = includeGenres,
|
||||
IncludeMedia = includeMedia,
|
||||
IncludePeople = includePeople,
|
||||
IncludeStudios = includeStudios,
|
||||
StartIndex = startIndex,
|
||||
UserId = userId ?? Guid.Empty,
|
||||
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
|
||||
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
|
||||
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
|
||||
ParentId = parentId,
|
||||
|
||||
IsKids = isKids,
|
||||
IsMovie = isMovie,
|
||||
IsNews = isNews,
|
||||
IsSeries = isSeries,
|
||||
IsSports = isSports
|
||||
});
|
||||
|
||||
return new SearchHintResult
|
||||
{
|
||||
TotalRecordCount = result.TotalRecordCount,
|
||||
SearchHints = result.Items.Select(GetSearchHintResult).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the search hint result.
|
||||
/// </summary>
|
||||
/// <param name="hintInfo">The hint info.</param>
|
||||
/// <returns>SearchHintResult.</returns>
|
||||
private SearchHint GetSearchHintResult(SearchHintInfo hintInfo)
|
||||
{
|
||||
var item = hintInfo.Item;
|
||||
|
||||
var result = new SearchHint
|
||||
{
|
||||
Name = item.Name,
|
||||
IndexNumber = item.IndexNumber,
|
||||
ParentIndexNumber = item.ParentIndexNumber,
|
||||
Id = item.Id,
|
||||
Type = item.GetClientTypeName(),
|
||||
MediaType = item.MediaType,
|
||||
MatchedTerm = hintInfo.MatchedTerm,
|
||||
RunTimeTicks = item.RunTimeTicks,
|
||||
ProductionYear = item.ProductionYear,
|
||||
ChannelId = item.ChannelId,
|
||||
EndDate = item.EndDate
|
||||
};
|
||||
|
||||
// legacy
|
||||
result.ItemId = result.Id;
|
||||
|
||||
if (item.IsFolder)
|
||||
{
|
||||
result.IsFolder = true;
|
||||
}
|
||||
|
||||
var primaryImageTag = _imageProcessor.GetImageCacheTag(item, ImageType.Primary);
|
||||
|
||||
if (primaryImageTag != null)
|
||||
{
|
||||
result.PrimaryImageTag = primaryImageTag;
|
||||
result.PrimaryImageAspectRatio = _dtoService.GetPrimaryImageAspectRatio(item);
|
||||
}
|
||||
|
||||
SetThumbImageInfo(result, item);
|
||||
SetBackdropImageInfo(result, item);
|
||||
|
||||
switch (item)
|
||||
{
|
||||
case IHasSeries hasSeries:
|
||||
result.Series = hasSeries.SeriesName;
|
||||
break;
|
||||
case LiveTvProgram program:
|
||||
result.StartDate = program.StartDate;
|
||||
break;
|
||||
case Series series:
|
||||
if (series.Status.HasValue)
|
||||
{
|
||||
result.Status = series.Status.Value.ToString();
|
||||
}
|
||||
|
||||
break;
|
||||
case MusicAlbum album:
|
||||
result.Artists = album.Artists;
|
||||
result.AlbumArtist = album.AlbumArtist;
|
||||
break;
|
||||
case Audio song:
|
||||
result.AlbumArtist = song.AlbumArtists?[0];
|
||||
result.Artists = song.Artists;
|
||||
|
||||
MusicAlbum musicAlbum = song.AlbumEntity;
|
||||
|
||||
if (musicAlbum != null)
|
||||
{
|
||||
result.Album = musicAlbum.Name;
|
||||
result.AlbumId = musicAlbum.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Album = song.Album;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (!item.ChannelId.Equals(Guid.Empty))
|
||||
{
|
||||
var channel = _libraryManager.GetItemById(item.ChannelId);
|
||||
result.ChannelName = channel?.Name;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void SetThumbImageInfo(SearchHint hint, BaseItem item)
|
||||
{
|
||||
var itemWithImage = item.HasImage(ImageType.Thumb) ? item : null;
|
||||
|
||||
if (itemWithImage == null && item is Episode)
|
||||
{
|
||||
itemWithImage = GetParentWithImage<Series>(item, ImageType.Thumb);
|
||||
}
|
||||
|
||||
if (itemWithImage == null)
|
||||
{
|
||||
itemWithImage = GetParentWithImage<BaseItem>(item, ImageType.Thumb);
|
||||
}
|
||||
|
||||
if (itemWithImage != null)
|
||||
{
|
||||
var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Thumb);
|
||||
|
||||
if (tag != null)
|
||||
{
|
||||
hint.ThumbImageTag = tag;
|
||||
hint.ThumbImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetBackdropImageInfo(SearchHint hint, BaseItem item)
|
||||
{
|
||||
var itemWithImage = (item.HasImage(ImageType.Backdrop) ? item : null)
|
||||
?? GetParentWithImage<BaseItem>(item, ImageType.Backdrop);
|
||||
|
||||
if (itemWithImage != null)
|
||||
{
|
||||
var tag = _imageProcessor.GetImageCacheTag(itemWithImage, ImageType.Backdrop);
|
||||
|
||||
if (tag != null)
|
||||
{
|
||||
hint.BackdropImageTag = tag;
|
||||
hint.BackdropImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private T GetParentWithImage<T>(BaseItem item, ImageType type)
|
||||
where T : BaseItem
|
||||
{
|
||||
return item.GetParents().OfType<T>().FirstOrDefault(i => i.HasImage(type));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,350 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Subtitles;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Net;
|
||||
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>
|
||||
/// Subtitle controller.
|
||||
/// </summary>
|
||||
[Route("")]
|
||||
public class SubtitleController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ISubtitleManager _subtitleManager;
|
||||
private readonly ISubtitleEncoder _subtitleEncoder;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IAuthorizationContext _authContext;
|
||||
private readonly ILogger<SubtitleController> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SubtitleController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="subtitleManager">Instance of <see cref="ISubtitleManager"/> interface.</param>
|
||||
/// <param name="subtitleEncoder">Instance of <see cref="ISubtitleEncoder"/> interface.</param>
|
||||
/// <param name="mediaSourceManager">Instance of <see cref="IMediaSourceManager"/> interface.</param>
|
||||
/// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param>
|
||||
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param>
|
||||
/// <param name="logger">Instance of <see cref="ILogger{SubtitleController}"/> interface.</param>
|
||||
public SubtitleController(
|
||||
ILibraryManager libraryManager,
|
||||
ISubtitleManager subtitleManager,
|
||||
ISubtitleEncoder subtitleEncoder,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
IAuthorizationContext authContext,
|
||||
ILogger<SubtitleController> logger)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_subtitleManager = subtitleManager;
|
||||
_subtitleEncoder = subtitleEncoder;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_providerManager = providerManager;
|
||||
_fileSystem = fileSystem;
|
||||
_authContext = authContext;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes an external subtitle file.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="index">The index of the subtitle file.</param>
|
||||
/// <response code="204">Subtitle deleted.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpDelete("Videos/{itemId}/Subtitles/{index}")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<Task> DeleteSubtitle(
|
||||
[FromRoute] Guid itemId,
|
||||
[FromRoute] int index)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
|
||||
if (item == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
_subtitleManager.DeleteSubtitles(item, index);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Search remote subtitles.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="language">The language of the subtitles.</param>
|
||||
/// <param name="isPerfectMatch">Optional. Only show subtitles which are a perfect match.</param>
|
||||
/// <response code="200">Subtitles retrieved.</response>
|
||||
/// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
|
||||
[HttpGet("Items/{itemId}/RemoteSearch/Subtitles/{language}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
|
||||
[FromRoute] Guid itemId,
|
||||
[FromRoute, Required] string? language,
|
||||
[FromQuery] bool? isPerfectMatch)
|
||||
{
|
||||
var video = (Video)_libraryManager.GetItemById(itemId);
|
||||
|
||||
return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a remote subtitle.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="subtitleId">The subtitle id.</param>
|
||||
/// <response code="204">Subtitle downloaded.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Items/{itemId}/RemoteSearch/Subtitles/{subtitleId}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> DownloadRemoteSubtitles(
|
||||
[FromRoute] Guid itemId,
|
||||
[FromRoute, Required] string? subtitleId)
|
||||
{
|
||||
var video = (Video)_libraryManager.GetItemById(itemId);
|
||||
|
||||
try
|
||||
{
|
||||
await _subtitleManager.DownloadSubtitles(video, subtitleId, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
_providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error downloading subtitles");
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the remote subtitles.
|
||||
/// </summary>
|
||||
/// <param name="id">The item id.</param>
|
||||
/// <response code="200">File returned.</response>
|
||||
/// <returns>A <see cref="FileStreamResult"/> with the subtitle file.</returns>
|
||||
[HttpGet("Providers/Subtitles/Subtitles/{id}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Produces(MediaTypeNames.Application.Octet)]
|
||||
public async Task<ActionResult> GetRemoteSubtitles([FromRoute, Required] string? id)
|
||||
{
|
||||
var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return File(result.Stream, MimeTypes.GetMimeType("file." + result.Format));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets subtitles in a specified format.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="mediaSourceId">The media source id.</param>
|
||||
/// <param name="index">The subtitle stream index.</param>
|
||||
/// <param name="format">The format of the returned subtitle.</param>
|
||||
/// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param>
|
||||
/// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param>
|
||||
/// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param>
|
||||
/// <param name="startPositionTicks">Optional. The start position of the subtitle in ticks.</param>
|
||||
/// <response code="200">File returned.</response>
|
||||
/// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
|
||||
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")]
|
||||
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}", Name = "GetSubtitle_2")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult> GetSubtitle(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] string? mediaSourceId,
|
||||
[FromRoute, Required] int index,
|
||||
[FromRoute, Required] string? format,
|
||||
[FromQuery] long? endPositionTicks,
|
||||
[FromQuery] bool copyTimestamps = false,
|
||||
[FromQuery] bool addVttTimeMap = false,
|
||||
[FromRoute] long startPositionTicks = 0)
|
||||
{
|
||||
if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
format = "json";
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(format))
|
||||
{
|
||||
var item = (Video)_libraryManager.GetItemById(itemId);
|
||||
|
||||
var idString = itemId.ToString("N", CultureInfo.InvariantCulture);
|
||||
var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false)
|
||||
.First(i => string.Equals(i.Id, mediaSourceId ?? idString, StringComparison.Ordinal));
|
||||
|
||||
var subtitleStream = mediaSource.MediaStreams
|
||||
.First(i => i.Type == MediaStreamType.Subtitle && i.Index == index);
|
||||
|
||||
FileStream stream = new FileStream(subtitleStream.Path, FileMode.Open, FileAccess.Read);
|
||||
return File(stream, MimeTypes.GetMimeType(subtitleStream.Path));
|
||||
}
|
||||
|
||||
if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap)
|
||||
{
|
||||
await using Stream stream = await EncodeSubtitles(itemId, mediaSourceId, index, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false);
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
|
||||
text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000", StringComparison.Ordinal);
|
||||
|
||||
return File(Encoding.UTF8.GetBytes(text), MimeTypes.GetMimeType("file." + format));
|
||||
}
|
||||
|
||||
return File(
|
||||
await EncodeSubtitles(
|
||||
itemId,
|
||||
mediaSourceId,
|
||||
index,
|
||||
format,
|
||||
startPositionTicks,
|
||||
endPositionTicks,
|
||||
copyTimestamps).ConfigureAwait(false),
|
||||
MimeTypes.GetMimeType("file." + format));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an HLS subtitle playlist.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="index">The subtitle stream index.</param>
|
||||
/// <param name="mediaSourceId">The media source id.</param>
|
||||
/// <param name="segmentLength">The subtitle segment length.</param>
|
||||
/// <response code="200">Subtitle playlist retrieved.</response>
|
||||
/// <returns>A <see cref="FileContentResult"/> with the HLS subtitle playlist.</returns>
|
||||
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
||||
public async Task<ActionResult> GetSubtitlePlaylist(
|
||||
[FromRoute] Guid itemId,
|
||||
[FromRoute] int index,
|
||||
[FromRoute] string? mediaSourceId,
|
||||
[FromQuery, Required] int segmentLength)
|
||||
{
|
||||
var item = (Video)_libraryManager.GetItemById(itemId);
|
||||
|
||||
var mediaSource = await _mediaSourceManager.GetMediaSource(item, mediaSourceId, null, false, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
var runtime = mediaSource.RunTimeTicks ?? -1;
|
||||
|
||||
if (runtime <= 0)
|
||||
{
|
||||
throw new ArgumentException("HLS Subtitles are not supported for this media.");
|
||||
}
|
||||
|
||||
var segmentLengthTicks = TimeSpan.FromSeconds(segmentLength).Ticks;
|
||||
if (segmentLengthTicks <= 0)
|
||||
{
|
||||
throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)");
|
||||
}
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("#EXTM3U")
|
||||
.Append("#EXT-X-TARGETDURATION:")
|
||||
.AppendLine(segmentLength.ToString(CultureInfo.InvariantCulture))
|
||||
.AppendLine("#EXT-X-VERSION:3")
|
||||
.AppendLine("#EXT-X-MEDIA-SEQUENCE:0")
|
||||
.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
|
||||
|
||||
long positionTicks = 0;
|
||||
|
||||
var accessToken = _authContext.GetAuthorizationInfo(Request).Token;
|
||||
|
||||
while (positionTicks < runtime)
|
||||
{
|
||||
var remaining = runtime - positionTicks;
|
||||
var lengthTicks = Math.Min(remaining, segmentLengthTicks);
|
||||
|
||||
builder.Append("#EXTINF:")
|
||||
.Append(TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture))
|
||||
.AppendLine(",");
|
||||
|
||||
var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
|
||||
|
||||
var url = string.Format(
|
||||
CultureInfo.CurrentCulture,
|
||||
"stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}",
|
||||
positionTicks.ToString(CultureInfo.InvariantCulture),
|
||||
endPositionTicks.ToString(CultureInfo.InvariantCulture),
|
||||
accessToken);
|
||||
|
||||
builder.AppendLine(url);
|
||||
|
||||
positionTicks += segmentLengthTicks;
|
||||
}
|
||||
|
||||
builder.AppendLine("#EXT-X-ENDLIST");
|
||||
return File(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a subtitle in the specified format.
|
||||
/// </summary>
|
||||
/// <param name="id">The media id.</param>
|
||||
/// <param name="mediaSourceId">The source media id.</param>
|
||||
/// <param name="index">The subtitle index.</param>
|
||||
/// <param name="format">The format to convert to.</param>
|
||||
/// <param name="startPositionTicks">The start position in ticks.</param>
|
||||
/// <param name="endPositionTicks">The end position in ticks.</param>
|
||||
/// <param name="copyTimestamps">Whether to copy the timestamps.</param>
|
||||
/// <returns>A <see cref="Task{Stream}"/> with the new subtitle file.</returns>
|
||||
private Task<Stream> EncodeSubtitles(
|
||||
Guid id,
|
||||
string? mediaSourceId,
|
||||
int index,
|
||||
string format,
|
||||
long startPositionTicks,
|
||||
long? endPositionTicks,
|
||||
bool copyTimestamps)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(id);
|
||||
|
||||
return _subtitleEncoder.GetSubtitles(
|
||||
item,
|
||||
mediaSourceId,
|
||||
index,
|
||||
format,
|
||||
startPositionTicks,
|
||||
endPositionTicks ?? 0,
|
||||
copyTimestamps,
|
||||
CancellationToken.None);
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue