Merge remote-tracking branch 'upstream/master' into query-fields
This commit is contained in:
commit
6748ba287d
|
@ -62,6 +62,7 @@ jobs:
|
|||
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: 'Download Reference Assembly Build Artifact'
|
||||
enabled: false
|
||||
inputs:
|
||||
source: "specific"
|
||||
artifact: "$(NugetPackageName)"
|
||||
|
@ -73,6 +74,7 @@ jobs:
|
|||
|
||||
- task: CopyFiles@2
|
||||
displayName: 'Copy Reference Assembly Build Artifact'
|
||||
enabled: false
|
||||
inputs:
|
||||
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
|
||||
contents: '**/*.dll'
|
||||
|
@ -83,6 +85,7 @@ jobs:
|
|||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Execute ABI Compatibility Check Tool'
|
||||
enabled: false
|
||||
inputs:
|
||||
command: custom
|
||||
custom: compat
|
||||
|
|
|
@ -63,6 +63,7 @@ jobs:
|
|||
sshEndpoint: repository
|
||||
sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
|
||||
contents: '**'
|
||||
targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
|
||||
|
||||
- job: OpenAPISpec
|
||||
dependsOn: Test
|
||||
|
@ -166,7 +167,7 @@ jobs:
|
|||
inputs:
|
||||
sshEndpoint: repository
|
||||
runOptions: 'commands'
|
||||
commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
|
||||
commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
|
||||
|
||||
- task: SSH@0
|
||||
displayName: 'Update Stable Repository'
|
||||
|
@ -175,7 +176,7 @@ jobs:
|
|||
inputs:
|
||||
sshEndpoint: repository
|
||||
runOptions: 'commands'
|
||||
commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
|
||||
commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
|
||||
|
||||
- job: PublishNuget
|
||||
displayName: 'Publish NuGet packages'
|
||||
|
|
|
@ -1346,8 +1346,8 @@ namespace Emby.Dlna.ContentDirectory
|
|||
{
|
||||
if (id.StartsWith(name + "_", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stubType = (StubType)Enum.Parse(typeof(StubType), name, true);
|
||||
id = id.Split(new[] { '_' }, 2)[1];
|
||||
stubType = Enum.Parse<StubType>(name, true);
|
||||
id = id.Split('_', 2)[1];
|
||||
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -123,7 +123,7 @@ namespace Emby.Dlna.Didl
|
|||
{
|
||||
foreach (var att in profile.XmlRootAttributes)
|
||||
{
|
||||
var parts = att.Name.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var parts = att.Name.Split(':', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
writer.WriteAttributeString(parts[0], parts[1], null, att.Value);
|
||||
|
|
|
@ -383,9 +383,9 @@ namespace Emby.Dlna
|
|||
continue;
|
||||
}
|
||||
|
||||
var filename = Path.GetFileName(name).Substring(namespaceName.Length);
|
||||
|
||||
var path = Path.Combine(systemProfilesPath, filename);
|
||||
var path = Path.Join(
|
||||
systemProfilesPath,
|
||||
Path.GetFileName(name.AsSpan()).Slice(namespaceName.Length));
|
||||
|
||||
using (var stream = _assembly.GetManifestResourceStream(name))
|
||||
{
|
||||
|
|
|
@ -168,7 +168,7 @@ namespace Emby.Dlna.Eventing
|
|||
|
||||
builder.Append("</e:propertyset>");
|
||||
|
||||
using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl);
|
||||
using var options = new HttpRequestMessage(new HttpMethod("NOTIFY"), subscription.CallbackUrl);
|
||||
options.Content = new StringContent(builder.ToString(), Encoding.UTF8, MediaTypeNames.Text.Xml);
|
||||
options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
|
||||
options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");
|
||||
|
|
|
@ -257,9 +257,10 @@ namespace Emby.Dlna.Main
|
|||
|
||||
private async Task RegisterServerEndpoints()
|
||||
{
|
||||
var addresses = await _appHost.GetLocalIpAddresses(CancellationToken.None).ConfigureAwait(false);
|
||||
var addresses = await _appHost.GetLocalIpAddresses().ConfigureAwait(false);
|
||||
|
||||
var udn = CreateUuid(_appHost.SystemId);
|
||||
var descriptorUri = "/dlna/" + udn + "/description.xml";
|
||||
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
|
@ -279,7 +280,6 @@ namespace Emby.Dlna.Main
|
|||
|
||||
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
|
||||
|
||||
var descriptorUri = "/dlna/" + udn + "/description.xml";
|
||||
var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri);
|
||||
|
||||
var device = new SsdpRootDevice
|
||||
|
|
|
@ -326,7 +326,7 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogDebug("{0} - Received PlayRequest: {1}", this._session.DeviceName, command.PlayCommand);
|
||||
_logger.LogDebug("{0} - Received PlayRequest: {1}", _session.DeviceName, command.PlayCommand);
|
||||
|
||||
var user = command.ControllingUserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(command.ControllingUserId);
|
||||
|
||||
|
@ -339,7 +339,7 @@ namespace Emby.Dlna.PlayTo
|
|||
var startIndex = command.StartIndex ?? 0;
|
||||
if (startIndex > 0)
|
||||
{
|
||||
items = items.Skip(startIndex).ToList();
|
||||
items = items.GetRange(startIndex, items.Count - startIndex);
|
||||
}
|
||||
|
||||
var playlist = new List<PlaylistItem>();
|
||||
|
|
|
@ -169,6 +169,7 @@ namespace Emby.Dlna.Service
|
|||
var result = new ControlRequestInfo(localName, namespaceURI);
|
||||
using var subReader = reader.ReadSubtree();
|
||||
await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false);
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -15,6 +15,11 @@ namespace Emby.Naming.Video
|
|||
public static CleanDateTimeResult Clean(string name, IReadOnlyList<Regex> cleanDateTimeRegexes)
|
||||
{
|
||||
CleanDateTimeResult result = new CleanDateTimeResult(name);
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var len = cleanDateTimeRegexes.Count;
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
|
|
|
@ -209,7 +209,10 @@ namespace Emby.Notifications
|
|||
_libraryUpdateTimer = null;
|
||||
}
|
||||
|
||||
items = items.Take(10).ToList();
|
||||
if (items.Count > 10)
|
||||
{
|
||||
items = items.GetRange(0, 10);
|
||||
}
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
|
|
|
@ -4,7 +4,6 @@ using System;
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
|
@ -30,7 +29,6 @@ using Emby.Server.Implementations.Cryptography;
|
|||
using Emby.Server.Implementations.Data;
|
||||
using Emby.Server.Implementations.Devices;
|
||||
using Emby.Server.Implementations.Dto;
|
||||
using Emby.Server.Implementations.HttpServer;
|
||||
using Emby.Server.Implementations.HttpServer.Security;
|
||||
using Emby.Server.Implementations.IO;
|
||||
using Emby.Server.Implementations.Library;
|
||||
|
@ -993,62 +991,36 @@ namespace Emby.Server.Implementations
|
|||
|
||||
protected abstract void RestartInternal();
|
||||
|
||||
/// <summary>
|
||||
/// Comparison function used in <see cref="GetPlugins" />.
|
||||
/// </summary>
|
||||
/// <param name="a">Item to compare.</param>
|
||||
/// <param name="b">Item to compare with.</param>
|
||||
/// <returns>Boolean result of the operation.</returns>
|
||||
private static int VersionCompare(
|
||||
(Version PluginVersion, string Name, string Path) a,
|
||||
(Version PluginVersion, string Name, string Path) b)
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true)
|
||||
{
|
||||
int compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture);
|
||||
|
||||
if (compare == 0)
|
||||
{
|
||||
return a.PluginVersion.CompareTo(b.PluginVersion);
|
||||
}
|
||||
|
||||
return compare;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of plugins to install.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to check.</param>
|
||||
/// <param name="cleanup">True if an attempt should be made to delete old plugs.</param>
|
||||
/// <returns>Enumerable list of dlls to load.</returns>
|
||||
private IEnumerable<string> GetPlugins(string path, bool cleanup = true)
|
||||
{
|
||||
var dllList = new List<string>();
|
||||
var versions = new List<(Version PluginVersion, string Name, string Path)>();
|
||||
var minimumVersion = new Version(0, 0, 0, 1);
|
||||
var versions = new List<LocalPlugin>();
|
||||
var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
|
||||
string metafile;
|
||||
|
||||
foreach (var dir in directories)
|
||||
{
|
||||
try
|
||||
{
|
||||
metafile = Path.Combine(dir, "meta.json");
|
||||
var metafile = Path.Combine(dir, "meta.json");
|
||||
if (File.Exists(metafile))
|
||||
{
|
||||
var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
|
||||
|
||||
if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
|
||||
{
|
||||
targetAbi = new Version(0, 0, 0, 1);
|
||||
targetAbi = minimumVersion;
|
||||
}
|
||||
|
||||
if (!Version.TryParse(manifest.Version, out var version))
|
||||
{
|
||||
version = new Version(0, 0, 0, 1);
|
||||
version = minimumVersion;
|
||||
}
|
||||
|
||||
if (ApplicationVersion >= targetAbi)
|
||||
{
|
||||
// Only load Plugins if the plugin is built for this version or below.
|
||||
versions.Add((version, manifest.Name, dir));
|
||||
versions.Add(new LocalPlugin(manifest.Guid, manifest.Name, version, dir));
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -1057,15 +1029,15 @@ namespace Emby.Server.Implementations
|
|||
metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1];
|
||||
|
||||
int versionIndex = dir.LastIndexOf('_');
|
||||
if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version ver))
|
||||
if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version parsedVersion))
|
||||
{
|
||||
// Versioned folder.
|
||||
versions.Add((ver, metafile, dir));
|
||||
versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Un-versioned folder - Add it under the path name and version 0.0.0.1.
|
||||
versions.Add((new Version(0, 0, 0, 1), metafile, dir));
|
||||
versions.Add(new LocalPlugin(Guid.Empty, metafile, minimumVersion, dir));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1076,14 +1048,14 @@ namespace Emby.Server.Implementations
|
|||
}
|
||||
|
||||
string lastName = string.Empty;
|
||||
versions.Sort(VersionCompare);
|
||||
versions.Sort(LocalPlugin.Compare);
|
||||
// Traverse backwards through the list.
|
||||
// The first item will be the latest version.
|
||||
for (int x = versions.Count - 1; x >= 0; x--)
|
||||
{
|
||||
if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
dllList.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
|
||||
versions[x].DllFiles.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
|
||||
lastName = versions[x].Name;
|
||||
continue;
|
||||
}
|
||||
|
@ -1091,6 +1063,7 @@ namespace Emby.Server.Implementations
|
|||
if (!string.IsNullOrEmpty(lastName) && cleanup)
|
||||
{
|
||||
// Attempt a cleanup of old folders.
|
||||
versions.RemoveAt(x);
|
||||
try
|
||||
{
|
||||
Logger.LogDebug("Deleting {Path}", versions[x].Path);
|
||||
|
@ -1103,7 +1076,7 @@ namespace Emby.Server.Implementations
|
|||
}
|
||||
}
|
||||
|
||||
return dllList;
|
||||
return versions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1114,21 +1087,24 @@ namespace Emby.Server.Implementations
|
|||
{
|
||||
if (Directory.Exists(ApplicationPaths.PluginsPath))
|
||||
{
|
||||
foreach (var file in GetPlugins(ApplicationPaths.PluginsPath))
|
||||
foreach (var plugin in GetLocalPlugins(ApplicationPaths.PluginsPath))
|
||||
{
|
||||
Assembly plugAss;
|
||||
try
|
||||
foreach (var file in plugin.DllFiles)
|
||||
{
|
||||
plugAss = Assembly.LoadFrom(file);
|
||||
}
|
||||
catch (FileLoadException ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to load assembly {Path}", file);
|
||||
continue;
|
||||
}
|
||||
Assembly plugAss;
|
||||
try
|
||||
{
|
||||
plugAss = Assembly.LoadFrom(file);
|
||||
}
|
||||
catch (FileLoadException ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed to load assembly {Path}", file);
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
|
||||
yield return plugAss;
|
||||
Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
|
||||
yield return plugAss;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -250,21 +250,16 @@ namespace Emby.Server.Implementations.Channels
|
|||
var all = channels;
|
||||
var totalCount = all.Count;
|
||||
|
||||
if (query.StartIndex.HasValue)
|
||||
if (query.StartIndex.HasValue || query.Limit.HasValue)
|
||||
{
|
||||
all = all.Skip(query.StartIndex.Value).ToList();
|
||||
int startIndex = query.StartIndex ?? 0;
|
||||
int count = query.Limit == null ? totalCount - startIndex : Math.Min(query.Limit.Value, totalCount - startIndex);
|
||||
all = all.GetRange(startIndex, count);
|
||||
}
|
||||
|
||||
if (query.Limit.HasValue)
|
||||
{
|
||||
all = all.Take(query.Limit.Value).ToList();
|
||||
}
|
||||
|
||||
var returnItems = all.ToArray();
|
||||
|
||||
if (query.RefreshLatestChannelItems)
|
||||
{
|
||||
foreach (var item in returnItems)
|
||||
foreach (var item in all)
|
||||
{
|
||||
RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
@ -272,7 +267,7 @@ namespace Emby.Server.Implementations.Channels
|
|||
|
||||
return new QueryResult<Channel>
|
||||
{
|
||||
Items = returnItems,
|
||||
Items = all,
|
||||
TotalRecordCount = totalCount
|
||||
};
|
||||
}
|
||||
|
|
|
@ -5002,26 +5002,33 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
|
||||
CheckDisposed();
|
||||
|
||||
var commandText = "select Distinct Name from People";
|
||||
var commandText = new StringBuilder("select Distinct p.Name from People p");
|
||||
|
||||
if (query.User != null && query.IsFavorite.HasValue)
|
||||
{
|
||||
commandText.Append(" LEFT JOIN TypedBaseItems tbi ON tbi.Name=p.Name AND tbi.Type='");
|
||||
commandText.Append(typeof(Person).FullName);
|
||||
commandText.Append("' LEFT JOIN UserDatas ON tbi.UserDataKey=key AND userId=@UserId");
|
||||
}
|
||||
|
||||
var whereClauses = GetPeopleWhereClauses(query, null);
|
||||
|
||||
if (whereClauses.Count != 0)
|
||||
{
|
||||
commandText += " where " + string.Join(" AND ", whereClauses);
|
||||
commandText.Append(" where ").Append(string.Join(" AND ", whereClauses));
|
||||
}
|
||||
|
||||
commandText += " order by ListOrder";
|
||||
commandText.Append(" order by ListOrder");
|
||||
|
||||
if (query.Limit > 0)
|
||||
{
|
||||
commandText += " LIMIT " + query.Limit;
|
||||
commandText.Append(" LIMIT ").Append(query.Limit);
|
||||
}
|
||||
|
||||
using (var connection = GetConnection(true))
|
||||
{
|
||||
var list = new List<string>();
|
||||
using (var statement = PrepareStatement(connection, commandText))
|
||||
using (var statement = PrepareStatement(connection, commandText.ToString()))
|
||||
{
|
||||
// Run this again to bind the params
|
||||
GetPeopleWhereClauses(query, statement);
|
||||
|
@ -5087,19 +5094,13 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
if (!query.ItemId.Equals(Guid.Empty))
|
||||
{
|
||||
whereClauses.Add("ItemId=@ItemId");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@ItemId", query.ItemId.ToByteArray());
|
||||
}
|
||||
statement?.TryBind("@ItemId", query.ItemId.ToByteArray());
|
||||
}
|
||||
|
||||
if (!query.AppearsInItemId.Equals(Guid.Empty))
|
||||
{
|
||||
whereClauses.Add("Name in (Select Name from People where ItemId=@AppearsInItemId)");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray());
|
||||
}
|
||||
whereClauses.Add("p.Name in (Select Name from People where ItemId=@AppearsInItemId)");
|
||||
statement?.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray());
|
||||
}
|
||||
|
||||
var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList();
|
||||
|
@ -5107,10 +5108,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
if (queryPersonTypes.Count == 1)
|
||||
{
|
||||
whereClauses.Add("PersonType=@PersonType");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@PersonType", queryPersonTypes[0]);
|
||||
}
|
||||
statement?.TryBind("@PersonType", queryPersonTypes[0]);
|
||||
}
|
||||
else if (queryPersonTypes.Count > 1)
|
||||
{
|
||||
|
@ -5124,10 +5122,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
if (queryExcludePersonTypes.Count == 1)
|
||||
{
|
||||
whereClauses.Add("PersonType<>@PersonType");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@PersonType", queryExcludePersonTypes[0]);
|
||||
}
|
||||
statement?.TryBind("@PersonType", queryExcludePersonTypes[0]);
|
||||
}
|
||||
else if (queryExcludePersonTypes.Count > 1)
|
||||
{
|
||||
|
@ -5139,19 +5134,24 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
if (query.MaxListOrder.HasValue)
|
||||
{
|
||||
whereClauses.Add("ListOrder<=@MaxListOrder");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@MaxListOrder", query.MaxListOrder.Value);
|
||||
}
|
||||
statement?.TryBind("@MaxListOrder", query.MaxListOrder.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.NameContains))
|
||||
{
|
||||
whereClauses.Add("Name like @NameContains");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@NameContains", "%" + query.NameContains + "%");
|
||||
}
|
||||
whereClauses.Add("p.Name like @NameContains");
|
||||
statement?.TryBind("@NameContains", "%" + query.NameContains + "%");
|
||||
}
|
||||
|
||||
if (query.IsFavorite.HasValue)
|
||||
{
|
||||
whereClauses.Add("isFavorite=@IsFavorite");
|
||||
statement?.TryBind("@IsFavorite", query.IsFavorite.Value);
|
||||
}
|
||||
|
||||
if (query.User != null)
|
||||
{
|
||||
statement?.TryBind("@UserId", query.User.InternalId);
|
||||
}
|
||||
|
||||
return whereClauses;
|
||||
|
@ -5420,6 +5420,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
NameStartsWithOrGreater = query.NameStartsWithOrGreater,
|
||||
Tags = query.Tags,
|
||||
OfficialRatings = query.OfficialRatings,
|
||||
StudioIds = query.StudioIds,
|
||||
GenreIds = query.GenreIds,
|
||||
Genres = query.Genres,
|
||||
Years = query.Years,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
|
@ -19,12 +20,12 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
public AuthorizationInfo Authenticate(HttpRequest request)
|
||||
{
|
||||
var auth = _authorizationContext.GetAuthorizationInfo(request);
|
||||
if (auth?.User == null)
|
||||
if (!auth.IsAuthenticated)
|
||||
{
|
||||
return null;
|
||||
throw new AuthenticationException("Invalid token.");
|
||||
}
|
||||
|
||||
if (auth.User.HasPermission(PermissionKind.IsDisabled))
|
||||
if (auth.User?.HasPermission(PermissionKind.IsDisabled) ?? false)
|
||||
{
|
||||
throw new SecurityException("User account has been disabled.");
|
||||
}
|
||||
|
|
|
@ -36,8 +36,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
|
||||
{
|
||||
var auth = GetAuthorizationDictionary(requestContext);
|
||||
var (authInfo, _) =
|
||||
GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
|
||||
var authInfo = GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
|
||||
return authInfo;
|
||||
}
|
||||
|
||||
|
@ -49,19 +48,13 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
private AuthorizationInfo GetAuthorization(HttpContext httpReq)
|
||||
{
|
||||
var auth = GetAuthorizationDictionary(httpReq);
|
||||
var (authInfo, originalAuthInfo) =
|
||||
GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
|
||||
|
||||
if (originalAuthInfo != null)
|
||||
{
|
||||
httpReq.Request.HttpContext.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
|
||||
}
|
||||
var authInfo = GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
|
||||
|
||||
httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
|
||||
return authInfo;
|
||||
}
|
||||
|
||||
private (AuthorizationInfo authInfo, AuthenticationInfo originalAuthenticationInfo) GetAuthorizationInfoFromDictionary(
|
||||
private AuthorizationInfo GetAuthorizationInfoFromDictionary(
|
||||
in Dictionary<string, string> auth,
|
||||
in IHeaderDictionary headers,
|
||||
in IQueryCollection queryString)
|
||||
|
@ -108,88 +101,102 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
|||
Device = device,
|
||||
DeviceId = deviceId,
|
||||
Version = version,
|
||||
Token = token
|
||||
Token = token,
|
||||
IsAuthenticated = false
|
||||
};
|
||||
|
||||
AuthenticationInfo originalAuthenticationInfo = null;
|
||||
if (!string.IsNullOrWhiteSpace(token))
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
var result = _authRepo.Get(new AuthenticationInfoQuery
|
||||
// Request doesn't contain a token.
|
||||
return authInfo;
|
||||
}
|
||||
|
||||
var result = _authRepo.Get(new AuthenticationInfoQuery
|
||||
{
|
||||
AccessToken = token
|
||||
});
|
||||
|
||||
if (result.Items.Count > 0)
|
||||
{
|
||||
authInfo.IsAuthenticated = true;
|
||||
}
|
||||
|
||||
var originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
|
||||
|
||||
if (originalAuthenticationInfo != null)
|
||||
{
|
||||
var updateToken = false;
|
||||
|
||||
// TODO: Remove these checks for IsNullOrWhiteSpace
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Client))
|
||||
{
|
||||
AccessToken = token
|
||||
});
|
||||
authInfo.Client = originalAuthenticationInfo.AppName;
|
||||
}
|
||||
|
||||
originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
|
||||
|
||||
if (originalAuthenticationInfo != null)
|
||||
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
|
||||
{
|
||||
var updateToken = false;
|
||||
authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
|
||||
}
|
||||
|
||||
// TODO: Remove these checks for IsNullOrWhiteSpace
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Client))
|
||||
{
|
||||
authInfo.Client = originalAuthenticationInfo.AppName;
|
||||
}
|
||||
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device
|
||||
var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Device))
|
||||
{
|
||||
authInfo.Device = originalAuthenticationInfo.DeviceName;
|
||||
}
|
||||
else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (allowTokenInfoUpdate)
|
||||
{
|
||||
authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
|
||||
updateToken = true;
|
||||
originalAuthenticationInfo.DeviceName = authInfo.Device;
|
||||
}
|
||||
}
|
||||
|
||||
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device
|
||||
var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Version))
|
||||
{
|
||||
authInfo.Version = originalAuthenticationInfo.AppVersion;
|
||||
}
|
||||
else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (allowTokenInfoUpdate)
|
||||
{
|
||||
updateToken = true;
|
||||
originalAuthenticationInfo.AppVersion = authInfo.Version;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Device))
|
||||
{
|
||||
authInfo.Device = originalAuthenticationInfo.DeviceName;
|
||||
}
|
||||
else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (allowTokenInfoUpdate)
|
||||
{
|
||||
updateToken = true;
|
||||
originalAuthenticationInfo.DeviceName = authInfo.Device;
|
||||
}
|
||||
}
|
||||
if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
|
||||
{
|
||||
originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
|
||||
updateToken = true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Version))
|
||||
{
|
||||
authInfo.Version = originalAuthenticationInfo.AppVersion;
|
||||
}
|
||||
else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (allowTokenInfoUpdate)
|
||||
{
|
||||
updateToken = true;
|
||||
originalAuthenticationInfo.AppVersion = authInfo.Version;
|
||||
}
|
||||
}
|
||||
if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
|
||||
{
|
||||
authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
|
||||
|
||||
if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
|
||||
if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
|
||||
originalAuthenticationInfo.UserName = authInfo.User.Username;
|
||||
updateToken = true;
|
||||
}
|
||||
|
||||
if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
|
||||
{
|
||||
authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
|
||||
authInfo.IsApiKey = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
authInfo.IsApiKey = false;
|
||||
}
|
||||
|
||||
if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
originalAuthenticationInfo.UserName = authInfo.User.Username;
|
||||
updateToken = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updateToken)
|
||||
{
|
||||
_authRepo.Update(originalAuthenticationInfo);
|
||||
}
|
||||
if (updateToken)
|
||||
{
|
||||
_authRepo.Update(originalAuthenticationInfo);
|
||||
}
|
||||
}
|
||||
|
||||
return (authInfo, originalAuthenticationInfo);
|
||||
return authInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -2440,6 +2440,21 @@ namespace Emby.Server.Implementations.Library
|
|||
new SubtitleResolver(BaseItem.LocalizationManager).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files);
|
||||
}
|
||||
|
||||
public BaseItem GetParentItem(string parentId, Guid? userId)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(parentId))
|
||||
{
|
||||
return GetItemById(new Guid(parentId));
|
||||
}
|
||||
|
||||
if (userId.HasValue && userId != Guid.Empty)
|
||||
{
|
||||
return GetUserRootFolder();
|
||||
}
|
||||
|
||||
return RootFolder;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsVideoFile(string path)
|
||||
{
|
||||
|
|
|
@ -15,6 +15,7 @@ using System.Threading.Tasks;
|
|||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
|
@ -33,17 +34,20 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
|
||||
private readonly IApplicationHost _appHost;
|
||||
private readonly ICryptoProvider _cryptoProvider;
|
||||
|
||||
public SchedulesDirect(
|
||||
ILogger<SchedulesDirect> logger,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IApplicationHost appHost)
|
||||
IApplicationHost appHost,
|
||||
ICryptoProvider cryptoProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_appHost = appHost;
|
||||
_cryptoProvider = cryptoProvider;
|
||||
}
|
||||
|
||||
private string UserAgent => _appHost.ApplicationUserAgent;
|
||||
|
@ -642,7 +646,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token");
|
||||
options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
|
||||
var hashedPasswordBytes = _cryptoProvider.ComputeHash("SHA1", Encoding.ASCII.GetBytes(password), Array.Empty<byte>());
|
||||
string hashedPassword = Hex.Encode(hashedPasswordBytes);
|
||||
options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
|
||||
|
||||
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
|
||||
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
|
|
|
@ -131,6 +131,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
await taskCompletionSource.Task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public string GetFilePath()
|
||||
{
|
||||
return TempFilePath;
|
||||
}
|
||||
|
||||
private Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.Run(async () =>
|
||||
|
|
|
@ -65,7 +65,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
{
|
||||
var channelIdPrefix = GetFullChannelIdPrefix(info);
|
||||
|
||||
return await new M3uParser(Logger, _httpClientFactory, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
|
||||
return await new M3uParser(Logger, _httpClientFactory, _appHost)
|
||||
.Parse(info, channelIdPrefix, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
|
||||
|
@ -126,7 +128,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
|
||||
public async Task Validate(TunerHostInfo info)
|
||||
{
|
||||
using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false))
|
||||
using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info, CancellationToken.None).ConfigureAwait(false))
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ using MediaBrowser.Common.Extensions;
|
|||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
|
@ -30,12 +31,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
_appHost = appHost;
|
||||
}
|
||||
|
||||
public async Task<List<ChannelInfo>> Parse(string url, string channelIdPrefix, string tunerHostId, CancellationToken cancellationToken)
|
||||
public async Task<List<ChannelInfo>> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken)
|
||||
{
|
||||
// Read the file and display it line by line.
|
||||
using (var reader = new StreamReader(await GetListingsStream(url, cancellationToken).ConfigureAwait(false)))
|
||||
using (var reader = new StreamReader(await GetListingsStream(info, cancellationToken).ConfigureAwait(false)))
|
||||
{
|
||||
return GetChannels(reader, channelIdPrefix, tunerHostId);
|
||||
return GetChannels(reader, channelIdPrefix, info.Id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,15 +49,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
}
|
||||
}
|
||||
|
||||
public Task<Stream> GetListingsStream(string url, CancellationToken cancellationToken)
|
||||
public async Task<Stream> GetListingsStream(TunerHostInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
if (info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.GetStreamAsync(url);
|
||||
using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url);
|
||||
if (!string.IsNullOrEmpty(info.UserAgent))
|
||||
{
|
||||
requestMessage.Headers.UserAgent.TryParseAdd(info.UserAgent);
|
||||
}
|
||||
|
||||
var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.SendAsync(requestMessage, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return Task.FromResult((Stream)File.OpenRead(url));
|
||||
return File.OpenRead(info.Url);
|
||||
}
|
||||
|
||||
private const string ExtInfPrefix = "#EXTINF:";
|
||||
|
|
|
@ -55,7 +55,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
var typeName = GetType().Name;
|
||||
Logger.LogInformation("Opening " + typeName + " Live stream from {0}", url);
|
||||
|
||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
// Response stream is disposed manually.
|
||||
var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
|
@ -121,6 +122,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||
}
|
||||
}
|
||||
|
||||
public string GetFilePath()
|
||||
{
|
||||
return TempFilePath;
|
||||
}
|
||||
|
||||
private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.Run(async () =>
|
||||
|
|
|
@ -113,5 +113,7 @@
|
|||
"TasksChannelsCategory": "Internetové kanály",
|
||||
"TasksApplicationCategory": "Aplikace",
|
||||
"TasksLibraryCategory": "Knihovna",
|
||||
"TasksMaintenanceCategory": "Údržba"
|
||||
"TasksMaintenanceCategory": "Údržba",
|
||||
"TaskCleanActivityLogDescription": "Smazat záznamy o aktivitě, které jsou starší než zadaná doba.",
|
||||
"TaskCleanActivityLog": "Smazat záznam aktivity"
|
||||
}
|
||||
|
|
|
@ -113,5 +113,7 @@
|
|||
"TasksChannelsCategory": "Internet Kanäle",
|
||||
"TasksApplicationCategory": "Anwendung",
|
||||
"TasksLibraryCategory": "Bibliothek",
|
||||
"TasksMaintenanceCategory": "Wartung"
|
||||
"TasksMaintenanceCategory": "Wartung",
|
||||
"TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.",
|
||||
"TaskCleanActivityLog": "Aktivitätsprotokoll aufräumen"
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@
|
|||
"SubtitleDownloadFailureFromForItem": "Fallo de descarga de subtítulos desde {0} para {1}",
|
||||
"Sync": "Sincronizar",
|
||||
"System": "Sistema",
|
||||
"TvShows": "Programas de televisión",
|
||||
"TvShows": "Series",
|
||||
"User": "Usuario",
|
||||
"UserCreatedWithName": "El usuario {0} ha sido creado",
|
||||
"UserDeletedWithName": "El usuario {0} ha sido borrado",
|
||||
|
|
|
@ -113,5 +113,7 @@
|
|||
"TaskCleanCache": "Vider le répertoire cache",
|
||||
"TasksApplicationCategory": "Application",
|
||||
"TasksLibraryCategory": "Bibliothèque",
|
||||
"TasksMaintenanceCategory": "Maintenance"
|
||||
"TasksMaintenanceCategory": "Maintenance",
|
||||
"TaskCleanActivityLogDescription": "Supprime les entrées du journal d'activité antérieures à l'âge configuré.",
|
||||
"TaskCleanActivityLog": "Nettoyer le journal d'activité"
|
||||
}
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
"Artists": "Izvođači",
|
||||
"AuthenticationSucceededWithUserName": "{0} uspješno ovjerena",
|
||||
"Books": "Knjige",
|
||||
"CameraImageUploadedFrom": "Nova fotografija sa kamere je uploadana iz {0}",
|
||||
"CameraImageUploadedFrom": "Nova fotografija sa kamere je učitana iz {0}",
|
||||
"Channels": "Kanali",
|
||||
"ChapterNameValue": "Poglavlje {0}",
|
||||
"Collections": "Kolekcije",
|
||||
"DeviceOfflineWithName": "{0} se odspojilo",
|
||||
"DeviceOnlineWithName": "{0} je spojeno",
|
||||
"FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave za {0}",
|
||||
"DeviceOfflineWithName": "{0} je prekinuo vezu",
|
||||
"DeviceOnlineWithName": "{0} je povezan",
|
||||
"FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave od {0}",
|
||||
"Favorites": "Favoriti",
|
||||
"Folders": "Mape",
|
||||
"Genres": "Žanrovi",
|
||||
|
@ -23,95 +23,97 @@
|
|||
"HeaderFavoriteShows": "Omiljene serije",
|
||||
"HeaderFavoriteSongs": "Omiljene pjesme",
|
||||
"HeaderLiveTV": "TV uživo",
|
||||
"HeaderNextUp": "Sljedeće je",
|
||||
"HeaderNextUp": "Slijedi",
|
||||
"HeaderRecordingGroups": "Grupa snimka",
|
||||
"HomeVideos": "Kućni videi",
|
||||
"HomeVideos": "Kućni video",
|
||||
"Inherit": "Naslijedi",
|
||||
"ItemAddedWithName": "{0} je dodano u biblioteku",
|
||||
"ItemRemovedWithName": "{0} je uklonjen iz biblioteke",
|
||||
"ItemRemovedWithName": "{0} je uklonjeno iz biblioteke",
|
||||
"LabelIpAddressValue": "IP adresa: {0}",
|
||||
"LabelRunningTimeValue": "Vrijeme rada: {0}",
|
||||
"Latest": "Najnovije",
|
||||
"MessageApplicationUpdated": "Jellyfin Server je ažuriran",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server je ažuriran na {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Odjeljak postavka servera {0} je ažuriran",
|
||||
"MessageServerConfigurationUpdated": "Postavke servera su ažurirane",
|
||||
"MessageApplicationUpdated": "Jellyfin server je ažuriran",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin server je ažuriran na {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Dio konfiguracije servera {0} je ažuriran",
|
||||
"MessageServerConfigurationUpdated": "Konfiguracija servera je ažurirana",
|
||||
"MixedContent": "Miješani sadržaj",
|
||||
"Movies": "Filmovi",
|
||||
"Music": "Glazba",
|
||||
"MusicVideos": "Glazbeni spotovi",
|
||||
"NameInstallFailed": "{0} neuspješnih instalacija",
|
||||
"NameSeasonNumber": "Sezona {0}",
|
||||
"NameSeasonUnknown": "Nepoznata sezona",
|
||||
"NameSeasonUnknown": "Sezona nepoznata",
|
||||
"NewVersionIsAvailable": "Nova verzija Jellyfin servera je dostupna za preuzimanje.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Dostupno ažuriranje aplikacije",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Instalirano ažuriranje aplikacije",
|
||||
"NotificationOptionAudioPlayback": "Reprodukcija glazbe započeta",
|
||||
"NotificationOptionAudioPlaybackStopped": "Reprodukcija audiozapisa je zaustavljena",
|
||||
"NotificationOptionCameraImageUploaded": "Slike kamere preuzete",
|
||||
"NotificationOptionInstallationFailed": "Instalacija neuspješna",
|
||||
"NotificationOptionNewLibraryContent": "Novi sadržaj je dodan",
|
||||
"NotificationOptionPluginError": "Dodatak otkazao",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Dostupno je ažuriranje aplikacije",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Instalirano je ažuriranje aplikacije",
|
||||
"NotificationOptionAudioPlayback": "Reprodukcija glazbe započela",
|
||||
"NotificationOptionAudioPlaybackStopped": "Reprodukcija glazbe zaustavljena",
|
||||
"NotificationOptionCameraImageUploaded": "Slika s kamere učitana",
|
||||
"NotificationOptionInstallationFailed": "Instalacija nije uspjela",
|
||||
"NotificationOptionNewLibraryContent": "Novi sadržaj dodan",
|
||||
"NotificationOptionPluginError": "Dodatak zakazao",
|
||||
"NotificationOptionPluginInstalled": "Dodatak instaliran",
|
||||
"NotificationOptionPluginUninstalled": "Dodatak uklonjen",
|
||||
"NotificationOptionPluginUpdateInstalled": "Instalirano ažuriranje za dodatak",
|
||||
"NotificationOptionServerRestartRequired": "Potrebno ponovo pokretanje servera",
|
||||
"NotificationOptionTaskFailed": "Zakazan zadatak nije izvršen",
|
||||
"NotificationOptionPluginUninstalled": "Dodatak deinstaliran",
|
||||
"NotificationOptionPluginUpdateInstalled": "Instalirano ažuriranje dodatka",
|
||||
"NotificationOptionServerRestartRequired": "Ponovno pokrenite server",
|
||||
"NotificationOptionTaskFailed": "Greška zakazanog zadatka",
|
||||
"NotificationOptionUserLockedOut": "Korisnik zaključan",
|
||||
"NotificationOptionVideoPlayback": "Reprodukcija videa započeta",
|
||||
"NotificationOptionVideoPlaybackStopped": "Reprodukcija videozapisa je zaustavljena",
|
||||
"Photos": "Slike",
|
||||
"Playlists": "Popis za reprodukciju",
|
||||
"NotificationOptionVideoPlayback": "Reprodukcija videa započela",
|
||||
"NotificationOptionVideoPlaybackStopped": "Reprodukcija videa zaustavljena",
|
||||
"Photos": "Fotografije",
|
||||
"Playlists": "Popisi za reprodukciju",
|
||||
"Plugin": "Dodatak",
|
||||
"PluginInstalledWithName": "{0} je instalirano",
|
||||
"PluginUninstalledWithName": "{0} je deinstalirano",
|
||||
"PluginUpdatedWithName": "{0} je ažurirano",
|
||||
"ProviderValue": "Pružitelj: {0}",
|
||||
"ProviderValue": "Pružatelj: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} neuspjelo",
|
||||
"ScheduledTaskStartedWithName": "{0} pokrenuto",
|
||||
"ServerNameNeedsToBeRestarted": "{0} treba biti ponovno pokrenuto",
|
||||
"ServerNameNeedsToBeRestarted": "{0} treba ponovno pokrenuti",
|
||||
"Shows": "Serije",
|
||||
"Songs": "Pjesme",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server se učitava. Pokušajte ponovo kasnije.",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.",
|
||||
"SubtitleDownloadFailureForItem": "Titlovi prijevoda nisu preuzeti za {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Prijevodi nisu uspješno preuzeti {0} od {1}",
|
||||
"Sync": "Sink.",
|
||||
"System": "Sistem",
|
||||
"SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}",
|
||||
"Sync": "Sinkronizacija",
|
||||
"System": "Sustav",
|
||||
"TvShows": "Serije",
|
||||
"User": "Korisnik",
|
||||
"UserCreatedWithName": "Korisnik {0} je stvoren",
|
||||
"UserCreatedWithName": "Korisnik {0} je kreiran",
|
||||
"UserDeletedWithName": "Korisnik {0} je obrisan",
|
||||
"UserDownloadingItemWithValues": "{0} se preuzima {1}",
|
||||
"UserDownloadingItemWithValues": "{0} preuzima {1}",
|
||||
"UserLockedOutWithName": "Korisnik {0} je zaključan",
|
||||
"UserOfflineFromDevice": "{0} se odspojilo od {1}",
|
||||
"UserOnlineFromDevice": "{0} je online od {1}",
|
||||
"UserOfflineFromDevice": "{0} prekinuo vezu od {1}",
|
||||
"UserOnlineFromDevice": "{0} povezan od {1}",
|
||||
"UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}",
|
||||
"UserPolicyUpdatedWithName": "Pravila za korisnika su ažurirana za {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} je pokrenuo {1}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} je zaustavio {1}",
|
||||
"UserPolicyUpdatedWithName": "Pravila za korisnika ažurirana su za {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} je pokrenuo reprodukciju {1} na {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} je završio reprodukciju {1} na {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} je dodano u medijsku biblioteku",
|
||||
"ValueSpecialEpisodeName": "Specijal - {0}",
|
||||
"ValueSpecialEpisodeName": "Posebno - {0}",
|
||||
"VersionNumber": "Verzija {0}",
|
||||
"TaskRefreshLibraryDescription": "Skenira vašu medijsku knjižnicu sa novim datotekama i osvježuje metapodatke.",
|
||||
"TaskRefreshLibrary": "Skeniraj medijsku knjižnicu",
|
||||
"TaskRefreshChapterImagesDescription": "Stvara sličice za videozapise koji imaju poglavlja.",
|
||||
"TaskRefreshChapterImages": "Raspakiraj slike poglavlja",
|
||||
"TaskCleanCacheDescription": "Briše priručne datoteke nepotrebne za sistem.",
|
||||
"TaskCleanCache": "Očisti priručnu memoriju",
|
||||
"TaskRefreshLibraryDescription": "Skenira medijsku biblioteku radi novih datoteka i osvježava metapodatke.",
|
||||
"TaskRefreshLibrary": "Skeniraj medijsku biblioteku",
|
||||
"TaskRefreshChapterImagesDescription": "Kreira sličice za videozapise koji imaju poglavlja.",
|
||||
"TaskRefreshChapterImages": "Izdvoji slike poglavlja",
|
||||
"TaskCleanCacheDescription": "Briše nepotrebne datoteke iz predmemorije.",
|
||||
"TaskCleanCache": "Očisti mapu predmemorije",
|
||||
"TasksApplicationCategory": "Aplikacija",
|
||||
"TasksMaintenanceCategory": "Održavanje",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Pretraživanje interneta za prijevodima koji nedostaju bazirano na konfiguraciji meta podataka.",
|
||||
"TaskDownloadMissingSubtitles": "Preuzimanje prijevoda koji nedostaju",
|
||||
"TaskRefreshChannelsDescription": "Osvježava informacije o internet kanalima.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Pretraži Internet za prijevodima koji nedostaju prema konfiguraciji metapodataka.",
|
||||
"TaskDownloadMissingSubtitles": "Preuzmi prijevod koji nedostaje",
|
||||
"TaskRefreshChannelsDescription": "Osvježava informacije Internet kanala.",
|
||||
"TaskRefreshChannels": "Osvježi kanale",
|
||||
"TaskCleanTranscodeDescription": "Briše transkodirane fajlove starije od jednog dana.",
|
||||
"TaskCleanTranscode": "Očisti direktorij za transkodiranje",
|
||||
"TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su podešeni da se ažuriraju automatski.",
|
||||
"TaskCleanTranscodeDescription": "Briše transkodirane datoteke starije od jednog dana.",
|
||||
"TaskCleanTranscode": "Očisti mapu transkodiranja",
|
||||
"TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su konfigurirani da se ažuriraju automatski.",
|
||||
"TaskUpdatePlugins": "Ažuriraj dodatke",
|
||||
"TaskRefreshPeopleDescription": "Ažurira meta podatke za glumce i redatelje u vašoj medijskoj biblioteci.",
|
||||
"TaskRefreshPeople": "Osvježi ljude",
|
||||
"TaskCleanLogsDescription": "Briši logove koji su stariji od {0} dana.",
|
||||
"TaskCleanLogs": "Očisti direktorij sa logovima",
|
||||
"TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i redatelje u medijskoj biblioteci.",
|
||||
"TaskRefreshPeople": "Osvježi osobe",
|
||||
"TaskCleanLogsDescription": "Briše zapise dnevnika koji su stariji od {0} dana.",
|
||||
"TaskCleanLogs": "Očisti mapu dnevnika zapisa",
|
||||
"TasksChannelsCategory": "Internet kanali",
|
||||
"TasksLibraryCategory": "Biblioteka"
|
||||
"TasksLibraryCategory": "Biblioteka",
|
||||
"TaskCleanActivityLogDescription": "Briše zapise dnevnika aktivnosti starije od navedenog vremena.",
|
||||
"TaskCleanActivityLog": "Očisti dnevnik aktivnosti"
|
||||
}
|
||||
|
|
|
@ -113,5 +113,7 @@
|
|||
"TasksChannelsCategory": "Canali su Internet",
|
||||
"TasksApplicationCategory": "Applicazione",
|
||||
"TasksLibraryCategory": "Libreria",
|
||||
"TasksMaintenanceCategory": "Manutenzione"
|
||||
"TasksMaintenanceCategory": "Manutenzione",
|
||||
"TaskCleanActivityLog": "Attività di Registro Completate",
|
||||
"TaskCleanActivityLogDescription": "Elimina gli inserimenti nel registro delle attività più vecchie dell’età configurata."
|
||||
}
|
||||
|
|
|
@ -96,7 +96,7 @@
|
|||
"TaskRefreshLibraryDescription": "メディアライブラリをスキャンして新しいファイルを探し、メタデータをリフレッシュします。",
|
||||
"TaskRefreshLibrary": "メディアライブラリのスキャン",
|
||||
"TaskCleanCacheDescription": "不要なキャッシュを消去します。",
|
||||
"TaskCleanCache": "キャッシュの掃除",
|
||||
"TaskCleanCache": "キャッシュを消去",
|
||||
"TasksChannelsCategory": "ネットチャンネル",
|
||||
"TasksApplicationCategory": "アプリケーション",
|
||||
"TasksLibraryCategory": "ライブラリ",
|
||||
|
@ -112,5 +112,7 @@
|
|||
"TaskDownloadMissingSubtitlesDescription": "メタデータ構成に基づいて、欠落している字幕をインターネットで検索します。",
|
||||
"TaskRefreshChapterImagesDescription": "チャプターのあるビデオのサムネイルを作成します。",
|
||||
"TaskRefreshChapterImages": "チャプター画像を抽出する",
|
||||
"TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする"
|
||||
"TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする",
|
||||
"TaskCleanActivityLogDescription": "設定された期間よりも古いアクティビティの履歴を削除します。",
|
||||
"TaskCleanActivityLog": "アクティビティの履歴を消去"
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
"HeaderRecordingGroups": "녹화 그룹",
|
||||
"HomeVideos": "홈 비디오",
|
||||
"Inherit": "상속",
|
||||
"ItemAddedWithName": "{0}가 라이브러리에 추가됨",
|
||||
"ItemAddedWithName": "{0}가 라이브러리에 추가되었습니다",
|
||||
"ItemRemovedWithName": "{0}가 라이브러리에서 제거됨",
|
||||
"LabelIpAddressValue": "IP 주소: {0}",
|
||||
"LabelRunningTimeValue": "상영 시간: {0}",
|
||||
|
@ -113,5 +113,7 @@
|
|||
"TaskCleanCacheDescription": "시스템에서 더 이상 필요하지 않은 캐시 파일을 삭제합니다.",
|
||||
"TaskCleanCache": "캐시 폴더 청소",
|
||||
"TasksChannelsCategory": "인터넷 채널",
|
||||
"TasksLibraryCategory": "라이브러리"
|
||||
"TasksLibraryCategory": "라이브러리",
|
||||
"TaskCleanActivityLogDescription": "구성된 기간보다 오래된 활동내역 삭제.",
|
||||
"TaskCleanActivityLog": "활동내역청소"
|
||||
}
|
||||
|
|
|
@ -113,5 +113,7 @@
|
|||
"TaskCleanLogsDescription": "Удаляются файлы журнала, возраст которых превышает {0} дн(я/ей).",
|
||||
"TaskRefreshLibraryDescription": "Сканируется медиатека на новые файлы и обновляются метаданные.",
|
||||
"TaskRefreshChapterImagesDescription": "Создаются эскизы для видео, которые содержат сцены.",
|
||||
"TaskCleanCacheDescription": "Удаляются файлы кэша, которые больше не нужны системе."
|
||||
"TaskCleanCacheDescription": "Удаляются файлы кэша, которые больше не нужны системе.",
|
||||
"TaskCleanActivityLogDescription": "Удаляет записи журнала активности старше установленного возраста.",
|
||||
"TaskCleanActivityLog": "Очистить журнал активности"
|
||||
}
|
||||
|
|
|
@ -3,20 +3,20 @@
|
|||
"AppDeviceValues": "Aplikacija: {0}, Naprava: {1}",
|
||||
"Application": "Aplikacija",
|
||||
"Artists": "Izvajalci",
|
||||
"AuthenticationSucceededWithUserName": "{0} preverjanje pristnosti uspešno",
|
||||
"AuthenticationSucceededWithUserName": "{0} se je uspešno prijavil",
|
||||
"Books": "Knjige",
|
||||
"CameraImageUploadedFrom": "Nova fotografija je bila naložena z {0}",
|
||||
"CameraImageUploadedFrom": "Nova fotografija je bila naložena iz {0}",
|
||||
"Channels": "Kanali",
|
||||
"ChapterNameValue": "Poglavje {0}",
|
||||
"Collections": "Zbirke",
|
||||
"DeviceOfflineWithName": "{0} je prekinil povezavo",
|
||||
"DeviceOnlineWithName": "{0} je povezan",
|
||||
"FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}",
|
||||
"FailedLoginAttemptWithUserName": "Neuspešen poskus prijave iz {0}",
|
||||
"Favorites": "Priljubljeno",
|
||||
"Folders": "Mape",
|
||||
"Genres": "Zvrsti",
|
||||
"HeaderAlbumArtists": "Izvajalci albuma",
|
||||
"HeaderContinueWatching": "Nadaljuj gledanje",
|
||||
"HeaderContinueWatching": "Nadaljuj z ogledom",
|
||||
"HeaderFavoriteAlbums": "Priljubljeni albumi",
|
||||
"HeaderFavoriteArtists": "Priljubljeni izvajalci",
|
||||
"HeaderFavoriteEpisodes": "Priljubljene epizode",
|
||||
|
@ -32,23 +32,23 @@
|
|||
"LabelIpAddressValue": "IP naslov: {0}",
|
||||
"LabelRunningTimeValue": "Čas trajanja: {0}",
|
||||
"Latest": "Najnovejše",
|
||||
"MessageApplicationUpdated": "Jellyfin Server je bil posodobljen",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server je bil posodobljen na {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Oddelek nastavitve strežnika {0} je bil posodobljen",
|
||||
"MessageApplicationUpdated": "Jellyfin strežnik je bil posodobljen",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin strežnik je bil posodobljen na {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Oddelek nastavitev {0} je bil posodobljen",
|
||||
"MessageServerConfigurationUpdated": "Nastavitve strežnika so bile posodobljene",
|
||||
"MixedContent": "Razne vsebine",
|
||||
"MixedContent": "Mešane vsebine",
|
||||
"Movies": "Filmi",
|
||||
"Music": "Glasba",
|
||||
"MusicVideos": "Glasbeni videi",
|
||||
"NameInstallFailed": "{0} namestitev neuspešna",
|
||||
"NameSeasonNumber": "Sezona {0}",
|
||||
"NameSeasonUnknown": "Season neznana",
|
||||
"NameSeasonUnknown": "Neznana sezona",
|
||||
"NewVersionIsAvailable": "Nova različica Jellyfin strežnika je na voljo za prenos.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Posodobitev aplikacije je na voljo",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Posodobitev aplikacije je bila nameščena",
|
||||
"NotificationOptionAudioPlayback": "Predvajanje zvoka začeto",
|
||||
"NotificationOptionAudioPlaybackStopped": "Predvajanje zvoka zaustavljeno",
|
||||
"NotificationOptionCameraImageUploaded": "Posnetek kamere naložen",
|
||||
"NotificationOptionAudioPlayback": "Predvajanje zvoka se je začelo",
|
||||
"NotificationOptionAudioPlaybackStopped": "Predvajanje zvoka se je ustavilo",
|
||||
"NotificationOptionCameraImageUploaded": "Fotografija naložena",
|
||||
"NotificationOptionInstallationFailed": "Namestitev neuspešna",
|
||||
"NotificationOptionNewLibraryContent": "Nove vsebine dodane",
|
||||
"NotificationOptionPluginError": "Napaka dodatka",
|
||||
|
@ -56,41 +56,41 @@
|
|||
"NotificationOptionPluginUninstalled": "Dodatek odstranjen",
|
||||
"NotificationOptionPluginUpdateInstalled": "Posodobitev dodatka nameščena",
|
||||
"NotificationOptionServerRestartRequired": "Potreben je ponovni zagon strežnika",
|
||||
"NotificationOptionTaskFailed": "Razporejena naloga neuspešna",
|
||||
"NotificationOptionTaskFailed": "Načrtovano opravilo neuspešno",
|
||||
"NotificationOptionUserLockedOut": "Uporabnik zaklenjen",
|
||||
"NotificationOptionVideoPlayback": "Predvajanje videa se je začelo",
|
||||
"NotificationOptionVideoPlaybackStopped": "Predvajanje videa se je ustavilo",
|
||||
"Photos": "Fotografije",
|
||||
"Playlists": "Seznami predvajanja",
|
||||
"Plugin": "Plugin",
|
||||
"Plugin": "Dodatek",
|
||||
"PluginInstalledWithName": "{0} je bil nameščen",
|
||||
"PluginUninstalledWithName": "{0} je bil odstranjen",
|
||||
"PluginUpdatedWithName": "{0} je bil posodobljen",
|
||||
"ProviderValue": "Provider: {0}",
|
||||
"ProviderValue": "Ponudnik: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} ni uspelo",
|
||||
"ScheduledTaskStartedWithName": "{0} začeto",
|
||||
"ServerNameNeedsToBeRestarted": "{0} mora biti ponovno zagnan",
|
||||
"Shows": "Serije",
|
||||
"Songs": "Pesmi",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server se nalaga. Poskusi ponovno kasneje.",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin strežnik se zaganja. Poskusite ponovno kasneje.",
|
||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Neuspešen prenos podnapisov iz {0} za {1}",
|
||||
"Sync": "Sinhroniziraj",
|
||||
"System": "System",
|
||||
"System": "Sistem",
|
||||
"TvShows": "TV serije",
|
||||
"User": "User",
|
||||
"User": "Uporabnik",
|
||||
"UserCreatedWithName": "Uporabnik {0} je bil ustvarjen",
|
||||
"UserDeletedWithName": "Uporabnik {0} je bil izbrisan",
|
||||
"UserDownloadingItemWithValues": "{0} prenaša {1}",
|
||||
"UserLockedOutWithName": "Uporabnik {0} je bil zaklenjen",
|
||||
"UserOfflineFromDevice": "{0} je prekinil povezavo z {1}",
|
||||
"UserOnlineFromDevice": "{0} je aktiven iz {1}",
|
||||
"UserOnlineFromDevice": "{0} je aktiven na {1}",
|
||||
"UserPasswordChangedWithName": "Geslo za uporabnika {0} je bilo spremenjeno",
|
||||
"UserPolicyUpdatedWithName": "Pravilnik uporabe je bil posodobljen za uporabnika {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} predvaja {1} na {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} je nehal predvajati {1} na {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} je bil dodan vaši knjižnici",
|
||||
"ValueSpecialEpisodeName": "Poseben - {0}",
|
||||
"ValueSpecialEpisodeName": "Posebna - {0}",
|
||||
"VersionNumber": "Različica {0}",
|
||||
"TaskDownloadMissingSubtitles": "Prenesi manjkajoče podnapise",
|
||||
"TaskRefreshChannelsDescription": "Osveži podatke spletnih kanalov.",
|
||||
|
@ -102,7 +102,7 @@
|
|||
"TaskRefreshPeopleDescription": "Osveži metapodatke za igralce in režiserje v vaši knjižnici.",
|
||||
"TaskRefreshPeople": "Osveži osebe",
|
||||
"TaskCleanLogsDescription": "Izbriše dnevniške datoteke starejše od {0} dni.",
|
||||
"TaskCleanLogs": "Počisti mapo dnevnika",
|
||||
"TaskCleanLogs": "Počisti mapo dnevnikov",
|
||||
"TaskRefreshLibraryDescription": "Preišče vašo knjižnico za nove datoteke in osveži metapodatke.",
|
||||
"TaskRefreshLibrary": "Preišči knjižnico predstavnosti",
|
||||
"TaskRefreshChapterImagesDescription": "Ustvari sličice za poglavja videoposnetkov.",
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi",
|
||||
"Channels": "Kanallar",
|
||||
"ChapterNameValue": "Bölüm {0}",
|
||||
"Collections": "Koleksiyonlar",
|
||||
"Collections": "Koleksiyon",
|
||||
"DeviceOfflineWithName": "{0} bağlantısı kesildi",
|
||||
"DeviceOnlineWithName": "{0} bağlı",
|
||||
"FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu",
|
||||
|
@ -23,7 +23,7 @@
|
|||
"HeaderFavoriteShows": "Favori Diziler",
|
||||
"HeaderFavoriteSongs": "Favori Şarkılar",
|
||||
"HeaderLiveTV": "Canlı TV",
|
||||
"HeaderNextUp": "Sonraki hafta",
|
||||
"HeaderNextUp": "Gelecek Hafta",
|
||||
"HeaderRecordingGroups": "Kayıt Grupları",
|
||||
"HomeVideos": "Ev videoları",
|
||||
"Inherit": "Devral",
|
||||
|
@ -113,5 +113,7 @@
|
|||
"TaskRefreshLibrary": "Medya Kütüphanesini Tara",
|
||||
"TaskRefreshChapterImagesDescription": "Sahnelere ayrılmış videolar için küçük resimler oluştur.",
|
||||
"TaskRefreshChapterImages": "Bölüm Resimlerini Çıkar",
|
||||
"TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler."
|
||||
"TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler.",
|
||||
"TaskCleanActivityLog": "İşlem Günlüğünü Temizle",
|
||||
"TaskCleanActivityLogDescription": "Belirtilen sureden daha eski etkinlik log kayıtları silindi."
|
||||
}
|
||||
|
|
|
@ -112,5 +112,7 @@
|
|||
"Books": "Sách",
|
||||
"AuthenticationSucceededWithUserName": "{0} xác thực thành công",
|
||||
"Application": "Ứng Dụng",
|
||||
"AppDeviceValues": "Ứng Dụng: {0}, Thiết Bị: {1}"
|
||||
"AppDeviceValues": "Ứng Dụng: {0}, Thiết Bị: {1}",
|
||||
"TaskCleanActivityLogDescription": "Xóa các mục nhật ký hoạt động cũ hơn độ tuổi đã cài đặt.",
|
||||
"TaskCleanActivityLog": "Xóa Nhật Ký Hoạt Động"
|
||||
}
|
||||
|
|
|
@ -113,5 +113,7 @@
|
|||
"TaskCleanCacheDescription": "删除系统不再需要的缓存文件。",
|
||||
"TaskCleanCache": "清理缓存目录",
|
||||
"TasksApplicationCategory": "应用程序",
|
||||
"TasksMaintenanceCategory": "维护"
|
||||
"TasksMaintenanceCategory": "维护",
|
||||
"TaskCleanActivityLog": "清理程序日志",
|
||||
"TaskCleanActivityLogDescription": "删除早于设置时间的活动日志条目。"
|
||||
}
|
||||
|
|
|
@ -112,5 +112,7 @@
|
|||
"TaskRefreshChapterImagesDescription": "為有章節的影片建立縮圖。",
|
||||
"TasksChannelsCategory": "網路頻道",
|
||||
"TasksApplicationCategory": "應用程式",
|
||||
"TasksMaintenanceCategory": "維修"
|
||||
"TasksMaintenanceCategory": "維護",
|
||||
"TaskCleanActivityLogDescription": "刪除超過所設時間的活動紀錄。",
|
||||
"TaskCleanActivityLog": "清除活動紀錄"
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ using MediaBrowser.Common.Configuration;
|
|||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Common.Updates;
|
||||
using MediaBrowser.Common.System;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Events;
|
||||
using MediaBrowser.Controller.Events.Updates;
|
||||
|
@ -25,7 +25,6 @@ using MediaBrowser.Model.Net;
|
|||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Updates;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MediaBrowser.Model.System;
|
||||
|
||||
namespace Emby.Server.Implementations.Updates
|
||||
{
|
||||
|
@ -49,7 +48,7 @@ namespace Emby.Server.Implementations.Updates
|
|||
/// Gets the application host.
|
||||
/// </summary>
|
||||
/// <value>The application host.</value>
|
||||
private readonly IApplicationHost _applicationHost;
|
||||
private readonly IServerApplicationHost _applicationHost;
|
||||
|
||||
private readonly IZipClient _zipClient;
|
||||
|
||||
|
@ -67,7 +66,7 @@ namespace Emby.Server.Implementations.Updates
|
|||
|
||||
public InstallationManager(
|
||||
ILogger<InstallationManager> logger,
|
||||
IApplicationHost appHost,
|
||||
IServerApplicationHost appHost,
|
||||
IApplicationPaths appPaths,
|
||||
IEventManager eventManager,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
|
@ -217,7 +216,8 @@ namespace Emby.Server.Implementations.Updates
|
|||
|
||||
private IEnumerable<InstallationInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
|
||||
{
|
||||
foreach (var plugin in _applicationHost.Plugins)
|
||||
var plugins = _applicationHost.GetLocalPlugins(_appPaths.PluginsPath);
|
||||
foreach (var plugin in plugins)
|
||||
{
|
||||
var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version);
|
||||
var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version);
|
||||
|
|
|
@ -50,6 +50,13 @@ namespace Jellyfin.Api.Auth
|
|||
bool localAccessOnly = false,
|
||||
bool requiredDownloadPermission = false)
|
||||
{
|
||||
// ApiKey is currently global admin, always allow.
|
||||
var isApiKey = ClaimHelpers.GetIsApiKey(claimsPrincipal);
|
||||
if (isApiKey)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ensure claim has userId.
|
||||
var userId = ClaimHelpers.GetUserId(claimsPrincipal);
|
||||
if (!userId.HasValue)
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
using System.Globalization;
|
||||
using System.Security.Authentication;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -43,24 +43,23 @@ namespace Jellyfin.Api.Auth
|
|||
try
|
||||
{
|
||||
var authorizationInfo = _authService.Authenticate(Request);
|
||||
if (authorizationInfo == null)
|
||||
var role = UserRoles.User;
|
||||
if (authorizationInfo.IsApiKey || authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
|
||||
{
|
||||
return Task.FromResult(AuthenticateResult.NoResult());
|
||||
// TODO return when legacy API is removed.
|
||||
// Don't spam the log with "Invalid User"
|
||||
// return Task.FromResult(AuthenticateResult.Fail("Invalid user"));
|
||||
role = UserRoles.Administrator;
|
||||
}
|
||||
|
||||
var claims = new[]
|
||||
{
|
||||
new Claim(ClaimTypes.Name, authorizationInfo.User.Username),
|
||||
new Claim(ClaimTypes.Role, authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User),
|
||||
new Claim(ClaimTypes.Name, authorizationInfo.User?.Username ?? string.Empty),
|
||||
new Claim(ClaimTypes.Role, role),
|
||||
new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)),
|
||||
new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId),
|
||||
new Claim(InternalClaimTypes.Device, authorizationInfo.Device),
|
||||
new Claim(InternalClaimTypes.Client, authorizationInfo.Client),
|
||||
new Claim(InternalClaimTypes.Version, authorizationInfo.Version),
|
||||
new Claim(InternalClaimTypes.Token, authorizationInfo.Token),
|
||||
new Claim(InternalClaimTypes.IsApiKey, authorizationInfo.IsApiKey.ToString(CultureInfo.InvariantCulture))
|
||||
};
|
||||
|
||||
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
||||
|
|
|
@ -29,13 +29,15 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
|
|||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement)
|
||||
{
|
||||
var validated = ValidateClaims(context.User);
|
||||
if (!validated)
|
||||
if (validated)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Fail();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
context.Succeed(requirement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,13 +29,15 @@ namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy
|
|||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement)
|
||||
{
|
||||
var validated = ValidateClaims(context.User, ignoreSchedule: true);
|
||||
if (!validated)
|
||||
if (validated)
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Fail();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
context.Succeed(requirement);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,13 +29,13 @@ namespace Jellyfin.Api.Auth.LocalAccessPolicy
|
|||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessRequirement requirement)
|
||||
{
|
||||
var validated = ValidateClaims(context.User, localAccessOnly: true);
|
||||
if (!validated)
|
||||
if (validated)
|
||||
{
|
||||
context.Fail();
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
context.Fail();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
|
|
|
@ -34,5 +34,10 @@
|
|||
/// Token.
|
||||
/// </summary>
|
||||
public const string Token = "Jellyfin-Token";
|
||||
|
||||
/// <summary>
|
||||
/// Is Api Key.
|
||||
/// </summary>
|
||||
public const string IsApiKey = "Jellyfin-IsApiKey";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -146,9 +146,9 @@ namespace Jellyfin.Api.Controllers
|
|||
NameLessThan = nameLessThan,
|
||||
NameStartsWith = nameStartsWith,
|
||||
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
||||
Tags = RequestHelpers.Split(tags, ',', true),
|
||||
OfficialRatings = RequestHelpers.Split(officialRatings, ',', true),
|
||||
Genres = RequestHelpers.Split(genres, ',', true),
|
||||
Tags = RequestHelpers.Split(tags, '|', true),
|
||||
OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
|
||||
Genres = RequestHelpers.Split(genres, '|', true),
|
||||
GenreIds = RequestHelpers.GetGuids(genreIds),
|
||||
StudioIds = RequestHelpers.GetGuids(studioIds),
|
||||
Person = person,
|
||||
|
@ -354,9 +354,9 @@ namespace Jellyfin.Api.Controllers
|
|||
NameLessThan = nameLessThan,
|
||||
NameStartsWith = nameStartsWith,
|
||||
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
||||
Tags = RequestHelpers.Split(tags, ',', true),
|
||||
OfficialRatings = RequestHelpers.Split(officialRatings, ',', true),
|
||||
Genres = RequestHelpers.Split(genres, ',', true),
|
||||
Tags = RequestHelpers.Split(tags, '|', true),
|
||||
OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
|
||||
Genres = RequestHelpers.Split(genres, '|', true),
|
||||
GenreIds = RequestHelpers.GetGuids(genreIds),
|
||||
StudioIds = RequestHelpers.GetGuids(studioIds),
|
||||
Person = person,
|
||||
|
|
|
@ -15,7 +15,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <summary>
|
||||
/// Devices Controller.
|
||||
/// </summary>
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
public class DevicesController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IDeviceManager _deviceManager;
|
||||
|
@ -46,7 +46,6 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <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] Guid? userId)
|
||||
{
|
||||
|
@ -62,7 +61,6 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <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)
|
||||
|
@ -84,7 +82,6 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <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)
|
||||
|
@ -107,7 +104,6 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <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(
|
||||
|
|
|
@ -81,6 +81,9 @@ namespace Jellyfin.Api.Controllers
|
|||
dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture);
|
||||
dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
|
||||
|
||||
// This will essentially be a noop if no changes have been made, but new prefs must be saved at least.
|
||||
_displayPreferencesManager.SaveChanges();
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
|
|
|
@ -77,6 +77,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// Gets Dlna media receiver registrar xml.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <response code="200">Dlna media receiver registrar xml returned.</response>
|
||||
/// <returns>Dlna media receiver registrar xml.</returns>
|
||||
[HttpGet("{serverId}/MediaReceiverRegistrar")]
|
||||
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")]
|
||||
|
@ -94,6 +95,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// Gets Dlna media receiver registrar xml.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <response code="200">Dlna media receiver registrar xml returned.</response>
|
||||
/// <returns>Dlna media receiver registrar xml.</returns>
|
||||
[HttpGet("{serverId}/ConnectionManager")]
|
||||
[HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")]
|
||||
|
@ -111,8 +113,12 @@ namespace Jellyfin.Api.Controllers
|
|||
/// Process a content directory control request.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <response code="200">Request processed.</response>
|
||||
/// <returns>Control response.</returns>
|
||||
[HttpPost("{serverId}/ContentDirectory/Control")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Produces(MediaTypeNames.Text.Xml)]
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId)
|
||||
{
|
||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
|
||||
|
@ -122,8 +128,12 @@ namespace Jellyfin.Api.Controllers
|
|||
/// Process a connection manager control request.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <response code="200">Request processed.</response>
|
||||
/// <returns>Control response.</returns>
|
||||
[HttpPost("{serverId}/ConnectionManager/Control")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Produces(MediaTypeNames.Text.Xml)]
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId)
|
||||
{
|
||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
|
||||
|
@ -133,8 +143,12 @@ namespace Jellyfin.Api.Controllers
|
|||
/// Process a media receiver registrar control request.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <response code="200">Request processed.</response>
|
||||
/// <returns>Control response.</returns>
|
||||
[HttpPost("{serverId}/MediaReceiverRegistrar/Control")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Produces(MediaTypeNames.Text.Xml)]
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId)
|
||||
{
|
||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
|
||||
|
@ -144,11 +158,15 @@ namespace Jellyfin.Api.Controllers
|
|||
/// Processes an event subscription request.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <response code="200">Request processed.</response>
|
||||
/// <returns>Event subscription response.</returns>
|
||||
[HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")]
|
||||
[HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Produces(MediaTypeNames.Text.Xml)]
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId)
|
||||
{
|
||||
return ProcessEventRequest(_mediaReceiverRegistrar);
|
||||
|
@ -158,11 +176,15 @@ namespace Jellyfin.Api.Controllers
|
|||
/// Processes an event subscription request.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <response code="200">Request processed.</response>
|
||||
/// <returns>Event subscription response.</returns>
|
||||
[HttpSubscribe("{serverId}/ContentDirectory/Events")]
|
||||
[HttpUnsubscribe("{serverId}/ContentDirectory/Events")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Produces(MediaTypeNames.Text.Xml)]
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId)
|
||||
{
|
||||
return ProcessEventRequest(_contentDirectory);
|
||||
|
@ -172,11 +194,15 @@ namespace Jellyfin.Api.Controllers
|
|||
/// Processes an event subscription request.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <response code="200">Request processed.</response>
|
||||
/// <returns>Event subscription response.</returns>
|
||||
[HttpSubscribe("{serverId}/ConnectionManager/Events")]
|
||||
[HttpUnsubscribe("{serverId}/ConnectionManager/Events")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Produces(MediaTypeNames.Text.Xml)]
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId)
|
||||
{
|
||||
return ProcessEventRequest(_connectionManager);
|
||||
|
|
|
@ -295,6 +295,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
|
||||
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
||||
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
|
||||
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
|
||||
|
@ -351,6 +352,7 @@ namespace Jellyfin.Api.Controllers
|
|||
[FromQuery] bool? breakOnNonKeyFrames,
|
||||
[FromQuery] int? audioSampleRate,
|
||||
[FromQuery] int? maxAudioBitDepth,
|
||||
[FromQuery] int? maxStreamingBitrate,
|
||||
[FromQuery] int? audioBitRate,
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
|
@ -403,7 +405,7 @@ namespace Jellyfin.Api.Controllers
|
|||
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
||||
AudioSampleRate = audioSampleRate,
|
||||
MaxAudioChannels = maxAudioChannels,
|
||||
AudioBitRate = audioBitRate,
|
||||
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
|
||||
MaxAudioBitDepth = maxAudioBitDepth,
|
||||
AudioChannels = audioChannels,
|
||||
Profile = profile,
|
||||
|
@ -623,6 +625,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
|
||||
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
||||
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
|
||||
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
|
||||
|
@ -677,6 +680,7 @@ namespace Jellyfin.Api.Controllers
|
|||
[FromQuery] bool? breakOnNonKeyFrames,
|
||||
[FromQuery] int? audioSampleRate,
|
||||
[FromQuery] int? maxAudioBitDepth,
|
||||
[FromQuery] int? maxStreamingBitrate,
|
||||
[FromQuery] int? audioBitRate,
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
|
@ -729,7 +733,7 @@ namespace Jellyfin.Api.Controllers
|
|||
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
||||
AudioSampleRate = audioSampleRate,
|
||||
MaxAudioChannels = maxAudioChannels,
|
||||
AudioBitRate = audioBitRate,
|
||||
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
|
||||
MaxAudioBitDepth = maxAudioBitDepth,
|
||||
AudioChannels = audioChannels,
|
||||
Profile = profile,
|
||||
|
@ -959,6 +963,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
|
||||
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
||||
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
|
||||
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
|
||||
|
@ -1017,6 +1022,7 @@ namespace Jellyfin.Api.Controllers
|
|||
[FromQuery] bool? breakOnNonKeyFrames,
|
||||
[FromQuery] int? audioSampleRate,
|
||||
[FromQuery] int? maxAudioBitDepth,
|
||||
[FromQuery] int? maxStreamingBitrate,
|
||||
[FromQuery] int? audioBitRate,
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
|
@ -1069,7 +1075,7 @@ namespace Jellyfin.Api.Controllers
|
|||
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
||||
AudioSampleRate = audioSampleRate,
|
||||
MaxAudioChannels = maxAudioChannels,
|
||||
AudioBitRate = audioBitRate,
|
||||
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
|
||||
MaxAudioBitDepth = maxAudioBitDepth,
|
||||
AudioChannels = audioChannels,
|
||||
Profile = profile,
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
@ -49,7 +47,6 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <summary>
|
||||
/// Gets all genres from a given item, folder, or the entire library.
|
||||
/// </summary>
|
||||
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
|
||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="searchTerm">The search term.</param>
|
||||
|
@ -57,22 +54,9 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
|
||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
|
||||
/// <param name="filters">Optional. Specify additional filters to apply.</param>
|
||||
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
|
||||
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
|
||||
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
|
||||
/// <param name="enableUserData">Optional, include user data.</param>
|
||||
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
|
||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
|
||||
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
|
||||
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
|
||||
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="userId">User id.</param>
|
||||
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
|
||||
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
|
||||
|
@ -84,7 +68,6 @@ namespace Jellyfin.Api.Controllers
|
|||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetGenres(
|
||||
[FromQuery] double? minCommunityRating,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? searchTerm,
|
||||
|
@ -92,22 +75,9 @@ namespace Jellyfin.Api.Controllers
|
|||
[FromQuery] ItemFields[] fields,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
||||
[FromQuery] bool? isFavorite,
|
||||
[FromQuery] string? mediaTypes,
|
||||
[FromQuery] string? genres,
|
||||
[FromQuery] string? genreIds,
|
||||
[FromQuery] string? officialRatings,
|
||||
[FromQuery] string? tags,
|
||||
[FromQuery] string? years,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] ImageType[] enableImageTypes,
|
||||
[FromQuery] string? person,
|
||||
[FromQuery] string? personIds,
|
||||
[FromQuery] string? personTypes,
|
||||
[FromQuery] string? studios,
|
||||
[FromQuery] string? studioIds,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
|
@ -117,42 +87,22 @@ namespace Jellyfin.Api.Controllers
|
|||
{
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
|
||||
|
||||
User? user = null;
|
||||
BaseItem parentItem;
|
||||
User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null;
|
||||
|
||||
if (userId.HasValue && !userId.Equals(Guid.Empty))
|
||||
{
|
||||
user = _userManager.GetUserById(userId.Value);
|
||||
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
else
|
||||
{
|
||||
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
var parentItem = _libraryManager.GetParentItem(parentId, userId);
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
|
||||
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
|
||||
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
|
||||
StartIndex = startIndex,
|
||||
Limit = limit,
|
||||
IsFavorite = isFavorite,
|
||||
NameLessThan = nameLessThan,
|
||||
NameStartsWith = nameStartsWith,
|
||||
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
||||
Tags = RequestHelpers.Split(tags, '|', true),
|
||||
OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
|
||||
Genres = RequestHelpers.Split(genres, '|', true),
|
||||
GenreIds = RequestHelpers.GetGuids(genreIds),
|
||||
StudioIds = RequestHelpers.GetGuids(studioIds),
|
||||
Person = person,
|
||||
PersonIds = RequestHelpers.GetGuids(personIds),
|
||||
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
|
||||
Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(),
|
||||
MinCommunityRating = minCommunityRating,
|
||||
DtoOptions = dtoOptions,
|
||||
SearchTerm = searchTerm,
|
||||
EnableTotalRecordCount = enableTotalRecordCount
|
||||
|
@ -170,87 +120,20 @@ namespace Jellyfin.Api.Controllers
|
|||
}
|
||||
}
|
||||
|
||||
// Studios
|
||||
if (!string.IsNullOrEmpty(studios))
|
||||
QueryResult<(BaseItem, ItemCounts)> result;
|
||||
if (parentItem is ICollectionFolder parentCollectionFolder
|
||||
&& (string.Equals(parentCollectionFolder.CollectionType, CollectionType.Music, StringComparison.Ordinal)
|
||||
|| string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal)))
|
||||
{
|
||||
query.StudioIds = studios.Split('|')
|
||||
.Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return _libraryManager.GetStudio(i);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}).Where(i => i != null)
|
||||
.Select(i => i!.Id)
|
||||
.ToArray();
|
||||
result = _libraryManager.GetMusicGenres(query);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = _libraryManager.GetGenres(query);
|
||||
}
|
||||
|
||||
foreach (var filter in filters)
|
||||
{
|
||||
switch (filter)
|
||||
{
|
||||
case ItemFilter.Dislikes:
|
||||
query.IsLiked = false;
|
||||
break;
|
||||
case ItemFilter.IsFavorite:
|
||||
query.IsFavorite = true;
|
||||
break;
|
||||
case ItemFilter.IsFavoriteOrLikes:
|
||||
query.IsFavoriteOrLiked = true;
|
||||
break;
|
||||
case ItemFilter.IsFolder:
|
||||
query.IsFolder = true;
|
||||
break;
|
||||
case ItemFilter.IsNotFolder:
|
||||
query.IsFolder = false;
|
||||
break;
|
||||
case ItemFilter.IsPlayed:
|
||||
query.IsPlayed = true;
|
||||
break;
|
||||
case ItemFilter.IsResumable:
|
||||
query.IsResumable = true;
|
||||
break;
|
||||
case ItemFilter.IsUnplayed:
|
||||
query.IsPlayed = false;
|
||||
break;
|
||||
case ItemFilter.Likes:
|
||||
query.IsLiked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var result = new QueryResult<(BaseItem, ItemCounts)>();
|
||||
|
||||
var dtos = result.Items.Select(i =>
|
||||
{
|
||||
var (baseItem, counts) = i;
|
||||
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(includeItemTypes))
|
||||
{
|
||||
dto.ChildCount = counts.ItemCount;
|
||||
dto.ProgramCount = counts.ProgramCount;
|
||||
dto.SeriesCount = counts.SeriesCount;
|
||||
dto.EpisodeCount = counts.EpisodeCount;
|
||||
dto.MovieCount = counts.MovieCount;
|
||||
dto.TrailerCount = counts.TrailerCount;
|
||||
dto.AlbumCount = counts.AlbumCount;
|
||||
dto.SongCount = counts.SongCount;
|
||||
dto.ArtistCount = counts.ArtistCount;
|
||||
}
|
||||
|
||||
return dto;
|
||||
});
|
||||
|
||||
return new QueryResult<BaseItemDto>
|
||||
{
|
||||
Items = dtos.ToArray(),
|
||||
TotalRecordCount = result.TotalRecordCount
|
||||
};
|
||||
var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes);
|
||||
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -309,9 +309,9 @@ namespace Jellyfin.Api.Controllers
|
|||
TotalRecordCount = list.Count
|
||||
};
|
||||
|
||||
if (limit.HasValue)
|
||||
if (limit.HasValue && limit < list.Count)
|
||||
{
|
||||
list = list.Take(limit.Value).ToList();
|
||||
list = list.GetRange(0, limit.Value);
|
||||
}
|
||||
|
||||
var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user);
|
||||
|
|
|
@ -590,7 +590,7 @@ namespace Jellyfin.Api.Controllers
|
|||
IsKids = isKids,
|
||||
IsSports = isSports,
|
||||
SeriesTimerId = seriesTimerId,
|
||||
Genres = RequestHelpers.Split(genres, ',', true),
|
||||
Genres = RequestHelpers.Split(genres, '|', true),
|
||||
GenreIds = RequestHelpers.GetGuids(genreIds)
|
||||
};
|
||||
|
||||
|
@ -645,7 +645,7 @@ namespace Jellyfin.Api.Controllers
|
|||
IsKids = body.IsKids,
|
||||
IsSports = body.IsSports,
|
||||
SeriesTimerId = body.SeriesTimerId,
|
||||
Genres = RequestHelpers.Split(body.Genres, ',', true),
|
||||
Genres = RequestHelpers.Split(body.Genres, '|', true),
|
||||
GenreIds = RequestHelpers.GetGuids(body.GenreIds)
|
||||
};
|
||||
|
||||
|
@ -1215,11 +1215,8 @@ namespace Jellyfin.Api.Controllers
|
|||
return NotFound();
|
||||
}
|
||||
|
||||
await using var memoryStream = new MemoryStream();
|
||||
await new ProgressiveFileCopier(liveStreamInfo, null, _transcodingJobHelper, CancellationToken.None)
|
||||
.WriteToAsync(memoryStream, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
return File(memoryStream, MimeTypes.GetMimeType("file." + container));
|
||||
var liveStream = new ProgressiveFileStream(liveStreamInfo.GetFilePath(), null, _transcodingJobHelper);
|
||||
return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container));
|
||||
}
|
||||
|
||||
private void AssertUserCanManageLiveTv()
|
||||
|
|
|
@ -104,7 +104,7 @@ namespace Jellyfin.Api.Controllers
|
|||
public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] long? maxStreamingBitrate,
|
||||
[FromQuery] int? maxStreamingBitrate,
|
||||
[FromQuery] long? startTimeTicks,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? subtitleStreamIndex,
|
||||
|
@ -234,7 +234,7 @@ namespace Jellyfin.Api.Controllers
|
|||
[FromQuery] string? openToken,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] long? maxStreamingBitrate,
|
||||
[FromQuery] int? maxStreamingBitrate,
|
||||
[FromQuery] long? startTimeTicks,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? subtitleStreamIndex,
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
@ -49,7 +47,6 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <summary>
|
||||
/// Gets all music genres from a given item, folder, or the entire library.
|
||||
/// </summary>
|
||||
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
|
||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="searchTerm">The search term.</param>
|
||||
|
@ -57,22 +54,9 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
|
||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
|
||||
/// <param name="filters">Optional. Specify additional filters to apply.</param>
|
||||
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
|
||||
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
|
||||
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
|
||||
/// <param name="enableUserData">Optional, include user data.</param>
|
||||
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
|
||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
|
||||
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
|
||||
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
|
||||
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="userId">User id.</param>
|
||||
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
|
||||
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
|
||||
|
@ -82,8 +66,8 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <response code="200">Music genres returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the queryresult of music genres.</returns>
|
||||
[HttpGet]
|
||||
[Obsolete("Use GetGenres instead")]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetMusicGenres(
|
||||
[FromQuery] double? minCommunityRating,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? searchTerm,
|
||||
|
@ -91,22 +75,9 @@ namespace Jellyfin.Api.Controllers
|
|||
[FromQuery] ItemFields[] fields,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
||||
[FromQuery] bool? isFavorite,
|
||||
[FromQuery] string? mediaTypes,
|
||||
[FromQuery] string? genres,
|
||||
[FromQuery] string? genreIds,
|
||||
[FromQuery] string? officialRatings,
|
||||
[FromQuery] string? tags,
|
||||
[FromQuery] string? years,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] ImageType[] enableImageTypes,
|
||||
[FromQuery] string? person,
|
||||
[FromQuery] string? personIds,
|
||||
[FromQuery] string? personTypes,
|
||||
[FromQuery] string? studios,
|
||||
[FromQuery] string? studioIds,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
|
@ -116,42 +87,22 @@ namespace Jellyfin.Api.Controllers
|
|||
{
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
|
||||
|
||||
User? user = null;
|
||||
BaseItem parentItem;
|
||||
User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null;
|
||||
|
||||
if (userId.HasValue && !userId.Equals(Guid.Empty))
|
||||
{
|
||||
user = _userManager.GetUserById(userId.Value);
|
||||
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
else
|
||||
{
|
||||
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
var parentItem = _libraryManager.GetParentItem(parentId, userId);
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
|
||||
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
|
||||
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
|
||||
StartIndex = startIndex,
|
||||
Limit = limit,
|
||||
IsFavorite = isFavorite,
|
||||
NameLessThan = nameLessThan,
|
||||
NameStartsWith = nameStartsWith,
|
||||
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
||||
Tags = RequestHelpers.Split(tags, '|', true),
|
||||
OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
|
||||
Genres = RequestHelpers.Split(genres, '|', true),
|
||||
GenreIds = RequestHelpers.GetGuids(genreIds),
|
||||
StudioIds = RequestHelpers.GetGuids(studioIds),
|
||||
Person = person,
|
||||
PersonIds = RequestHelpers.GetGuids(personIds),
|
||||
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
|
||||
Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(),
|
||||
MinCommunityRating = minCommunityRating,
|
||||
DtoOptions = dtoOptions,
|
||||
SearchTerm = searchTerm,
|
||||
EnableTotalRecordCount = enableTotalRecordCount
|
||||
|
@ -169,87 +120,10 @@ namespace Jellyfin.Api.Controllers
|
|||
}
|
||||
}
|
||||
|
||||
// Studios
|
||||
if (!string.IsNullOrEmpty(studios))
|
||||
{
|
||||
query.StudioIds = studios.Split('|')
|
||||
.Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return _libraryManager.GetStudio(i);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}).Where(i => i != null)
|
||||
.Select(i => i!.Id)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
foreach (var filter in filters)
|
||||
{
|
||||
switch (filter)
|
||||
{
|
||||
case ItemFilter.Dislikes:
|
||||
query.IsLiked = false;
|
||||
break;
|
||||
case ItemFilter.IsFavorite:
|
||||
query.IsFavorite = true;
|
||||
break;
|
||||
case ItemFilter.IsFavoriteOrLikes:
|
||||
query.IsFavoriteOrLiked = true;
|
||||
break;
|
||||
case ItemFilter.IsFolder:
|
||||
query.IsFolder = true;
|
||||
break;
|
||||
case ItemFilter.IsNotFolder:
|
||||
query.IsFolder = false;
|
||||
break;
|
||||
case ItemFilter.IsPlayed:
|
||||
query.IsPlayed = true;
|
||||
break;
|
||||
case ItemFilter.IsResumable:
|
||||
query.IsResumable = true;
|
||||
break;
|
||||
case ItemFilter.IsUnplayed:
|
||||
query.IsPlayed = false;
|
||||
break;
|
||||
case ItemFilter.Likes:
|
||||
query.IsLiked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var result = _libraryManager.GetMusicGenres(query);
|
||||
|
||||
var dtos = result.Items.Select(i =>
|
||||
{
|
||||
var (baseItem, counts) = i;
|
||||
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(includeItemTypes))
|
||||
{
|
||||
dto.ChildCount = counts.ItemCount;
|
||||
dto.ProgramCount = counts.ProgramCount;
|
||||
dto.SeriesCount = counts.SeriesCount;
|
||||
dto.EpisodeCount = counts.EpisodeCount;
|
||||
dto.MovieCount = counts.MovieCount;
|
||||
dto.TrailerCount = counts.TrailerCount;
|
||||
dto.AlbumCount = counts.AlbumCount;
|
||||
dto.SongCount = counts.SongCount;
|
||||
dto.ArtistCount = counts.ArtistCount;
|
||||
}
|
||||
|
||||
return dto;
|
||||
});
|
||||
|
||||
return new QueryResult<BaseItemDto>
|
||||
{
|
||||
Items = dtos.ToArray(),
|
||||
TotalRecordCount = result.TotalRecordCount
|
||||
};
|
||||
var shouldIncludeItemTypes = !string.IsNullOrWhiteSpace(includeItemTypes);
|
||||
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
|
@ -28,6 +27,7 @@ namespace Jellyfin.Api.Controllers
|
|||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IDtoService _dtoService;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PersonsController"/> class.
|
||||
|
@ -35,220 +35,81 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
|
||||
public PersonsController(
|
||||
ILibraryManager libraryManager,
|
||||
IDtoService dtoService,
|
||||
IUserManager userManager)
|
||||
IUserManager userManager,
|
||||
IUserDataManager userDataManager)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_dtoService = dtoService;
|
||||
_userManager = userManager;
|
||||
_userDataManager = userDataManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all persons from a given item, folder, or the entire library.
|
||||
/// Gets all persons.
|
||||
/// </summary>
|
||||
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
|
||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="searchTerm">The search term.</param>
|
||||
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
|
||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
|
||||
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
|
||||
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
|
||||
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
|
||||
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
|
||||
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not. userId is required.</param>
|
||||
/// <param name="enableUserData">Optional, include user data.</param>
|
||||
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
|
||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
|
||||
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
|
||||
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
|
||||
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="excludePersonTypes">Optional. If specified results will be filtered to exclude those containing the specified PersonType. Allows multiple, comma-delimited.</param>
|
||||
/// <param name="personTypes">Optional. If specified results will be filtered to include only those containing the specified PersonType. Allows multiple, comma-delimited.</param>
|
||||
/// <param name="appearsInItemId">Optional. If specified, person results will be filtered on items related to said persons.</param>
|
||||
/// <param name="userId">User id.</param>
|
||||
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
|
||||
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
|
||||
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
|
||||
/// <param name="enableImages">Optional, include image information in output.</param>
|
||||
/// <param name="enableTotalRecordCount">Optional. Include total record count.</param>
|
||||
/// <response code="200">Persons returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the queryresult of persons.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetPersons(
|
||||
[FromQuery] double? minCommunityRating,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? searchTerm,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery] ItemFields[] fields,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
||||
[FromQuery] bool? isFavorite,
|
||||
[FromQuery] string? mediaTypes,
|
||||
[FromQuery] string? genres,
|
||||
[FromQuery] string? genreIds,
|
||||
[FromQuery] string? officialRatings,
|
||||
[FromQuery] string? tags,
|
||||
[FromQuery] string? years,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] ImageType[] enableImageTypes,
|
||||
[FromQuery] string? person,
|
||||
[FromQuery] string? personIds,
|
||||
[FromQuery] string? excludePersonTypes,
|
||||
[FromQuery] string? personTypes,
|
||||
[FromQuery] string? studios,
|
||||
[FromQuery] string? studioIds,
|
||||
[FromQuery] string? appearsInItemId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
[FromQuery] string? nameLessThan,
|
||||
[FromQuery] bool? enableImages = true,
|
||||
[FromQuery] bool enableTotalRecordCount = true)
|
||||
[FromQuery] bool? enableImages = true)
|
||||
{
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
User? user = null;
|
||||
BaseItem parentItem;
|
||||
|
||||
if (userId.HasValue && !userId.Equals(Guid.Empty))
|
||||
{
|
||||
user = _userManager.GetUserById(userId.Value);
|
||||
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
else
|
||||
{
|
||||
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite);
|
||||
var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery
|
||||
{
|
||||
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
|
||||
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
|
||||
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
|
||||
StartIndex = startIndex,
|
||||
Limit = limit,
|
||||
IsFavorite = isFavorite,
|
||||
NameLessThan = nameLessThan,
|
||||
NameStartsWith = nameStartsWith,
|
||||
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
||||
Tags = RequestHelpers.Split(tags, '|', true),
|
||||
OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
|
||||
Genres = RequestHelpers.Split(genres, '|', true),
|
||||
GenreIds = RequestHelpers.GetGuids(genreIds),
|
||||
StudioIds = RequestHelpers.GetGuids(studioIds),
|
||||
Person = person,
|
||||
PersonIds = RequestHelpers.GetGuids(personIds),
|
||||
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
|
||||
Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(),
|
||||
MinCommunityRating = minCommunityRating,
|
||||
DtoOptions = dtoOptions,
|
||||
SearchTerm = searchTerm,
|
||||
EnableTotalRecordCount = enableTotalRecordCount
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(parentId))
|
||||
{
|
||||
if (parentItem is Folder)
|
||||
{
|
||||
query.AncestorIds = new[] { new Guid(parentId) };
|
||||
}
|
||||
else
|
||||
{
|
||||
query.ItemIds = new[] { new Guid(parentId) };
|
||||
}
|
||||
}
|
||||
|
||||
// Studios
|
||||
if (!string.IsNullOrEmpty(studios))
|
||||
{
|
||||
query.StudioIds = studios.Split('|')
|
||||
.Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return _libraryManager.GetStudio(i);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}).Where(i => i != null)
|
||||
.Select(i => i!.Id)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
foreach (var filter in filters)
|
||||
{
|
||||
switch (filter)
|
||||
{
|
||||
case ItemFilter.Dislikes:
|
||||
query.IsLiked = false;
|
||||
break;
|
||||
case ItemFilter.IsFavorite:
|
||||
query.IsFavorite = true;
|
||||
break;
|
||||
case ItemFilter.IsFavoriteOrLikes:
|
||||
query.IsFavoriteOrLiked = true;
|
||||
break;
|
||||
case ItemFilter.IsFolder:
|
||||
query.IsFolder = true;
|
||||
break;
|
||||
case ItemFilter.IsNotFolder:
|
||||
query.IsFolder = false;
|
||||
break;
|
||||
case ItemFilter.IsPlayed:
|
||||
query.IsPlayed = true;
|
||||
break;
|
||||
case ItemFilter.IsResumable:
|
||||
query.IsResumable = true;
|
||||
break;
|
||||
case ItemFilter.IsUnplayed:
|
||||
query.IsPlayed = false;
|
||||
break;
|
||||
case ItemFilter.Likes:
|
||||
query.IsLiked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var result = new QueryResult<(BaseItem, ItemCounts)>();
|
||||
|
||||
var dtos = result.Items.Select(i =>
|
||||
{
|
||||
var (baseItem, counts) = i;
|
||||
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(includeItemTypes))
|
||||
{
|
||||
dto.ChildCount = counts.ItemCount;
|
||||
dto.ProgramCount = counts.ProgramCount;
|
||||
dto.SeriesCount = counts.SeriesCount;
|
||||
dto.EpisodeCount = counts.EpisodeCount;
|
||||
dto.MovieCount = counts.MovieCount;
|
||||
dto.TrailerCount = counts.TrailerCount;
|
||||
dto.AlbumCount = counts.AlbumCount;
|
||||
dto.SongCount = counts.SongCount;
|
||||
dto.ArtistCount = counts.ArtistCount;
|
||||
}
|
||||
|
||||
return dto;
|
||||
ExcludePersonTypes = RequestHelpers.Split(excludePersonTypes, ',', true),
|
||||
NameContains = searchTerm,
|
||||
User = user,
|
||||
IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite,
|
||||
AppearsInItemId = string.IsNullOrEmpty(appearsInItemId) ? Guid.Empty : Guid.Parse(appearsInItemId),
|
||||
Limit = limit ?? 0
|
||||
});
|
||||
|
||||
return new QueryResult<BaseItemDto>
|
||||
{
|
||||
Items = dtos.ToArray(),
|
||||
TotalRecordCount = result.TotalRecordCount
|
||||
Items = peopleItems.Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user)).ToArray(),
|
||||
TotalRecordCount = peopleItems.Count
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
@ -47,7 +45,6 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <summary>
|
||||
/// Gets all studios from a given item, folder, or the entire library.
|
||||
/// </summary>
|
||||
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
|
||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="searchTerm">Optional. Search term.</param>
|
||||
|
@ -55,22 +52,10 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
|
||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
|
||||
/// <param name="filters">Optional. Specify additional filters to apply.</param>
|
||||
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
|
||||
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
|
||||
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
|
||||
/// <param name="enableUserData">Optional, include user data.</param>
|
||||
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
|
||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
|
||||
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param>
|
||||
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
|
||||
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="userId">User id.</param>
|
||||
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
|
||||
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
|
||||
|
@ -82,7 +67,6 @@ namespace Jellyfin.Api.Controllers
|
|||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetStudios(
|
||||
[FromQuery] double? minCommunityRating,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? searchTerm,
|
||||
|
@ -90,22 +74,10 @@ namespace Jellyfin.Api.Controllers
|
|||
[FromQuery] ItemFields[] fields,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
||||
[FromQuery] bool? isFavorite,
|
||||
[FromQuery] string? mediaTypes,
|
||||
[FromQuery] string? genres,
|
||||
[FromQuery] string? genreIds,
|
||||
[FromQuery] string? officialRatings,
|
||||
[FromQuery] string? tags,
|
||||
[FromQuery] string? years,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] ImageType[] enableImageTypes,
|
||||
[FromQuery] string? person,
|
||||
[FromQuery] string? personIds,
|
||||
[FromQuery] string? personTypes,
|
||||
[FromQuery] string? studios,
|
||||
[FromQuery] string? studioIds,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
|
@ -117,44 +89,23 @@ namespace Jellyfin.Api.Controllers
|
|||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
User? user = null;
|
||||
BaseItem parentItem;
|
||||
User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null;
|
||||
|
||||
if (userId.HasValue && !userId.Equals(Guid.Empty))
|
||||
{
|
||||
user = _userManager.GetUserById(userId.Value);
|
||||
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
else
|
||||
{
|
||||
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
var parentItem = _libraryManager.GetParentItem(parentId, userId);
|
||||
|
||||
var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
|
||||
var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
|
||||
var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
ExcludeItemTypes = excludeItemTypesArr,
|
||||
IncludeItemTypes = includeItemTypesArr,
|
||||
MediaTypes = mediaTypesArr,
|
||||
StartIndex = startIndex,
|
||||
Limit = limit,
|
||||
IsFavorite = isFavorite,
|
||||
NameLessThan = nameLessThan,
|
||||
NameStartsWith = nameStartsWith,
|
||||
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
||||
Tags = RequestHelpers.Split(tags, ',', true),
|
||||
OfficialRatings = RequestHelpers.Split(officialRatings, ',', true),
|
||||
Genres = RequestHelpers.Split(genres, ',', true),
|
||||
GenreIds = RequestHelpers.GetGuids(genreIds),
|
||||
StudioIds = RequestHelpers.GetGuids(studioIds),
|
||||
Person = person,
|
||||
PersonIds = RequestHelpers.GetGuids(personIds),
|
||||
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
|
||||
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
|
||||
MinCommunityRating = minCommunityRating,
|
||||
DtoOptions = dtoOptions,
|
||||
SearchTerm = searchTerm,
|
||||
EnableTotalRecordCount = enableTotalRecordCount
|
||||
|
@ -172,84 +123,9 @@ namespace Jellyfin.Api.Controllers
|
|||
}
|
||||
}
|
||||
|
||||
// Studios
|
||||
if (!string.IsNullOrEmpty(studios))
|
||||
{
|
||||
query.StudioIds = studios.Split('|').Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return _libraryManager.GetStudio(i);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}).Where(i => i != null).Select(i => i!.Id)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
foreach (var filter in filters)
|
||||
{
|
||||
switch (filter)
|
||||
{
|
||||
case ItemFilter.Dislikes:
|
||||
query.IsLiked = false;
|
||||
break;
|
||||
case ItemFilter.IsFavorite:
|
||||
query.IsFavorite = true;
|
||||
break;
|
||||
case ItemFilter.IsFavoriteOrLikes:
|
||||
query.IsFavoriteOrLiked = true;
|
||||
break;
|
||||
case ItemFilter.IsFolder:
|
||||
query.IsFolder = true;
|
||||
break;
|
||||
case ItemFilter.IsNotFolder:
|
||||
query.IsFolder = false;
|
||||
break;
|
||||
case ItemFilter.IsPlayed:
|
||||
query.IsPlayed = true;
|
||||
break;
|
||||
case ItemFilter.IsResumable:
|
||||
query.IsResumable = true;
|
||||
break;
|
||||
case ItemFilter.IsUnplayed:
|
||||
query.IsPlayed = false;
|
||||
break;
|
||||
case ItemFilter.Likes:
|
||||
query.IsLiked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var result = new QueryResult<(BaseItem, ItemCounts)>();
|
||||
var dtos = result.Items.Select(i =>
|
||||
{
|
||||
var (baseItem, itemCounts) = i;
|
||||
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(includeItemTypes))
|
||||
{
|
||||
dto.ChildCount = itemCounts.ItemCount;
|
||||
dto.ProgramCount = itemCounts.ProgramCount;
|
||||
dto.SeriesCount = itemCounts.SeriesCount;
|
||||
dto.EpisodeCount = itemCounts.EpisodeCount;
|
||||
dto.MovieCount = itemCounts.MovieCount;
|
||||
dto.TrailerCount = itemCounts.TrailerCount;
|
||||
dto.AlbumCount = itemCounts.AlbumCount;
|
||||
dto.SongCount = itemCounts.SongCount;
|
||||
dto.ArtistCount = itemCounts.ArtistCount;
|
||||
}
|
||||
|
||||
return dto;
|
||||
});
|
||||
|
||||
return new QueryResult<BaseItemDto>
|
||||
{
|
||||
Items = dtos.ToArray(),
|
||||
TotalRecordCount = result.TotalRecordCount
|
||||
};
|
||||
var result = _libraryManager.GetStudios(query);
|
||||
var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes);
|
||||
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -11,6 +11,9 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Models.SubtitleDtos;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
|
@ -21,6 +24,7 @@ using MediaBrowser.Model.Entities;
|
|||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using MediaBrowser.Model.Subtitles;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -34,6 +38,7 @@ namespace Jellyfin.Api.Controllers
|
|||
[Route("")]
|
||||
public class SubtitleController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ISubtitleManager _subtitleManager;
|
||||
private readonly ISubtitleEncoder _subtitleEncoder;
|
||||
|
@ -46,6 +51,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SubtitleController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <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>
|
||||
|
@ -55,6 +61,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <param name="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param>
|
||||
/// <param name="logger">Instance of <see cref="ILogger{SubtitleController}"/> interface.</param>
|
||||
public SubtitleController(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILibraryManager libraryManager,
|
||||
ISubtitleManager subtitleManager,
|
||||
ISubtitleEncoder subtitleEncoder,
|
||||
|
@ -64,6 +71,7 @@ namespace Jellyfin.Api.Controllers
|
|||
IAuthorizationContext authContext,
|
||||
ILogger<SubtitleController> logger)
|
||||
{
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_libraryManager = libraryManager;
|
||||
_subtitleManager = subtitleManager;
|
||||
_subtitleEncoder = subtitleEncoder;
|
||||
|
@ -319,6 +327,33 @@ namespace Jellyfin.Api.Controllers
|
|||
return File(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upload an external subtitle file.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item the subtitle belongs to.</param>
|
||||
/// <param name="body">The request body.</param>
|
||||
/// <response code="204">Subtitle uploaded.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Videos/{itemId}/Subtitles")]
|
||||
public async Task<ActionResult> UploadSubtitle(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromBody, Required] UploadSubtitleDto body)
|
||||
{
|
||||
var video = (Video)_libraryManager.GetItemById(itemId);
|
||||
var data = Convert.FromBase64String(body.Data);
|
||||
await using var memoryStream = new MemoryStream(data);
|
||||
await _subtitleManager.UploadSubtitle(
|
||||
video,
|
||||
new SubtitleResponse
|
||||
{
|
||||
Format = body.Format,
|
||||
Language = body.Language,
|
||||
IsForced = body.IsForced,
|
||||
Stream = memoryStream
|
||||
}).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a subtitle in the specified format.
|
||||
/// </summary>
|
||||
|
@ -351,5 +386,95 @@ namespace Jellyfin.Api.Controllers
|
|||
copyTimestamps,
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of available fallback font files.
|
||||
/// </summary>
|
||||
/// <response code="200">Information retrieved.</response>
|
||||
/// <returns>An array of <see cref="FontFile"/> with the available font files.</returns>
|
||||
[HttpGet("FallbackFont/Fonts")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public IEnumerable<FontFile> GetFallbackFontList()
|
||||
{
|
||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||
var fallbackFontPath = encodingOptions.FallbackFontPath;
|
||||
|
||||
if (!string.IsNullOrEmpty(fallbackFontPath))
|
||||
{
|
||||
var files = _fileSystem.GetFiles(fallbackFontPath, new[] { ".woff", ".woff2", ".ttf", ".otf" }, false, false);
|
||||
var fontFiles = files
|
||||
.Select(i => new FontFile
|
||||
{
|
||||
Name = i.Name,
|
||||
Size = i.Length,
|
||||
DateCreated = _fileSystem.GetCreationTimeUtc(i),
|
||||
DateModified = _fileSystem.GetLastWriteTimeUtc(i)
|
||||
})
|
||||
.OrderBy(i => i.Size)
|
||||
.ThenBy(i => i.Name)
|
||||
.ThenByDescending(i => i.DateModified)
|
||||
.ThenByDescending(i => i.DateCreated);
|
||||
// max total size 20M
|
||||
const int MaxSize = 20971520;
|
||||
var sizeCounter = 0L;
|
||||
foreach (var fontFile in fontFiles)
|
||||
{
|
||||
sizeCounter += fontFile.Size;
|
||||
if (sizeCounter >= MaxSize)
|
||||
{
|
||||
_logger.LogWarning("Some fonts will not be sent due to size limitations");
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return fontFile;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("The path of fallback font folder has not been set");
|
||||
encodingOptions.EnableFallbackFont = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a fallback font file.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the fallback font file to get.</param>
|
||||
/// <response code="200">Fallback font file retrieved.</response>
|
||||
/// <returns>The fallback font file.</returns>
|
||||
[HttpGet("FallbackFont/Fonts/{name}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult GetFallbackFont([FromRoute, Required] string name)
|
||||
{
|
||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||
var fallbackFontPath = encodingOptions.FallbackFontPath;
|
||||
|
||||
if (!string.IsNullOrEmpty(fallbackFontPath))
|
||||
{
|
||||
var fontFile = _fileSystem.GetFiles(fallbackFontPath)
|
||||
.First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||
var fileSize = fontFile?.Length;
|
||||
|
||||
if (fontFile != null && fileSize != null && fileSize > 0)
|
||||
{
|
||||
_logger.LogDebug("Fallback font size is {fileSize} Bytes", fileSize);
|
||||
return PhysicalFile(fontFile.FullName, MimeTypes.GetMimeType(fontFile.FullName));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("The selected font is null or empty");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("The path of fallback font folder has not been set");
|
||||
encodingOptions.EnableFallbackFont = false;
|
||||
}
|
||||
|
||||
// returning HTTP 204 will break the SubtitlesOctopus
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Data.Enums;
|
||||
|
@ -9,6 +10,7 @@ using MediaBrowser.Controller.Entities;
|
|||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
|
@ -18,6 +20,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// The suggestions controller.
|
||||
/// </summary>
|
||||
[Route("")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
public class SuggestionsController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IDtoService _dtoService;
|
||||
|
|
|
@ -76,6 +76,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <param name="maxAudioChannels">Optional. The maximum number of audio channels.</param>
|
||||
/// <param name="transcodingAudioChannels">Optional. The number of how many audio channels to transcode to.</param>
|
||||
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
|
||||
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
||||
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
|
||||
/// <param name="transcodingContainer">Optional. The container to transcode to.</param>
|
||||
/// <param name="transcodingProtocol">Optional. The transcoding protocol.</param>
|
||||
|
@ -88,23 +89,22 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <response code="302">Redirected to remote audio stream.</response>
|
||||
/// <returns>A <see cref="Task"/> containing the audio file.</returns>
|
||||
[HttpGet("Audio/{itemId}/universal")]
|
||||
[HttpGet("Audio/{itemId}/universal.{container}", Name = "GetUniversalAudioStream_2")]
|
||||
[HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")]
|
||||
[HttpHead("Audio/{itemId}/universal.{container}", Name = "HeadUniversalAudioStream_2")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status302Found)]
|
||||
[ProducesAudioFile]
|
||||
public async Task<ActionResult> GetUniversalAudioStream(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute] string? container,
|
||||
[FromQuery] string? container,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] int? transcodingAudioChannels,
|
||||
[FromQuery] long? maxStreamingBitrate,
|
||||
[FromQuery] int? maxStreamingBitrate,
|
||||
[FromQuery] int? audioBitRate,
|
||||
[FromQuery] long? startTimeTicks,
|
||||
[FromQuery] string? transcodingContainer,
|
||||
[FromQuery] string? transcodingProtocol,
|
||||
|
@ -212,7 +212,7 @@ namespace Jellyfin.Api.Controllers
|
|||
AudioSampleRate = maxAudioSampleRate,
|
||||
MaxAudioChannels = maxAudioChannels,
|
||||
MaxAudioBitDepth = maxAudioBitDepth,
|
||||
AudioChannels = isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
|
||||
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
|
||||
StartTimeTicks = startTimeTicks,
|
||||
SubtitleMethod = SubtitleDeliveryMethod.Hls,
|
||||
RequireAvc = true,
|
||||
|
@ -244,7 +244,7 @@ namespace Jellyfin.Api.Controllers
|
|||
BreakOnNonKeyFrames = breakOnNonKeyFrames,
|
||||
AudioSampleRate = maxAudioSampleRate,
|
||||
MaxAudioChannels = maxAudioChannels,
|
||||
AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
|
||||
AudioBitRate = isStatic ? (int?)null : (audioBitRate ?? maxStreamingBitrate),
|
||||
MaxAudioBitDepth = maxAudioBitDepth,
|
||||
AudioChannels = maxAudioChannels,
|
||||
CopyTimestamps = true,
|
||||
|
@ -270,20 +270,24 @@ namespace Jellyfin.Api.Controllers
|
|||
{
|
||||
var deviceProfile = new DeviceProfile();
|
||||
|
||||
var directPlayProfiles = new List<DirectPlayProfile>();
|
||||
|
||||
var containers = RequestHelpers.Split(container, ',', true);
|
||||
|
||||
foreach (var cont in containers)
|
||||
int len = containers.Length;
|
||||
var directPlayProfiles = new DirectPlayProfile[len];
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
var parts = RequestHelpers.Split(cont, ',', true);
|
||||
var parts = RequestHelpers.Split(containers[i], '|', true);
|
||||
|
||||
var audioCodecs = parts.Length == 1 ? null : string.Join(",", parts.Skip(1).ToArray());
|
||||
var audioCodecs = parts.Length == 1 ? null : string.Join(',', parts.Skip(1));
|
||||
|
||||
directPlayProfiles.Add(new DirectPlayProfile { Type = DlnaProfileType.Audio, Container = parts[0], AudioCodec = audioCodecs });
|
||||
directPlayProfiles[i] = new DirectPlayProfile
|
||||
{
|
||||
Type = DlnaProfileType.Audio,
|
||||
Container = parts[0],
|
||||
AudioCodec = audioCodecs
|
||||
};
|
||||
}
|
||||
|
||||
deviceProfile.DirectPlayProfiles = directPlayProfiles.ToArray();
|
||||
deviceProfile.DirectPlayProfiles = directPlayProfiles;
|
||||
|
||||
deviceProfile.TranscodingProfiles = new[]
|
||||
{
|
||||
|
|
|
@ -530,6 +530,33 @@ namespace Jellyfin.Api.Controllers
|
|||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user based on auth token.
|
||||
/// </summary>
|
||||
/// <response code="200">User returned.</response>
|
||||
/// <response code="400">Token is not owned by a user.</response>
|
||||
/// <returns>A <see cref="UserDto"/> for the authenticated user.</returns>
|
||||
[HttpGet("Me")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public ActionResult<UserDto> GetCurrentUser()
|
||||
{
|
||||
var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
|
||||
if (userId == null)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var user = _userManager.GetUserById(userId.Value);
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
return _userManager.GetUserDto(user);
|
||||
}
|
||||
|
||||
private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
|
||||
{
|
||||
var users = _userManager.Users;
|
||||
|
|
|
@ -63,6 +63,19 @@ namespace Jellyfin.Api.Helpers
|
|||
public static string? GetToken(in ClaimsPrincipal user)
|
||||
=> GetClaimValue(user, InternalClaimTypes.Token);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a flag specifying whether the request is using an api key.
|
||||
/// </summary>
|
||||
/// <param name="user">Current claims principal.</param>
|
||||
/// <returns>The flag specifying whether the request is using an api key.</returns>
|
||||
public static bool GetIsApiKey(in ClaimsPrincipal user)
|
||||
{
|
||||
var claimValue = GetClaimValue(user, InternalClaimTypes.IsApiKey);
|
||||
return !string.IsNullOrEmpty(claimValue)
|
||||
&& bool.TryParse(claimValue, out var parsedClaimValue)
|
||||
&& parsedClaimValue;
|
||||
}
|
||||
|
||||
private static string? GetClaimValue(in ClaimsPrincipal user, string name)
|
||||
{
|
||||
return user?.Identities
|
||||
|
|
|
@ -123,9 +123,8 @@ namespace Jellyfin.Api.Helpers
|
|||
state.Dispose();
|
||||
}
|
||||
|
||||
await new ProgressiveFileCopier(outputPath, job, transcodingJobHelper, CancellationToken.None)
|
||||
.WriteToAsync(httpContext.Response.Body, CancellationToken.None).ConfigureAwait(false);
|
||||
return new FileStreamResult(httpContext.Response.Body, contentType);
|
||||
var stream = new ProgressiveFileStream(outputPath, job, transcodingJobHelper);
|
||||
return new FileStreamResult(stream, contentType);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
@ -166,7 +166,7 @@ namespace Jellyfin.Api.Helpers
|
|||
MediaSourceInfo mediaSource,
|
||||
DeviceProfile profile,
|
||||
AuthorizationInfo auth,
|
||||
long? maxBitrate,
|
||||
int? maxBitrate,
|
||||
long startTimeTicks,
|
||||
string mediaSourceId,
|
||||
int? audioStreamIndex,
|
||||
|
@ -551,7 +551,7 @@ namespace Jellyfin.Api.Helpers
|
|||
}
|
||||
}
|
||||
|
||||
private long? GetMaxBitrate(long? clientMaxBitrate, User user, string ipAddress)
|
||||
private int? GetMaxBitrate(int? clientMaxBitrate, User user, string ipAddress)
|
||||
{
|
||||
var maxBitrate = clientMaxBitrate;
|
||||
var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0;
|
||||
|
|
166
Jellyfin.Api/Helpers/ProgressiveFileStream.cs
Normal file
166
Jellyfin.Api/Helpers/ProgressiveFileStream.cs
Normal file
|
@ -0,0 +1,166 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Models.PlaybackDtos;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace Jellyfin.Api.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// A progressive file stream for transferring transcoded files as they are written to.
|
||||
/// </summary>
|
||||
public class ProgressiveFileStream : Stream
|
||||
{
|
||||
private readonly FileStream _fileStream;
|
||||
private readonly TranscodingJobDto? _job;
|
||||
private readonly TranscodingJobHelper _transcodingJobHelper;
|
||||
private readonly bool _allowAsyncFileRead;
|
||||
private int _bytesWritten;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ProgressiveFileStream"/> class.
|
||||
/// </summary>
|
||||
/// <param name="filePath">The path to the transcoded file.</param>
|
||||
/// <param name="job">The transcoding job information.</param>
|
||||
/// <param name="transcodingJobHelper">The transcoding job helper.</param>
|
||||
public ProgressiveFileStream(string filePath, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper)
|
||||
{
|
||||
_job = job;
|
||||
_transcodingJobHelper = transcodingJobHelper;
|
||||
_bytesWritten = 0;
|
||||
|
||||
var fileOptions = FileOptions.SequentialScan;
|
||||
_allowAsyncFileRead = false;
|
||||
|
||||
// use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
fileOptions |= FileOptions.Asynchronous;
|
||||
_allowAsyncFileRead = true;
|
||||
}
|
||||
|
||||
_fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanRead => _fileStream.CanRead;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanSeek => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanWrite => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override long Length => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override long Position
|
||||
{
|
||||
get => throw new NotSupportedException();
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Flush()
|
||||
{
|
||||
_fileStream.Flush();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
return _fileStream.Read(buffer, offset, count);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
int totalBytesRead = 0;
|
||||
int remainingBytesToRead = count;
|
||||
|
||||
int newOffset = offset;
|
||||
while (remainingBytesToRead > 0)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
int bytesRead;
|
||||
if (_allowAsyncFileRead)
|
||||
{
|
||||
bytesRead = await _fileStream.ReadAsync(buffer, newOffset, remainingBytesToRead, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
bytesRead = _fileStream.Read(buffer, newOffset, remainingBytesToRead);
|
||||
}
|
||||
|
||||
remainingBytesToRead -= bytesRead;
|
||||
newOffset += bytesRead;
|
||||
|
||||
if (bytesRead > 0)
|
||||
{
|
||||
_bytesWritten += bytesRead;
|
||||
totalBytesRead += bytesRead;
|
||||
|
||||
if (_job != null)
|
||||
{
|
||||
_job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the job is null it's a live stream and will require user action to close
|
||||
if (_job?.HasExited ?? false)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await Task.Delay(50, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return totalBytesRead;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetLength(long value)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_fileStream.Dispose();
|
||||
|
||||
if (_job != null)
|
||||
{
|
||||
_transcodingJobHelper.OnTranscodeEndRequest(_job);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_disposed = true;
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,14 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
|
@ -159,5 +164,67 @@ namespace Jellyfin.Api.Helpers
|
|||
.Select(i => i!.Value)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item fields.
|
||||
/// </summary>
|
||||
/// <param name="imageTypes">The image types string.</param>
|
||||
/// <returns>IEnumerable{ItemFields}.</returns>
|
||||
internal static ImageType[] GetImageTypes(string? imageTypes)
|
||||
{
|
||||
if (string.IsNullOrEmpty(imageTypes))
|
||||
{
|
||||
return Array.Empty<ImageType>();
|
||||
}
|
||||
|
||||
return Split(imageTypes, ',', true)
|
||||
.Select(v =>
|
||||
{
|
||||
if (Enum.TryParse(v, true, out ImageType value))
|
||||
{
|
||||
return (ImageType?)value;
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.Where(i => i.HasValue)
|
||||
.Select(i => i!.Value)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
internal static QueryResult<BaseItemDto> CreateQueryResult(
|
||||
QueryResult<(BaseItem, ItemCounts)> result,
|
||||
DtoOptions dtoOptions,
|
||||
IDtoService dtoService,
|
||||
bool includeItemTypes,
|
||||
User? user)
|
||||
{
|
||||
var dtos = result.Items.Select(i =>
|
||||
{
|
||||
var (baseItem, counts) = i;
|
||||
var dto = dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
|
||||
|
||||
if (includeItemTypes)
|
||||
{
|
||||
dto.ChildCount = counts.ItemCount;
|
||||
dto.ProgramCount = counts.ProgramCount;
|
||||
dto.SeriesCount = counts.SeriesCount;
|
||||
dto.EpisodeCount = counts.EpisodeCount;
|
||||
dto.MovieCount = counts.MovieCount;
|
||||
dto.TrailerCount = counts.TrailerCount;
|
||||
dto.AlbumCount = counts.AlbumCount;
|
||||
dto.SongCount = counts.SongCount;
|
||||
dto.ArtistCount = counts.ArtistCount;
|
||||
}
|
||||
|
||||
return dto;
|
||||
});
|
||||
|
||||
return new QueryResult<BaseItemDto>
|
||||
{
|
||||
Items = dtos.ToArray(),
|
||||
TotalRecordCount = result.TotalRecordCount
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,9 +50,9 @@ namespace Jellyfin.Api.Helpers
|
|||
|
||||
var returnItems = items;
|
||||
|
||||
if (limit.HasValue)
|
||||
if (limit.HasValue && limit < returnItems.Count)
|
||||
{
|
||||
returnItems = returnItems.Take(limit.Value).ToList();
|
||||
returnItems = returnItems.GetRange(0, limit.Value);
|
||||
}
|
||||
|
||||
var dtos = dtoService.GetBaseItemDtos(returnItems, dtoOptions, user);
|
||||
|
|
|
@ -15,7 +15,7 @@ namespace Jellyfin.Api.ModelBinders
|
|||
public Task BindModelAsync(ModelBindingContext bindingContext)
|
||||
{
|
||||
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
|
||||
var elementType = bindingContext.ModelType.GetElementType();
|
||||
var elementType = bindingContext.ModelType.GetElementType() ?? bindingContext.ModelType.GenericTypeArguments[0];
|
||||
var converter = TypeDescriptor.GetConverter(elementType);
|
||||
|
||||
if (valueProviderResult.Length > 1)
|
||||
|
|
34
Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs
Normal file
34
Jellyfin.Api/Models/SubtitleDtos/UploadSubtitleDto.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Jellyfin.Api.Models.SubtitleDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Upload subtitles dto.
|
||||
/// </summary>
|
||||
public class UploadSubtitleDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the subtitle language.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Language { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the subtitle format.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Format { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the subtitle is forced.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public bool IsForced { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the subtitle data.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Data { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using Emby.Server.Implementations;
|
||||
using Jellyfin.Api.Auth;
|
||||
using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
|
||||
using Jellyfin.Api.Auth.DownloadPolicy;
|
||||
|
@ -27,6 +28,8 @@ using Microsoft.AspNetCore.Builder;
|
|||
using Microsoft.AspNetCore.Cors.Infrastructure;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Interfaces;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes;
|
||||
|
@ -209,7 +212,19 @@ namespace Jellyfin.Server.Extensions
|
|||
{
|
||||
return serviceCollection.AddSwaggerGen(c =>
|
||||
{
|
||||
c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API", Version = "v1" });
|
||||
c.SwaggerDoc("api-docs", new OpenApiInfo
|
||||
{
|
||||
Title = "Jellyfin API",
|
||||
Version = "v1",
|
||||
Extensions = new Dictionary<string, IOpenApiExtension>
|
||||
{
|
||||
{
|
||||
"x-jellyfin-version",
|
||||
new OpenApiString(typeof(ApplicationHost).Assembly.GetName().Version?.ToString())
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
c.AddSecurityDefinition(AuthenticationSchemes.CustomAuthentication, new OpenApiSecurityScheme
|
||||
{
|
||||
Type = SecuritySchemeType.ApiKey,
|
||||
|
|
|
@ -21,8 +21,8 @@ namespace MediaBrowser.Common.Json.Converters
|
|||
/// <inheritdoc />
|
||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var structType = typeToConvert.GetElementType();
|
||||
var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0];
|
||||
return (JsonConverter)Activator.CreateInstance(typeof(JsonCommaDelimitedArrayConverter<>).MakeGenericType(structType));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
113
MediaBrowser.Common/Plugins/LocalPlugin.cs
Normal file
113
MediaBrowser.Common/Plugins/LocalPlugin.cs
Normal file
|
@ -0,0 +1,113 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MediaBrowser.Common.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// Local plugin struct.
|
||||
/// </summary>
|
||||
public class LocalPlugin : IEquatable<LocalPlugin>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LocalPlugin"/> class.
|
||||
/// </summary>
|
||||
/// <param name="id">The plugin id.</param>
|
||||
/// <param name="name">The plugin name.</param>
|
||||
/// <param name="version">The plugin version.</param>
|
||||
/// <param name="path">The plugin path.</param>
|
||||
public LocalPlugin(Guid id, string name, Version version, string path)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
Version = version;
|
||||
Path = path;
|
||||
DllFiles = new List<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin id.
|
||||
/// </summary>
|
||||
public Guid Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin name.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin version.
|
||||
/// </summary>
|
||||
public Version Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin path.
|
||||
/// </summary>
|
||||
public string Path { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of dll files for this plugin.
|
||||
/// </summary>
|
||||
public List<string> DllFiles { get; }
|
||||
|
||||
/// <summary>
|
||||
/// == operator.
|
||||
/// </summary>
|
||||
/// <param name="left">Left item.</param>
|
||||
/// <param name="right">Right item.</param>
|
||||
/// <returns>Comparison result.</returns>
|
||||
public static bool operator ==(LocalPlugin left, LocalPlugin right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// != operator.
|
||||
/// </summary>
|
||||
/// <param name="left">Left item.</param>
|
||||
/// <param name="right">Right item.</param>
|
||||
/// <returns>Comparison result.</returns>
|
||||
public static bool operator !=(LocalPlugin left, LocalPlugin right)
|
||||
{
|
||||
return !left.Equals(right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compare two <see cref="LocalPlugin"/>.
|
||||
/// </summary>
|
||||
/// <param name="a">The first item.</param>
|
||||
/// <param name="b">The second item.</param>
|
||||
/// <returns>Comparison result.</returns>
|
||||
public static int Compare(LocalPlugin a, LocalPlugin b)
|
||||
{
|
||||
var compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture);
|
||||
|
||||
// Id is not equal but name is.
|
||||
if (a.Id != b.Id && compare == 0)
|
||||
{
|
||||
compare = a.Id.CompareTo(b.Id);
|
||||
}
|
||||
|
||||
return compare == 0 ? a.Version.CompareTo(b.Version) : compare;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is LocalPlugin other && this.Equals(other);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Name.GetHashCode(StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(LocalPlugin other)
|
||||
{
|
||||
return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase)
|
||||
&& Id.Equals(other.Id);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using Jellyfin.Data.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
|
@ -23,6 +24,10 @@ namespace MediaBrowser.Controller.Entities
|
|||
|
||||
public string NameContains { get; set; }
|
||||
|
||||
public User User { get; set; }
|
||||
|
||||
public bool? IsFavorite { get; set; }
|
||||
|
||||
public InternalPeopleQuery()
|
||||
{
|
||||
PersonTypes = Array.Empty<string>();
|
||||
|
|
|
@ -12,6 +12,9 @@ namespace MediaBrowser.Controller
|
|||
/// <summary>
|
||||
/// Gets the display preferences for the user and client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will create the display preferences if it does not exist, but it will not save automatically.
|
||||
/// </remarks>
|
||||
/// <param name="userId">The user's id.</param>
|
||||
/// <param name="client">The client string.</param>
|
||||
/// <returns>The associated display preferences.</returns>
|
||||
|
@ -20,6 +23,9 @@ namespace MediaBrowser.Controller
|
|||
/// <summary>
|
||||
/// Gets the default item display preferences for the user and client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will create the item display preferences if it does not exist, but it will not save automatically.
|
||||
/// </remarks>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="client">The client string.</param>
|
||||
|
|
|
@ -6,8 +6,8 @@ using System.Net;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Model.System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace MediaBrowser.Controller
|
||||
{
|
||||
|
@ -56,10 +56,11 @@ namespace MediaBrowser.Controller
|
|||
/// <summary>
|
||||
/// Gets the system info.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param>
|
||||
/// <returns>SystemInfo.</returns>
|
||||
Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken);
|
||||
Task<SystemInfo> GetSystemInfo(CancellationToken cancellationToken = default);
|
||||
|
||||
Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken);
|
||||
Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all the local IP addresses of this API instance. Each address is validated by sending a 'ping' request
|
||||
|
@ -67,7 +68,7 @@ namespace MediaBrowser.Controller
|
|||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param>
|
||||
/// <returns>A list containing all the local IP addresses of the server.</returns>
|
||||
Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken);
|
||||
Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a local (LAN) URL that can be used to access the API. The hostname used is the first valid configured
|
||||
|
@ -75,7 +76,7 @@ namespace MediaBrowser.Controller
|
|||
/// </summary>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the task.</param>
|
||||
/// <returns>The server URL.</returns>
|
||||
Task<string> GetLocalApiUrl(CancellationToken cancellationToken);
|
||||
Task<string> GetLocalApiUrl(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a localhost URL that can be used to access the API using the loop-back IP address (127.0.0.1)
|
||||
|
@ -119,5 +120,13 @@ namespace MediaBrowser.Controller
|
|||
string ExpandVirtualPath(string path);
|
||||
|
||||
string ReverseVirtualPath(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of local plugins.
|
||||
/// </summary>
|
||||
/// <param name="path">Plugin base directory.</param>
|
||||
/// <param name="cleanup">Cleanup old plugins.</param>
|
||||
/// <returns>Enumerable of local plugins.</returns>
|
||||
IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -570,5 +570,7 @@ namespace MediaBrowser.Controller.Library
|
|||
List<MediaStream> streams,
|
||||
string videoPath,
|
||||
string[] files);
|
||||
|
||||
BaseItem GetParentItem(string parentId, Guid? userId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -115,5 +115,7 @@ namespace MediaBrowser.Controller.Library
|
|||
public interface IDirectStreamProvider
|
||||
{
|
||||
Task CopyToAsync(Stream stream, CancellationToken cancellationToken);
|
||||
|
||||
string GetFilePath();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2675,9 +2675,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
state.MediaSource = mediaSource;
|
||||
|
||||
var request = state.BaseRequest;
|
||||
if (!string.IsNullOrWhiteSpace(request.AudioCodec))
|
||||
var supportedAudioCodecs = state.SupportedAudioCodecs;
|
||||
if (request != null && supportedAudioCodecs != null && supportedAudioCodecs.Length > 0)
|
||||
{
|
||||
var supportedAudioCodecsList = request.AudioCodec.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||
var supportedAudioCodecsList = supportedAudioCodecs.ToList();
|
||||
|
||||
ShiftAudioCodecsIfNeeded(supportedAudioCodecsList, state.AudioStream);
|
||||
|
||||
|
@ -3084,7 +3085,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
}
|
||||
}
|
||||
|
||||
var whichCodec = videoStream.Codec.ToLowerInvariant();
|
||||
var whichCodec = videoStream.Codec?.ToLowerInvariant();
|
||||
switch (whichCodec)
|
||||
{
|
||||
case "avc":
|
||||
|
|
|
@ -287,6 +287,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
return BaseRequest.AudioChannels;
|
||||
}
|
||||
|
||||
if (BaseRequest.TranscodingMaxAudioChannels.HasValue)
|
||||
{
|
||||
return BaseRequest.TranscodingMaxAudioChannels;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(codec))
|
||||
{
|
||||
var value = BaseRequest.GetOption(codec, "audiochannels");
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using Jellyfin.Data.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// The request authorization info.
|
||||
/// </summary>
|
||||
public class AuthorizationInfo
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -43,6 +44,19 @@ namespace MediaBrowser.Controller.Net
|
|||
/// <value>The token.</value>
|
||||
public string Token { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the authorization is from an api key.
|
||||
/// </summary>
|
||||
public bool IsApiKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user making the request.
|
||||
/// </summary>
|
||||
public User User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the token is authenticated.
|
||||
/// </summary>
|
||||
public bool IsAuthenticated { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
@ -52,6 +53,14 @@ namespace MediaBrowser.Controller.Subtitles
|
|||
/// </summary>
|
||||
Task DownloadSubtitles(Video video, LibraryOptions libraryOptions, string subtitleId, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Upload new subtitle.
|
||||
/// </summary>
|
||||
/// <param name="video">The video the subtitle belongs to.</param>
|
||||
/// <param name="response">The subtitle response.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
Task UploadSubtitle(Video video, SubtitleResponse response);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the remote subtitles.
|
||||
/// </summary>
|
||||
|
|
|
@ -9,6 +9,10 @@ namespace MediaBrowser.Model.Configuration
|
|||
|
||||
public string TranscodingTempPath { get; set; }
|
||||
|
||||
public string FallbackFontPath { get; set; }
|
||||
|
||||
public bool EnableFallbackFont { get; set; }
|
||||
|
||||
public double DownMixAudioBoost { get; set; }
|
||||
|
||||
public int MaxMuxingQueueSize { get; set; }
|
||||
|
@ -69,6 +73,7 @@ namespace MediaBrowser.Model.Configuration
|
|||
|
||||
public EncodingOptions()
|
||||
{
|
||||
EnableFallbackFont = false;
|
||||
DownMixAudioBoost = 2;
|
||||
MaxMuxingQueueSize = 2048;
|
||||
EnableThrottling = false;
|
||||
|
|
|
@ -49,7 +49,7 @@ namespace MediaBrowser.Model.Dlna
|
|||
/// <summary>
|
||||
/// The application's configured quality setting.
|
||||
/// </summary>
|
||||
public long? MaxBitrate { get; set; }
|
||||
public int? MaxBitrate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the context.
|
||||
|
@ -67,7 +67,7 @@ namespace MediaBrowser.Model.Dlna
|
|||
/// Gets the maximum bitrate.
|
||||
/// </summary>
|
||||
/// <returns>System.Nullable<System.Int32>.</returns>
|
||||
public long? GetMaxBitrate(bool isAudio)
|
||||
public int? GetMaxBitrate(bool isAudio)
|
||||
{
|
||||
if (MaxBitrate.HasValue)
|
||||
{
|
||||
|
|
|
@ -62,9 +62,9 @@ namespace MediaBrowser.Model.Dlna
|
|||
|
||||
public int? MaxIconHeight { get; set; }
|
||||
|
||||
public long? MaxStreamingBitrate { get; set; }
|
||||
public int? MaxStreamingBitrate { get; set; }
|
||||
|
||||
public long? MaxStaticBitrate { get; set; }
|
||||
public int? MaxStaticBitrate { get; set; }
|
||||
|
||||
public int? MusicStreamingTranscodingBitrate { get; set; }
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ namespace MediaBrowser.Model.MediaInfo
|
|||
|
||||
public string PlaySessionId { get; set; }
|
||||
|
||||
public long? MaxStreamingBitrate { get; set; }
|
||||
public int? MaxStreamingBitrate { get; set; }
|
||||
|
||||
public long? StartTimeTicks { get; set; }
|
||||
|
||||
|
|
34
MediaBrowser.Model/Subtitles/FontFile.cs
Normal file
34
MediaBrowser.Model/Subtitles/FontFile.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
using System;
|
||||
|
||||
namespace MediaBrowser.Model.Subtitles
|
||||
{
|
||||
/// <summary>
|
||||
/// Class FontFile.
|
||||
/// </summary>
|
||||
public class FontFile
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the size.
|
||||
/// </summary>
|
||||
/// <value>The size.</value>
|
||||
public long Size { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date created.
|
||||
/// </summary>
|
||||
/// <value>The date created.</value>
|
||||
public DateTime DateCreated { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date modified.
|
||||
/// </summary>
|
||||
/// <value>The date modified.</value>
|
||||
public DateTime DateModified { get; set; }
|
||||
}
|
||||
}
|
|
@ -150,37 +150,11 @@ namespace MediaBrowser.Providers.Subtitles
|
|||
var parts = subtitleId.Split(new[] { '_' }, 2);
|
||||
var provider = GetProvider(parts[0]);
|
||||
|
||||
var saveInMediaFolder = libraryOptions.SaveSubtitlesWithMedia;
|
||||
|
||||
try
|
||||
{
|
||||
var response = await GetRemoteSubtitles(subtitleId, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using (var stream = response.Stream)
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
|
||||
memoryStream.Position = 0;
|
||||
|
||||
var savePaths = new List<string>();
|
||||
var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant();
|
||||
|
||||
if (response.IsForced)
|
||||
{
|
||||
saveFileName += ".forced";
|
||||
}
|
||||
|
||||
saveFileName += "." + response.Format.ToLowerInvariant();
|
||||
|
||||
if (saveInMediaFolder)
|
||||
{
|
||||
savePaths.Add(Path.Combine(video.ContainingFolderPath, saveFileName));
|
||||
}
|
||||
|
||||
savePaths.Add(Path.Combine(video.GetInternalMetadataPath(), saveFileName));
|
||||
|
||||
await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false);
|
||||
}
|
||||
await TrySaveSubtitle(video, libraryOptions, response).ConfigureAwait(false);
|
||||
}
|
||||
catch (RateLimitExceededException)
|
||||
{
|
||||
|
@ -199,6 +173,47 @@ namespace MediaBrowser.Providers.Subtitles
|
|||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UploadSubtitle(Video video, SubtitleResponse response)
|
||||
{
|
||||
var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(video);
|
||||
return TrySaveSubtitle(video, libraryOptions, response);
|
||||
}
|
||||
|
||||
private async Task TrySaveSubtitle(
|
||||
Video video,
|
||||
LibraryOptions libraryOptions,
|
||||
SubtitleResponse response)
|
||||
{
|
||||
var saveInMediaFolder = libraryOptions.SaveSubtitlesWithMedia;
|
||||
|
||||
using (var stream = response.Stream)
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
|
||||
memoryStream.Position = 0;
|
||||
|
||||
var savePaths = new List<string>();
|
||||
var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant();
|
||||
|
||||
if (response.IsForced)
|
||||
{
|
||||
saveFileName += ".forced";
|
||||
}
|
||||
|
||||
saveFileName += "." + response.Format.ToLowerInvariant();
|
||||
|
||||
if (saveInMediaFolder)
|
||||
{
|
||||
savePaths.Add(Path.Combine(video.ContainingFolderPath, saveFileName));
|
||||
}
|
||||
|
||||
savePaths.Add(Path.Combine(video.GetInternalMetadataPath(), saveFileName));
|
||||
|
||||
await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task TrySaveToFiles(Stream stream, List<string> savePaths)
|
||||
{
|
||||
Exception exceptionToThrow = null;
|
||||
|
|
|
@ -8,6 +8,7 @@ using Jellyfin.Api.Auth;
|
|||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
@ -68,14 +69,14 @@ namespace Jellyfin.Api.Tests.Auth
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAuthenticateAsyncShouldFailOnSecurityException()
|
||||
public async Task HandleAuthenticateAsyncShouldFailOnAuthenticationException()
|
||||
{
|
||||
var errorMessage = _fixture.Create<string>();
|
||||
|
||||
_jellyfinAuthServiceMock.Setup(
|
||||
a => a.Authenticate(
|
||||
It.IsAny<HttpRequest>()))
|
||||
.Throws(new SecurityException(errorMessage));
|
||||
.Throws(new AuthenticationException(errorMessage));
|
||||
|
||||
var authenticateResult = await _sut.AuthenticateAsync();
|
||||
|
||||
|
@ -128,6 +129,7 @@ namespace Jellyfin.Api.Tests.Auth
|
|||
var authorizationInfo = _fixture.Create<AuthorizationInfo>();
|
||||
authorizationInfo.User = _fixture.Create<User>();
|
||||
authorizationInfo.User.SetPermission(PermissionKind.IsAdministrator, isAdmin);
|
||||
authorizationInfo.IsApiKey = false;
|
||||
|
||||
_jellyfinAuthServiceMock.Setup(
|
||||
a => a.Authenticate(
|
||||
|
|
|
@ -45,7 +45,7 @@ namespace Jellyfin.Api.Tests
|
|||
{
|
||||
new Claim(ClaimTypes.Role, role),
|
||||
new Claim(ClaimTypes.Name, "jellyfin"),
|
||||
new Claim(InternalClaimTypes.UserId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
|
||||
new Claim(InternalClaimTypes.UserId, Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)),
|
||||
new Claim(InternalClaimTypes.DeviceId, Guid.Empty.ToString("N", CultureInfo.InvariantCulture)),
|
||||
new Claim(InternalClaimTypes.Device, "test"),
|
||||
new Claim(InternalClaimTypes.Client, "test"),
|
||||
|
|
|
@ -11,82 +11,82 @@ namespace Jellyfin.Common.Tests.Json
|
|||
[Fact]
|
||||
public static void Deserialize_String_Valid_Success()
|
||||
{
|
||||
var desiredValue = new GenericBodyModel<string>
|
||||
var desiredValue = new GenericBodyArrayModel<string>
|
||||
{
|
||||
Value = new[] { "a", "b", "c" }
|
||||
};
|
||||
|
||||
var options = new JsonSerializerOptions();
|
||||
var value = JsonSerializer.Deserialize<GenericBodyModel<string>>(@"{ ""Value"": ""a,b,c"" }", options);
|
||||
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": ""a,b,c"" }", options);
|
||||
Assert.Equal(desiredValue.Value, value?.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public static void Deserialize_String_Space_Valid_Success()
|
||||
{
|
||||
var desiredValue = new GenericBodyModel<string>
|
||||
var desiredValue = new GenericBodyArrayModel<string>
|
||||
{
|
||||
Value = new[] { "a", "b", "c" }
|
||||
};
|
||||
|
||||
var options = new JsonSerializerOptions();
|
||||
var value = JsonSerializer.Deserialize<GenericBodyModel<string>>(@"{ ""Value"": ""a, b, c"" }", options);
|
||||
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": ""a, b, c"" }", options);
|
||||
Assert.Equal(desiredValue.Value, value?.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public static void Deserialize_GenericCommandType_Valid_Success()
|
||||
{
|
||||
var desiredValue = new GenericBodyModel<GeneralCommandType>
|
||||
var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
|
||||
{
|
||||
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
|
||||
};
|
||||
|
||||
var options = new JsonSerializerOptions();
|
||||
options.Converters.Add(new JsonStringEnumConverter());
|
||||
var value = JsonSerializer.Deserialize<GenericBodyModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,MoveDown"" }", options);
|
||||
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,MoveDown"" }", options);
|
||||
Assert.Equal(desiredValue.Value, value?.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public static void Deserialize_GenericCommandType_Space_Valid_Success()
|
||||
{
|
||||
var desiredValue = new GenericBodyModel<GeneralCommandType>
|
||||
var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
|
||||
{
|
||||
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
|
||||
};
|
||||
|
||||
var options = new JsonSerializerOptions();
|
||||
options.Converters.Add(new JsonStringEnumConverter());
|
||||
var value = JsonSerializer.Deserialize<GenericBodyModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp, MoveDown"" }", options);
|
||||
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp, MoveDown"" }", options);
|
||||
Assert.Equal(desiredValue.Value, value?.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public static void Deserialize_String_Array_Valid_Success()
|
||||
{
|
||||
var desiredValue = new GenericBodyModel<string>
|
||||
var desiredValue = new GenericBodyArrayModel<string>
|
||||
{
|
||||
Value = new[] { "a", "b", "c" }
|
||||
};
|
||||
|
||||
var options = new JsonSerializerOptions();
|
||||
var value = JsonSerializer.Deserialize<GenericBodyModel<string>>(@"{ ""Value"": [""a"",""b"",""c""] }", options);
|
||||
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<string>>(@"{ ""Value"": [""a"",""b"",""c""] }", options);
|
||||
Assert.Equal(desiredValue.Value, value?.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public static void Deserialize_GenericCommandType_Array_Valid_Success()
|
||||
{
|
||||
var desiredValue = new GenericBodyModel<GeneralCommandType>
|
||||
var desiredValue = new GenericBodyArrayModel<GeneralCommandType>
|
||||
{
|
||||
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
|
||||
};
|
||||
|
||||
var options = new JsonSerializerOptions();
|
||||
options.Converters.Add(new JsonStringEnumConverter());
|
||||
var value = JsonSerializer.Deserialize<GenericBodyModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", options);
|
||||
var value = JsonSerializer.Deserialize<GenericBodyArrayModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", options);
|
||||
Assert.Equal(desiredValue.Value, value?.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Jellyfin.Common.Tests.Models;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Common.Tests.Json
|
||||
{
|
||||
public static class JsonCommaDelimitedIReadOnlyListTests
|
||||
{
|
||||
[Fact]
|
||||
public static void Deserialize_String_Valid_Success()
|
||||
{
|
||||
var desiredValue = new GenericBodyIReadOnlyListModel<string>
|
||||
{
|
||||
Value = new[] { "a", "b", "c" }
|
||||
};
|
||||
|
||||
var options = new JsonSerializerOptions();
|
||||
var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": ""a,b,c"" }", options);
|
||||
Assert.Equal(desiredValue.Value, value?.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public static void Deserialize_String_Space_Valid_Success()
|
||||
{
|
||||
var desiredValue = new GenericBodyIReadOnlyListModel<string>
|
||||
{
|
||||
Value = new[] { "a", "b", "c" }
|
||||
};
|
||||
|
||||
var options = new JsonSerializerOptions();
|
||||
var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": ""a, b, c"" }", options);
|
||||
Assert.Equal(desiredValue.Value, value?.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public static void Deserialize_GenericCommandType_Valid_Success()
|
||||
{
|
||||
var desiredValue = new GenericBodyIReadOnlyListModel<GeneralCommandType>
|
||||
{
|
||||
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
|
||||
};
|
||||
|
||||
var options = new JsonSerializerOptions();
|
||||
options.Converters.Add(new JsonStringEnumConverter());
|
||||
var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp,MoveDown"" }", options);
|
||||
Assert.Equal(desiredValue.Value, value?.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public static void Deserialize_GenericCommandType_Space_Valid_Success()
|
||||
{
|
||||
var desiredValue = new GenericBodyIReadOnlyListModel<GeneralCommandType>
|
||||
{
|
||||
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
|
||||
};
|
||||
|
||||
var options = new JsonSerializerOptions();
|
||||
options.Converters.Add(new JsonStringEnumConverter());
|
||||
var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": ""MoveUp, MoveDown"" }", options);
|
||||
Assert.Equal(desiredValue.Value, value?.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public static void Deserialize_String_Array_Valid_Success()
|
||||
{
|
||||
var desiredValue = new GenericBodyIReadOnlyListModel<string>
|
||||
{
|
||||
Value = new[] { "a", "b", "c" }
|
||||
};
|
||||
|
||||
var options = new JsonSerializerOptions();
|
||||
var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<string>>(@"{ ""Value"": [""a"",""b"",""c""] }", options);
|
||||
Assert.Equal(desiredValue.Value, value?.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public static void Deserialize_GenericCommandType_Array_Valid_Success()
|
||||
{
|
||||
var desiredValue = new GenericBodyIReadOnlyListModel<GeneralCommandType>
|
||||
{
|
||||
Value = new[] { GeneralCommandType.MoveUp, GeneralCommandType.MoveDown }
|
||||
};
|
||||
|
||||
var options = new JsonSerializerOptions();
|
||||
options.Converters.Add(new JsonStringEnumConverter());
|
||||
var value = JsonSerializer.Deserialize<GenericBodyIReadOnlyListModel<GeneralCommandType>>(@"{ ""Value"": [""MoveUp"", ""MoveDown""] }", options);
|
||||
Assert.Equal(desiredValue.Value, value?.Value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ namespace Jellyfin.Common.Tests.Models
|
|||
/// The generic body model.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The value type.</typeparam>
|
||||
public class GenericBodyModel<T>
|
||||
public class GenericBodyArrayModel<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the value.
|
||||
|
@ -17,4 +17,4 @@ namespace Jellyfin.Common.Tests.Models
|
|||
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
|
||||
public T[] Value { get; set; } = default!;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using MediaBrowser.Common.Json.Converters;
|
||||
|
||||
namespace Jellyfin.Common.Tests.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// The generic body <c>IReadOnlyList</c> model.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The value type.</typeparam>
|
||||
public class GenericBodyIReadOnlyListModel<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the value.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
|
||||
public IReadOnlyList<T> Value { get; set; } = default!;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user