Merge branch 'master' into Comment1

This commit is contained in:
BaronGreenback 2020-11-12 09:18:49 +00:00 committed by GitHub
commit 27bb17ef9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
171 changed files with 2279 additions and 2039 deletions

View File

@ -62,6 +62,7 @@ jobs:
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: 'Download Reference Assembly Build Artifact' displayName: 'Download Reference Assembly Build Artifact'
enabled: false
inputs: inputs:
source: "specific" source: "specific"
artifact: "$(NugetPackageName)" artifact: "$(NugetPackageName)"
@ -73,6 +74,7 @@ jobs:
- task: CopyFiles@2 - task: CopyFiles@2
displayName: 'Copy Reference Assembly Build Artifact' displayName: 'Copy Reference Assembly Build Artifact'
enabled: false
inputs: inputs:
sourceFolder: $(System.ArtifactsDirectory)/current-artifacts sourceFolder: $(System.ArtifactsDirectory)/current-artifacts
contents: '**/*.dll' contents: '**/*.dll'
@ -83,6 +85,7 @@ jobs:
- task: DotNetCoreCLI@2 - task: DotNetCoreCLI@2
displayName: 'Execute ABI Compatibility Check Tool' displayName: 'Execute ABI Compatibility Check Tool'
enabled: false
inputs: inputs:
command: custom command: custom
custom: compat custom: compat

View File

@ -28,15 +28,13 @@ jobs:
inputs: inputs:
script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar" script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar"
## Generate npm api client ## Authenticate with npm registry
# Unstable - task: npmAuthenticate@0
- task: CmdLine@2
displayName: 'Build unstable typescript axios client'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs: inputs:
script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory) $(Build.BuildNumber)" workingFile: ./.npmrc
customEndpoint: 'jellyfin-bot for NPM'
# Stable ## Generate npm api client
- task: CmdLine@2 - task: CmdLine@2
displayName: 'Build stable typescript axios client' displayName: 'Build stable typescript axios client'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
@ -51,17 +49,6 @@ jobs:
workingDir: ./apiclient/generated/typescript/axios workingDir: ./apiclient/generated/typescript/axios
## Publish npm packages ## Publish npm packages
# Unstable
- task: Npm@1
displayName: 'Publish unstable typescript axios client'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs:
command: publish
publishRegistry: useFeed
publishFeed: 'jellyfin/unstable'
workingDir: ./apiclient/generated/typescript/axios
# Stable
- task: Npm@1 - task: Npm@1
displayName: 'Publish stable typescript axios client' displayName: 'Publish stable typescript axios client'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')

View File

@ -63,6 +63,7 @@ jobs:
sshEndpoint: repository sshEndpoint: repository
sourceFolder: '$(Build.SourcesDirectory)/deployment/dist' sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
contents: '**' contents: '**'
targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
- job: OpenAPISpec - job: OpenAPISpec
dependsOn: Test dependsOn: Test
@ -166,7 +167,7 @@ jobs:
inputs: inputs:
sshEndpoint: repository sshEndpoint: repository
runOptions: 'commands' 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 - task: SSH@0
displayName: 'Update Stable Repository' displayName: 'Update Stable Repository'
@ -175,7 +176,7 @@ jobs:
inputs: inputs:
sshEndpoint: repository sshEndpoint: repository
runOptions: 'commands' 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 - job: PublishNuget
displayName: 'Publish NuGet packages' displayName: 'Publish NuGet packages'

3
.npmrc Normal file
View File

@ -0,0 +1,3 @@
registry=https://registry.npmjs.org/
@jellyfin:registry=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/
always-auth=true

View File

@ -487,7 +487,7 @@ namespace Emby.Dlna.ContentDirectory
User = user, User = user,
Recursive = true, Recursive = true,
IsMissing = false, IsMissing = false,
ExcludeItemTypes = new[] { typeof(Book).Name }, ExcludeItemTypes = new[] { nameof(Book) },
IsFolder = isFolder, IsFolder = isFolder,
MediaTypes = mediaTypes, MediaTypes = mediaTypes,
DtoOptions = GetDtoOptions() DtoOptions = GetDtoOptions()
@ -556,7 +556,7 @@ namespace Emby.Dlna.ContentDirectory
Limit = limit, Limit = limit,
StartIndex = startIndex, StartIndex = startIndex,
IsVirtualItem = false, IsVirtualItem = false,
ExcludeItemTypes = new[] { typeof(Book).Name }, ExcludeItemTypes = new[] { nameof(Book) },
IsPlaceHolder = false, IsPlaceHolder = false,
DtoOptions = GetDtoOptions() DtoOptions = GetDtoOptions()
}; };
@ -575,7 +575,7 @@ namespace Emby.Dlna.ContentDirectory
StartIndex = startIndex, StartIndex = startIndex,
Limit = limit, Limit = limit,
}; };
query.IncludeItemTypes = new[] { typeof(LiveTvChannel).Name }; query.IncludeItemTypes = new[] { nameof(LiveTvChannel) };
SetSorting(query, sort, false); SetSorting(query, sort, false);
@ -910,7 +910,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent; query.Parent = parent;
query.SetUser(user); query.SetUser(user);
query.IncludeItemTypes = new[] { typeof(Series).Name }; query.IncludeItemTypes = new[] { nameof(Series) };
var result = _libraryManager.GetItemsResult(query); var result = _libraryManager.GetItemsResult(query);
@ -923,7 +923,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent; query.Parent = parent;
query.SetUser(user); query.SetUser(user);
query.IncludeItemTypes = new[] { typeof(Movie).Name }; query.IncludeItemTypes = new[] { nameof(Movie) };
var result = _libraryManager.GetItemsResult(query); var result = _libraryManager.GetItemsResult(query);
@ -936,7 +936,7 @@ namespace Emby.Dlna.ContentDirectory
// query.Parent = parent; // query.Parent = parent;
query.SetUser(user); query.SetUser(user);
query.IncludeItemTypes = new[] { typeof(BoxSet).Name }; query.IncludeItemTypes = new[] { nameof(BoxSet) };
var result = _libraryManager.GetItemsResult(query); var result = _libraryManager.GetItemsResult(query);
@ -949,7 +949,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent; query.Parent = parent;
query.SetUser(user); query.SetUser(user);
query.IncludeItemTypes = new[] { typeof(MusicAlbum).Name }; query.IncludeItemTypes = new[] { nameof(MusicAlbum) };
var result = _libraryManager.GetItemsResult(query); var result = _libraryManager.GetItemsResult(query);
@ -962,7 +962,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent; query.Parent = parent;
query.SetUser(user); query.SetUser(user);
query.IncludeItemTypes = new[] { typeof(Audio).Name }; query.IncludeItemTypes = new[] { nameof(Audio) };
var result = _libraryManager.GetItemsResult(query); var result = _libraryManager.GetItemsResult(query);
@ -975,7 +975,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent; query.Parent = parent;
query.SetUser(user); query.SetUser(user);
query.IsFavorite = true; query.IsFavorite = true;
query.IncludeItemTypes = new[] { typeof(Audio).Name }; query.IncludeItemTypes = new[] { nameof(Audio) };
var result = _libraryManager.GetItemsResult(query); var result = _libraryManager.GetItemsResult(query);
@ -988,7 +988,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent; query.Parent = parent;
query.SetUser(user); query.SetUser(user);
query.IsFavorite = true; query.IsFavorite = true;
query.IncludeItemTypes = new[] { typeof(Series).Name }; query.IncludeItemTypes = new[] { nameof(Series) };
var result = _libraryManager.GetItemsResult(query); var result = _libraryManager.GetItemsResult(query);
@ -1001,7 +1001,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent; query.Parent = parent;
query.SetUser(user); query.SetUser(user);
query.IsFavorite = true; query.IsFavorite = true;
query.IncludeItemTypes = new[] { typeof(Episode).Name }; query.IncludeItemTypes = new[] { nameof(Episode) };
var result = _libraryManager.GetItemsResult(query); var result = _libraryManager.GetItemsResult(query);
@ -1014,7 +1014,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent; query.Parent = parent;
query.SetUser(user); query.SetUser(user);
query.IsFavorite = true; query.IsFavorite = true;
query.IncludeItemTypes = new[] { typeof(Movie).Name }; query.IncludeItemTypes = new[] { nameof(Movie) };
var result = _libraryManager.GetItemsResult(query); var result = _libraryManager.GetItemsResult(query);
@ -1027,7 +1027,7 @@ namespace Emby.Dlna.ContentDirectory
query.Parent = parent; query.Parent = parent;
query.SetUser(user); query.SetUser(user);
query.IsFavorite = true; query.IsFavorite = true;
query.IncludeItemTypes = new[] { typeof(MusicAlbum).Name }; query.IncludeItemTypes = new[] { nameof(MusicAlbum) };
var result = _libraryManager.GetItemsResult(query); var result = _libraryManager.GetItemsResult(query);
@ -1181,7 +1181,7 @@ namespace Emby.Dlna.ContentDirectory
{ {
UserId = user.Id, UserId = user.Id,
Limit = 50, Limit = 50,
IncludeItemTypes = new[] { typeof(Episode).Name }, IncludeItemTypes = new[] { nameof(Episode) },
ParentId = parent == null ? Guid.Empty : parent.Id, ParentId = parent == null ? Guid.Empty : parent.Id,
GroupItems = false GroupItems = false
}, },
@ -1215,7 +1215,7 @@ namespace Emby.Dlna.ContentDirectory
Recursive = true, Recursive = true,
ParentId = parentId, ParentId = parentId,
ArtistIds = new[] { item.Id }, ArtistIds = new[] { item.Id },
IncludeItemTypes = new[] { typeof(MusicAlbum).Name }, IncludeItemTypes = new[] { nameof(MusicAlbum) },
Limit = limit, Limit = limit,
StartIndex = startIndex, StartIndex = startIndex,
DtoOptions = GetDtoOptions() DtoOptions = GetDtoOptions()
@ -1259,7 +1259,7 @@ namespace Emby.Dlna.ContentDirectory
Recursive = true, Recursive = true,
ParentId = parentId, ParentId = parentId,
GenreIds = new[] { item.Id }, GenreIds = new[] { item.Id },
IncludeItemTypes = new[] { typeof(MusicAlbum).Name }, IncludeItemTypes = new[] { nameof(MusicAlbum) },
Limit = limit, Limit = limit,
StartIndex = startIndex, StartIndex = startIndex,
DtoOptions = GetDtoOptions() DtoOptions = GetDtoOptions()
@ -1346,8 +1346,8 @@ namespace Emby.Dlna.ContentDirectory
{ {
if (id.StartsWith(name + "_", StringComparison.OrdinalIgnoreCase)) if (id.StartsWith(name + "_", StringComparison.OrdinalIgnoreCase))
{ {
stubType = (StubType)Enum.Parse(typeof(StubType), name, true); stubType = Enum.Parse<StubType>(name, true);
id = id.Split(new[] { '_' }, 2)[1]; id = id.Split('_', 2)[1];
break; break;
} }

View File

@ -123,7 +123,7 @@ namespace Emby.Dlna.Didl
{ {
foreach (var att in profile.XmlRootAttributes) 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) if (parts.Length == 2)
{ {
writer.WriteAttributeString(parts[0], parts[1], null, att.Value); writer.WriteAttributeString(parts[0], parts[1], null, att.Value);

View File

@ -383,9 +383,9 @@ namespace Emby.Dlna
continue; continue;
} }
var filename = Path.GetFileName(name).Substring(namespaceName.Length); var path = Path.Join(
systemProfilesPath,
var path = Path.Combine(systemProfilesPath, filename); Path.GetFileName(name.AsSpan()).Slice(namespaceName.Length));
using (var stream = _assembly.GetManifestResourceStream(name)) using (var stream = _assembly.GetManifestResourceStream(name))
{ {

View File

@ -168,7 +168,7 @@ namespace Emby.Dlna.Eventing
builder.Append("</e:propertyset>"); 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.Content = new StringContent(builder.ToString(), Encoding.UTF8, MediaTypeNames.Text.Xml);
options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType); options.Headers.TryAddWithoutValidation("NT", subscription.NotificationType);
options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange"); options.Headers.TryAddWithoutValidation("NTS", "upnp:propchange");

View File

@ -257,9 +257,10 @@ namespace Emby.Dlna.Main
private async Task RegisterServerEndpoints() 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 udn = CreateUuid(_appHost.SystemId);
var descriptorUri = "/dlna/" + udn + "/description.xml";
foreach (var address in addresses) foreach (var address in addresses)
{ {
@ -279,7 +280,6 @@ namespace Emby.Dlna.Main
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address); _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
var descriptorUri = "/dlna/" + udn + "/description.xml";
var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri); var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri);
var device = new SsdpRootDevice var device = new SsdpRootDevice

View File

@ -326,7 +326,7 @@ namespace Emby.Dlna.PlayTo
public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken) 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); 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; var startIndex = command.StartIndex ?? 0;
if (startIndex > 0) if (startIndex > 0)
{ {
items = items.Skip(startIndex).ToList(); items = items.GetRange(startIndex, items.Count - startIndex);
} }
var playlist = new List<PlaylistItem>(); var playlist = new List<PlaylistItem>();

View File

@ -169,6 +169,7 @@ namespace Emby.Dlna.Service
var result = new ControlRequestInfo(localName, namespaceURI); var result = new ControlRequestInfo(localName, namespaceURI);
using var subReader = reader.ReadSubtree(); using var subReader = reader.ReadSubtree();
await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false); await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false);
return result;
} }
else else
{ {

View File

@ -15,6 +15,11 @@ namespace Emby.Naming.Video
public static CleanDateTimeResult Clean(string name, IReadOnlyList<Regex> cleanDateTimeRegexes) public static CleanDateTimeResult Clean(string name, IReadOnlyList<Regex> cleanDateTimeRegexes)
{ {
CleanDateTimeResult result = new CleanDateTimeResult(name); CleanDateTimeResult result = new CleanDateTimeResult(name);
if (string.IsNullOrEmpty(name))
{
return result;
}
var len = cleanDateTimeRegexes.Count; var len = cleanDateTimeRegexes.Count;
for (int i = 0; i < len; i++) for (int i = 0; i < len; i++)
{ {

View File

@ -209,7 +209,10 @@ namespace Emby.Notifications
_libraryUpdateTimer = null; _libraryUpdateTimer = null;
} }
items = items.Take(10).ToList(); if (items.Count > 10)
{
items = items.GetRange(0, 10);
}
foreach (var item in items) foreach (var item in items)
{ {

View File

@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.AppBase
} }
/// <inheritdoc /> /// <inheritdoc />
public string VirtualDataPath { get; } = "%AppDataPath%"; public string VirtualDataPath => "%AppDataPath%";
/// <summary> /// <summary>
/// Gets the image cache path. /// Gets the image cache path.

View File

@ -4,7 +4,6 @@ using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
@ -30,7 +29,6 @@ using Emby.Server.Implementations.Cryptography;
using Emby.Server.Implementations.Data; using Emby.Server.Implementations.Data;
using Emby.Server.Implementations.Devices; using Emby.Server.Implementations.Devices;
using Emby.Server.Implementations.Dto; using Emby.Server.Implementations.Dto;
using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.HttpServer.Security; using Emby.Server.Implementations.HttpServer.Security;
using Emby.Server.Implementations.IO; using Emby.Server.Implementations.IO;
using Emby.Server.Implementations.Library; using Emby.Server.Implementations.Library;
@ -339,7 +337,7 @@ namespace Emby.Server.Implementations
/// Gets the email address for use within a comment section of a user agent field. /// Gets the email address for use within a comment section of a user agent field.
/// Presently used to provide contact information to MusicBrainz service. /// Presently used to provide contact information to MusicBrainz service.
/// </summary> /// </summary>
public string ApplicationUserAgentAddress { get; } = "team@jellyfin.org"; public string ApplicationUserAgentAddress => "team@jellyfin.org";
/// <summary> /// <summary>
/// Gets the current application name. /// Gets the current application name.
@ -403,7 +401,7 @@ namespace Emby.Server.Implementations
/// <summary> /// <summary>
/// Resolves this instance. /// Resolves this instance.
/// </summary> /// </summary>
/// <typeparam name="T">The type</typeparam> /// <typeparam name="T">The type.</typeparam>
/// <returns>``0.</returns> /// <returns>``0.</returns>
public T Resolve<T>() => ServiceProvider.GetService<T>(); public T Resolve<T>() => ServiceProvider.GetService<T>();
@ -993,62 +991,36 @@ namespace Emby.Server.Implementations
protected abstract void RestartInternal(); protected abstract void RestartInternal();
/// <summary> /// <inheritdoc/>
/// Comparison function used in <see cref="GetPlugins" />. public IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true)
/// </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)
{ {
int compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture); var minimumVersion = new Version(0, 0, 0, 1);
var versions = new List<LocalPlugin>();
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 directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly); var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
string metafile;
foreach (var dir in directories) foreach (var dir in directories)
{ {
try try
{ {
metafile = Path.Combine(dir, "meta.json"); var metafile = Path.Combine(dir, "meta.json");
if (File.Exists(metafile)) if (File.Exists(metafile))
{ {
var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile); var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
if (!Version.TryParse(manifest.TargetAbi, out var targetAbi)) 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)) if (!Version.TryParse(manifest.Version, out var version))
{ {
version = new Version(0, 0, 0, 1); version = minimumVersion;
} }
if (ApplicationVersion >= targetAbi) if (ApplicationVersion >= targetAbi)
{ {
// Only load Plugins if the plugin is built for this version or below. // 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 else
@ -1057,15 +1029,15 @@ namespace Emby.Server.Implementations
metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1]; metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1];
int versionIndex = dir.LastIndexOf('_'); 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. // Versioned folder.
versions.Add((ver, metafile, dir)); versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir));
} }
else else
{ {
// Un-versioned folder - Add it under the path name and version 0.0.0.1. // 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; string lastName = string.Empty;
versions.Sort(VersionCompare); versions.Sort(LocalPlugin.Compare);
// Traverse backwards through the list. // Traverse backwards through the list.
// The first item will be the latest version. // The first item will be the latest version.
for (int x = versions.Count - 1; x >= 0; x--) for (int x = versions.Count - 1; x >= 0; x--)
{ {
if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase)) 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; lastName = versions[x].Name;
continue; continue;
} }
@ -1091,6 +1063,7 @@ namespace Emby.Server.Implementations
if (!string.IsNullOrEmpty(lastName) && cleanup) if (!string.IsNullOrEmpty(lastName) && cleanup)
{ {
// Attempt a cleanup of old folders. // Attempt a cleanup of old folders.
versions.RemoveAt(x);
try try
{ {
Logger.LogDebug("Deleting {Path}", versions[x].Path); Logger.LogDebug("Deleting {Path}", versions[x].Path);
@ -1103,7 +1076,7 @@ namespace Emby.Server.Implementations
} }
} }
return dllList; return versions;
} }
/// <summary> /// <summary>
@ -1114,21 +1087,24 @@ namespace Emby.Server.Implementations
{ {
if (Directory.Exists(ApplicationPaths.PluginsPath)) if (Directory.Exists(ApplicationPaths.PluginsPath))
{ {
foreach (var file in GetPlugins(ApplicationPaths.PluginsPath)) foreach (var plugin in GetLocalPlugins(ApplicationPaths.PluginsPath))
{ {
Assembly plugAss; foreach (var file in plugin.DllFiles)
try
{ {
plugAss = Assembly.LoadFrom(file); Assembly plugAss;
} try
catch (FileLoadException ex) {
{ plugAss = Assembly.LoadFrom(file);
Logger.LogError(ex, "Failed to load assembly {Path}", file); }
continue; catch (FileLoadException ex)
} {
Logger.LogError(ex, "Failed to load assembly {Path}", file);
continue;
}
Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file); Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
yield return plugAss; yield return plugAss;
}
} }
} }

View File

@ -250,21 +250,16 @@ namespace Emby.Server.Implementations.Channels
var all = channels; var all = channels;
var totalCount = all.Count; 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) if (query.RefreshLatestChannelItems)
{ {
foreach (var item in returnItems) foreach (var item in all)
{ {
RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult(); RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult();
} }
@ -272,7 +267,7 @@ namespace Emby.Server.Implementations.Channels
return new QueryResult<Channel> return new QueryResult<Channel>
{ {
Items = returnItems, Items = all,
TotalRecordCount = totalCount TotalRecordCount = totalCount
}; };
} }
@ -543,7 +538,7 @@ namespace Emby.Server.Implementations.Channels
return _libraryManager.GetItemIds( return _libraryManager.GetItemIds(
new InternalItemsQuery new InternalItemsQuery
{ {
IncludeItemTypes = new[] { typeof(Channel).Name }, IncludeItemTypes = new[] { nameof(Channel) },
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) } OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
}).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray(); }).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray();
} }

View File

@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.Channels
var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery
{ {
IncludeItemTypes = new[] { typeof(Channel).Name }, IncludeItemTypes = new[] { nameof(Channel) },
ExcludeItemIds = installedChannelIds.ToArray() ExcludeItemIds = installedChannelIds.ToArray()
}); });

View File

@ -2403,11 +2403,11 @@ namespace Emby.Server.Implementations.Data
if (string.IsNullOrEmpty(item.OfficialRating)) if (string.IsNullOrEmpty(item.OfficialRating))
{ {
builder.Append("((OfficialRating is null) * 10)"); builder.Append("(OfficialRating is null * 10)");
} }
else else
{ {
builder.Append("((OfficialRating=@ItemOfficialRating) * 10)"); builder.Append("(OfficialRating=@ItemOfficialRating * 10)");
} }
if (item.ProductionYear.HasValue) if (item.ProductionYear.HasValue)
@ -2416,8 +2416,26 @@ namespace Emby.Server.Implementations.Data
builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 5 Then 5 Else 0 End )"); builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 5 Then 5 Else 0 End )");
} }
//// genres, tags // genres, tags, studios, person, year?
builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId)) * 10)"); builder.Append("+ (Select count(1) * 10 from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId))");
if (item is MusicArtist)
{
// Match albums where the artist is AlbumArtist against other albums.
// It is assumed that similar albums => similar artists.
builder.Append(
@"+ (WITH artistValues AS (
SELECT DISTINCT albumValues.CleanValue
FROM ItemValues albumValues
INNER JOIN ItemValues artistAlbums ON albumValues.ItemId = artistAlbums.ItemId
INNER JOIN TypedBaseItems artistItem ON artistAlbums.CleanValue = artistItem.CleanName AND artistAlbums.TYPE = 1 AND artistItem.Guid = @SimilarItemId
), similarArtist AS (
SELECT albumValues.ItemId
FROM ItemValues albumValues
INNER JOIN ItemValues artistAlbums ON albumValues.ItemId = artistAlbums.ItemId
INNER JOIN TypedBaseItems artistItem ON artistAlbums.CleanValue = artistItem.CleanName AND artistAlbums.TYPE = 1 AND artistItem.Guid = A.Guid
) SELECT COUNT(DISTINCT(CleanValue)) * 10 FROM ItemValues WHERE ItemId IN (SELECT ItemId FROM similarArtist) AND CleanValue IN (SELECT CleanValue FROM artistValues))");
}
builder.Append(") as SimilarityScore"); builder.Append(") as SimilarityScore");
@ -3914,7 +3932,7 @@ namespace Emby.Server.Implementations.Data
if (query.IsPlayed.HasValue) if (query.IsPlayed.HasValue)
{ {
// We should probably figure this out for all folders, but for right now, this is the only place where we need it // We should probably figure this out for all folders, but for right now, this is the only place where we need it
if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], typeof(Series).Name, StringComparison.OrdinalIgnoreCase)) if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], nameof(Series), StringComparison.OrdinalIgnoreCase))
{ {
if (query.IsPlayed.Value) if (query.IsPlayed.Value)
{ {
@ -4755,29 +4773,29 @@ namespace Emby.Server.Implementations.Data
{ {
var list = new List<string>(); var list = new List<string>();
if (IsTypeInQuery(typeof(Person).Name, query)) if (IsTypeInQuery(nameof(Person), query))
{ {
list.Add(typeof(Person).Name); list.Add(nameof(Person));
} }
if (IsTypeInQuery(typeof(Genre).Name, query)) if (IsTypeInQuery(nameof(Genre), query))
{ {
list.Add(typeof(Genre).Name); list.Add(nameof(Genre));
} }
if (IsTypeInQuery(typeof(MusicGenre).Name, query)) if (IsTypeInQuery(nameof(MusicGenre), query))
{ {
list.Add(typeof(MusicGenre).Name); list.Add(nameof(MusicGenre));
} }
if (IsTypeInQuery(typeof(MusicArtist).Name, query)) if (IsTypeInQuery(nameof(MusicArtist), query))
{ {
list.Add(typeof(MusicArtist).Name); list.Add(nameof(MusicArtist));
} }
if (IsTypeInQuery(typeof(Studio).Name, query)) if (IsTypeInQuery(nameof(Studio), query))
{ {
list.Add(typeof(Studio).Name); list.Add(nameof(Studio));
} }
return list; return list;
@ -4832,12 +4850,12 @@ namespace Emby.Server.Implementations.Data
var types = new[] var types = new[]
{ {
typeof(Episode).Name, nameof(Episode),
typeof(Video).Name, nameof(Video),
typeof(Movie).Name, nameof(Movie),
typeof(MusicVideo).Name, nameof(MusicVideo),
typeof(Series).Name, nameof(Series),
typeof(Season).Name nameof(Season)
}; };
if (types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase))) if (types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase)))
@ -5002,26 +5020,33 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
CheckDisposed(); 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); var whereClauses = GetPeopleWhereClauses(query, null);
if (whereClauses.Count != 0) 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) if (query.Limit > 0)
{ {
commandText += " LIMIT " + query.Limit; commandText.Append(" LIMIT ").Append(query.Limit);
} }
using (var connection = GetConnection(true)) using (var connection = GetConnection(true))
{ {
var list = new List<string>(); 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 // Run this again to bind the params
GetPeopleWhereClauses(query, statement); GetPeopleWhereClauses(query, statement);
@ -5045,7 +5070,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
CheckDisposed(); CheckDisposed();
var commandText = "select ItemId, Name, Role, PersonType, SortOrder from People"; var commandText = "select ItemId, Name, Role, PersonType, SortOrder from People p";
var whereClauses = GetPeopleWhereClauses(query, null); var whereClauses = GetPeopleWhereClauses(query, null);
@ -5087,19 +5112,13 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (!query.ItemId.Equals(Guid.Empty)) if (!query.ItemId.Equals(Guid.Empty))
{ {
whereClauses.Add("ItemId=@ItemId"); 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)) if (!query.AppearsInItemId.Equals(Guid.Empty))
{ {
whereClauses.Add("Name in (Select Name from People where ItemId=@AppearsInItemId)"); whereClauses.Add("p.Name in (Select Name from People where ItemId=@AppearsInItemId)");
if (statement != null) statement?.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray());
{
statement.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray());
}
} }
var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList(); var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList();
@ -5107,10 +5126,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (queryPersonTypes.Count == 1) if (queryPersonTypes.Count == 1)
{ {
whereClauses.Add("PersonType=@PersonType"); whereClauses.Add("PersonType=@PersonType");
if (statement != null) statement?.TryBind("@PersonType", queryPersonTypes[0]);
{
statement.TryBind("@PersonType", queryPersonTypes[0]);
}
} }
else if (queryPersonTypes.Count > 1) else if (queryPersonTypes.Count > 1)
{ {
@ -5124,10 +5140,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (queryExcludePersonTypes.Count == 1) if (queryExcludePersonTypes.Count == 1)
{ {
whereClauses.Add("PersonType<>@PersonType"); whereClauses.Add("PersonType<>@PersonType");
if (statement != null) statement?.TryBind("@PersonType", queryExcludePersonTypes[0]);
{
statement.TryBind("@PersonType", queryExcludePersonTypes[0]);
}
} }
else if (queryExcludePersonTypes.Count > 1) else if (queryExcludePersonTypes.Count > 1)
{ {
@ -5139,19 +5152,24 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (query.MaxListOrder.HasValue) if (query.MaxListOrder.HasValue)
{ {
whereClauses.Add("ListOrder<=@MaxListOrder"); whereClauses.Add("ListOrder<=@MaxListOrder");
if (statement != null) statement?.TryBind("@MaxListOrder", query.MaxListOrder.Value);
{
statement.TryBind("@MaxListOrder", query.MaxListOrder.Value);
}
} }
if (!string.IsNullOrWhiteSpace(query.NameContains)) if (!string.IsNullOrWhiteSpace(query.NameContains))
{ {
whereClauses.Add("Name like @NameContains"); whereClauses.Add("p.Name like @NameContains");
if (statement != null) statement?.TryBind("@NameContains", "%" + query.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; return whereClauses;
@ -5420,6 +5438,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
NameStartsWithOrGreater = query.NameStartsWithOrGreater, NameStartsWithOrGreater = query.NameStartsWithOrGreater,
Tags = query.Tags, Tags = query.Tags,
OfficialRatings = query.OfficialRatings, OfficialRatings = query.OfficialRatings,
StudioIds = query.StudioIds,
GenreIds = query.GenreIds, GenreIds = query.GenreIds,
Genres = query.Genres, Genres = query.Genres,
Years = query.Years, Years = query.Years,

View File

@ -465,7 +465,7 @@ namespace Emby.Server.Implementations.Dto
{ {
var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery
{ {
IncludeItemTypes = new[] { typeof(MusicAlbum).Name }, IncludeItemTypes = new[] { nameof(MusicAlbum) },
Name = item.Album, Name = item.Album,
Limit = 1 Limit = 1
}); });

View File

@ -1,6 +1,7 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -19,12 +20,12 @@ namespace Emby.Server.Implementations.HttpServer.Security
public AuthorizationInfo Authenticate(HttpRequest request) public AuthorizationInfo Authenticate(HttpRequest request)
{ {
var auth = _authorizationContext.GetAuthorizationInfo(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."); throw new SecurityException("User account has been disabled.");
} }

View File

@ -36,8 +36,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext) public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
{ {
var auth = GetAuthorizationDictionary(requestContext); var auth = GetAuthorizationDictionary(requestContext);
var (authInfo, _) = var authInfo = GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
return authInfo; return authInfo;
} }
@ -49,19 +48,13 @@ namespace Emby.Server.Implementations.HttpServer.Security
private AuthorizationInfo GetAuthorization(HttpContext httpReq) private AuthorizationInfo GetAuthorization(HttpContext httpReq)
{ {
var auth = GetAuthorizationDictionary(httpReq); var auth = GetAuthorizationDictionary(httpReq);
var (authInfo, originalAuthInfo) = var authInfo = GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
if (originalAuthInfo != null)
{
httpReq.Request.HttpContext.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
}
httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo; httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
return authInfo; return authInfo;
} }
private (AuthorizationInfo authInfo, AuthenticationInfo originalAuthenticationInfo) GetAuthorizationInfoFromDictionary( private AuthorizationInfo GetAuthorizationInfoFromDictionary(
in Dictionary<string, string> auth, in Dictionary<string, string> auth,
in IHeaderDictionary headers, in IHeaderDictionary headers,
in IQueryCollection queryString) in IQueryCollection queryString)
@ -108,88 +101,102 @@ namespace Emby.Server.Implementations.HttpServer.Security
Device = device, Device = device,
DeviceId = deviceId, DeviceId = deviceId,
Version = version, 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 (string.IsNullOrWhiteSpace(authInfo.DeviceId))
if (originalAuthenticationInfo != null)
{ {
var updateToken = false; authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
}
// TODO: Remove these checks for IsNullOrWhiteSpace // Temporary. TODO - allow clients to specify that the token has been shared with a casting device
if (string.IsNullOrWhiteSpace(authInfo.Client)) var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
{
authInfo.Client = originalAuthenticationInfo.AppName;
}
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 if (string.IsNullOrWhiteSpace(authInfo.Version))
var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1; {
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)) if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
{ {
authInfo.Device = originalAuthenticationInfo.DeviceName; originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
} updateToken = true;
else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase)) }
{
if (allowTokenInfoUpdate)
{
updateToken = true;
originalAuthenticationInfo.DeviceName = authInfo.Device;
}
}
if (string.IsNullOrWhiteSpace(authInfo.Version)) if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
{ {
authInfo.Version = originalAuthenticationInfo.AppVersion; authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
}
else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{
updateToken = true;
originalAuthenticationInfo.AppVersion = authInfo.Version;
}
}
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; updateToken = true;
} }
if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty)) authInfo.IsApiKey = true;
{ }
authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId); else
{
authInfo.IsApiKey = false;
}
if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase)) if (updateToken)
{ {
originalAuthenticationInfo.UserName = authInfo.User.Username; _authRepo.Update(originalAuthenticationInfo);
updateToken = true;
}
}
if (updateToken)
{
_authRepo.Update(originalAuthenticationInfo);
}
} }
} }
return (authInfo, originalAuthenticationInfo); return authInfo;
} }
/// <summary> /// <summary>
@ -267,7 +274,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (param.Length == 2) if (param.Length == 2)
{ {
var value = NormalizeValue(param[1].Trim(new[] { '"' })); var value = NormalizeValue(param[1].Trim(new[] { '"' }));
result.Add(param[0], value); result[param[0]] = value;
} }
} }

View File

@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Images
// return _libraryManager.GetItemList(new InternalItemsQuery // return _libraryManager.GetItemList(new InternalItemsQuery
// { // {
// ArtistIds = new[] { item.Id }, // ArtistIds = new[] { item.Id },
// IncludeItemTypes = new[] { typeof(MusicAlbum).Name }, // IncludeItemTypes = new[] { nameof(MusicAlbum) },
// OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) }, // OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
// Limit = 4, // Limit = 4,
// Recursive = true, // Recursive = true,

View File

@ -42,7 +42,12 @@ namespace Emby.Server.Implementations.Images
return _libraryManager.GetItemList(new InternalItemsQuery return _libraryManager.GetItemList(new InternalItemsQuery
{ {
Genres = new[] { item.Name }, Genres = new[] { item.Name },
IncludeItemTypes = new[] { typeof(MusicAlbum).Name, typeof(MusicVideo).Name, typeof(Audio).Name }, IncludeItemTypes = new[]
{
nameof(MusicAlbum),
nameof(MusicVideo),
nameof(Audio)
},
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) }, OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
Limit = 4, Limit = 4,
Recursive = true, Recursive = true,
@ -77,7 +82,7 @@ namespace Emby.Server.Implementations.Images
return _libraryManager.GetItemList(new InternalItemsQuery return _libraryManager.GetItemList(new InternalItemsQuery
{ {
Genres = new[] { item.Name }, Genres = new[] { item.Name },
IncludeItemTypes = new[] { typeof(Series).Name, typeof(Movie).Name }, IncludeItemTypes = new[] { nameof(Series), nameof(Movie) },
OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) }, OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
Limit = 4, Limit = 4,
Recursive = true, Recursive = true,

View File

@ -2440,6 +2440,21 @@ namespace Emby.Server.Implementations.Library
new SubtitleResolver(BaseItem.LocalizationManager).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files); 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 /> /// <inheritdoc />
public bool IsVideoFile(string path) public bool IsVideoFile(string path)
{ {

View File

@ -49,7 +49,7 @@ namespace Emby.Server.Implementations.Library
var genres = item var genres = item
.GetRecursiveChildren(user, new InternalItemsQuery(user) .GetRecursiveChildren(user, new InternalItemsQuery(user)
{ {
IncludeItemTypes = new[] { typeof(Audio).Name }, IncludeItemTypes = new[] { nameof(Audio) },
DtoOptions = dtoOptions DtoOptions = dtoOptions
}) })
.Cast<Audio>() .Cast<Audio>()
@ -86,7 +86,7 @@ namespace Emby.Server.Implementations.Library
{ {
return _libraryManager.GetItemList(new InternalItemsQuery(user) return _libraryManager.GetItemList(new InternalItemsQuery(user)
{ {
IncludeItemTypes = new[] { typeof(Audio).Name }, IncludeItemTypes = new[] { nameof(Audio) },
GenreIds = genreIds.ToArray(), GenreIds = genreIds.ToArray(),

View File

@ -87,61 +87,61 @@ namespace Emby.Server.Implementations.Library
var excludeItemTypes = query.ExcludeItemTypes.ToList(); var excludeItemTypes = query.ExcludeItemTypes.ToList();
var includeItemTypes = (query.IncludeItemTypes ?? Array.Empty<string>()).ToList(); var includeItemTypes = (query.IncludeItemTypes ?? Array.Empty<string>()).ToList();
excludeItemTypes.Add(typeof(Year).Name); excludeItemTypes.Add(nameof(Year));
excludeItemTypes.Add(typeof(Folder).Name); excludeItemTypes.Add(nameof(Folder));
if (query.IncludeGenres && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Genre", StringComparer.OrdinalIgnoreCase))) if (query.IncludeGenres && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Genre", StringComparer.OrdinalIgnoreCase)))
{ {
if (!query.IncludeMedia) if (!query.IncludeMedia)
{ {
AddIfMissing(includeItemTypes, typeof(Genre).Name); AddIfMissing(includeItemTypes, nameof(Genre));
AddIfMissing(includeItemTypes, typeof(MusicGenre).Name); AddIfMissing(includeItemTypes, nameof(MusicGenre));
} }
} }
else else
{ {
AddIfMissing(excludeItemTypes, typeof(Genre).Name); AddIfMissing(excludeItemTypes, nameof(Genre));
AddIfMissing(excludeItemTypes, typeof(MusicGenre).Name); AddIfMissing(excludeItemTypes, nameof(MusicGenre));
} }
if (query.IncludePeople && (includeItemTypes.Count == 0 || includeItemTypes.Contains("People", StringComparer.OrdinalIgnoreCase) || includeItemTypes.Contains("Person", StringComparer.OrdinalIgnoreCase))) if (query.IncludePeople && (includeItemTypes.Count == 0 || includeItemTypes.Contains("People", StringComparer.OrdinalIgnoreCase) || includeItemTypes.Contains("Person", StringComparer.OrdinalIgnoreCase)))
{ {
if (!query.IncludeMedia) if (!query.IncludeMedia)
{ {
AddIfMissing(includeItemTypes, typeof(Person).Name); AddIfMissing(includeItemTypes, nameof(Person));
} }
} }
else else
{ {
AddIfMissing(excludeItemTypes, typeof(Person).Name); AddIfMissing(excludeItemTypes, nameof(Person));
} }
if (query.IncludeStudios && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Studio", StringComparer.OrdinalIgnoreCase))) if (query.IncludeStudios && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Studio", StringComparer.OrdinalIgnoreCase)))
{ {
if (!query.IncludeMedia) if (!query.IncludeMedia)
{ {
AddIfMissing(includeItemTypes, typeof(Studio).Name); AddIfMissing(includeItemTypes, nameof(Studio));
} }
} }
else else
{ {
AddIfMissing(excludeItemTypes, typeof(Studio).Name); AddIfMissing(excludeItemTypes, nameof(Studio));
} }
if (query.IncludeArtists && (includeItemTypes.Count == 0 || includeItemTypes.Contains("MusicArtist", StringComparer.OrdinalIgnoreCase))) if (query.IncludeArtists && (includeItemTypes.Count == 0 || includeItemTypes.Contains("MusicArtist", StringComparer.OrdinalIgnoreCase)))
{ {
if (!query.IncludeMedia) if (!query.IncludeMedia)
{ {
AddIfMissing(includeItemTypes, typeof(MusicArtist).Name); AddIfMissing(includeItemTypes, nameof(MusicArtist));
} }
} }
else else
{ {
AddIfMissing(excludeItemTypes, typeof(MusicArtist).Name); AddIfMissing(excludeItemTypes, nameof(MusicArtist));
} }
AddIfMissing(excludeItemTypes, typeof(CollectionFolder).Name); AddIfMissing(excludeItemTypes, nameof(CollectionFolder));
AddIfMissing(excludeItemTypes, typeof(Folder).Name); AddIfMissing(excludeItemTypes, nameof(Folder));
var mediaTypes = query.MediaTypes.ToList(); var mediaTypes = query.MediaTypes.ToList();
if (includeItemTypes.Count > 0) if (includeItemTypes.Count > 0)

View File

@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.Library.Validators
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{ {
IncludeItemTypes = new[] { typeof(MusicArtist).Name }, IncludeItemTypes = new[] { nameof(MusicArtist) },
IsDeadArtist = true, IsDeadArtist = true,
IsLocked = false IsLocked = false
}).Cast<MusicArtist>().ToList(); }).Cast<MusicArtist>().ToList();

View File

@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.Library.Validators
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{ {
IncludeItemTypes = new[] { typeof(Person).Name }, IncludeItemTypes = new[] { nameof(Person) },
IsDeadPerson = true, IsDeadPerson = true,
IsLocked = false IsLocked = false
}); });

View File

@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Validators
var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
{ {
IncludeItemTypes = new[] { typeof(Studio).Name }, IncludeItemTypes = new[] { nameof(Studio) },
IsDeadStudio = true, IsDeadStudio = true,
IsLocked = false IsLocked = false
}); });

View File

@ -1790,7 +1790,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{ {
var program = string.IsNullOrWhiteSpace(timer.ProgramId) ? null : _libraryManager.GetItemList(new InternalItemsQuery var program = string.IsNullOrWhiteSpace(timer.ProgramId) ? null : _libraryManager.GetItemList(new InternalItemsQuery
{ {
IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, IncludeItemTypes = new[] { nameof(LiveTvProgram) },
Limit = 1, Limit = 1,
ExternalId = timer.ProgramId, ExternalId = timer.ProgramId,
DtoOptions = new DtoOptions(true) DtoOptions = new DtoOptions(true)
@ -2151,7 +2151,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{ {
var query = new InternalItemsQuery var query = new InternalItemsQuery
{ {
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name }, IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
Limit = 1, Limit = 1,
DtoOptions = new DtoOptions(true) DtoOptions = new DtoOptions(true)
{ {
@ -2370,7 +2370,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var query = new InternalItemsQuery var query = new InternalItemsQuery
{ {
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name }, IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
ExternalSeriesId = seriesTimer.SeriesId, ExternalSeriesId = seriesTimer.SeriesId,
DtoOptions = new DtoOptions(true) DtoOptions = new DtoOptions(true)
{ {
@ -2405,7 +2405,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
channel = _libraryManager.GetItemList( channel = _libraryManager.GetItemList(
new InternalItemsQuery new InternalItemsQuery
{ {
IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name }, IncludeItemTypes = new string[] { nameof(LiveTvChannel) },
ItemIds = new[] { parent.ChannelId }, ItemIds = new[] { parent.ChannelId },
DtoOptions = new DtoOptions() DtoOptions = new DtoOptions()
}).FirstOrDefault() as LiveTvChannel; }).FirstOrDefault() as LiveTvChannel;
@ -2464,7 +2464,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
channel = _libraryManager.GetItemList( channel = _libraryManager.GetItemList(
new InternalItemsQuery new InternalItemsQuery
{ {
IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name }, IncludeItemTypes = new string[] { nameof(LiveTvChannel) },
ItemIds = new[] { programInfo.ChannelId }, ItemIds = new[] { programInfo.ChannelId },
DtoOptions = new DtoOptions() DtoOptions = new DtoOptions()
}).FirstOrDefault() as LiveTvChannel; }).FirstOrDefault() as LiveTvChannel;
@ -2529,7 +2529,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var seriesIds = _libraryManager.GetItemIds( var seriesIds = _libraryManager.GetItemIds(
new InternalItemsQuery new InternalItemsQuery
{ {
IncludeItemTypes = new[] { typeof(Series).Name }, IncludeItemTypes = new[] { nameof(Series) },
Name = program.Name Name = program.Name
}).ToArray(); }).ToArray();
@ -2542,7 +2542,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{ {
var result = _libraryManager.GetItemIds(new InternalItemsQuery var result = _libraryManager.GetItemIds(new InternalItemsQuery
{ {
IncludeItemTypes = new[] { typeof(Episode).Name }, IncludeItemTypes = new[] { nameof(Episode) },
ParentIndexNumber = program.SeasonNumber.Value, ParentIndexNumber = program.SeasonNumber.Value,
IndexNumber = program.EpisodeNumber.Value, IndexNumber = program.EpisodeNumber.Value,
AncestorIds = seriesIds, AncestorIds = seriesIds,

View File

@ -15,6 +15,7 @@ using System.Threading.Tasks;
using MediaBrowser.Common; using MediaBrowser.Common;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.LiveTv;
@ -33,17 +34,20 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1); private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
private readonly IApplicationHost _appHost; private readonly IApplicationHost _appHost;
private readonly ICryptoProvider _cryptoProvider;
public SchedulesDirect( public SchedulesDirect(
ILogger<SchedulesDirect> logger, ILogger<SchedulesDirect> logger,
IJsonSerializer jsonSerializer, IJsonSerializer jsonSerializer,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
IApplicationHost appHost) IApplicationHost appHost,
ICryptoProvider cryptoProvider)
{ {
_logger = logger; _logger = logger;
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_appHost = appHost; _appHost = appHost;
_cryptoProvider = cryptoProvider;
} }
private string UserAgent => _appHost.ApplicationUserAgent; private string UserAgent => _appHost.ApplicationUserAgent;
@ -642,7 +646,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token"); 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); using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);

View File

@ -159,7 +159,7 @@ namespace Emby.Server.Implementations.LiveTv
{ {
var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery
{ {
IncludeItemTypes = new string[] { typeof(Series).Name }, IncludeItemTypes = new string[] { nameof(Series) },
Name = seriesName, Name = seriesName,
Limit = 1, Limit = 1,
ImageTypes = new ImageType[] { ImageType.Thumb }, ImageTypes = new ImageType[] { ImageType.Thumb },
@ -253,7 +253,7 @@ namespace Emby.Server.Implementations.LiveTv
{ {
var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery
{ {
IncludeItemTypes = new string[] { typeof(Series).Name }, IncludeItemTypes = new string[] { nameof(Series) },
Name = seriesName, Name = seriesName,
Limit = 1, Limit = 1,
ImageTypes = new ImageType[] { ImageType.Thumb }, ImageTypes = new ImageType[] { ImageType.Thumb },
@ -296,7 +296,7 @@ namespace Emby.Server.Implementations.LiveTv
var program = _libraryManager.GetItemList(new InternalItemsQuery var program = _libraryManager.GetItemList(new InternalItemsQuery
{ {
IncludeItemTypes = new string[] { typeof(Series).Name }, IncludeItemTypes = new string[] { nameof(Series) },
Name = seriesName, Name = seriesName,
Limit = 1, Limit = 1,
ImageTypes = new ImageType[] { ImageType.Primary }, ImageTypes = new ImageType[] { ImageType.Primary },
@ -307,7 +307,7 @@ namespace Emby.Server.Implementations.LiveTv
{ {
program = _libraryManager.GetItemList(new InternalItemsQuery program = _libraryManager.GetItemList(new InternalItemsQuery
{ {
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name }, IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
ExternalSeriesId = programSeriesId, ExternalSeriesId = programSeriesId,
Limit = 1, Limit = 1,
ImageTypes = new ImageType[] { ImageType.Primary }, ImageTypes = new ImageType[] { ImageType.Primary },

View File

@ -187,7 +187,7 @@ namespace Emby.Server.Implementations.LiveTv
IsKids = query.IsKids, IsKids = query.IsKids,
IsSports = query.IsSports, IsSports = query.IsSports,
IsSeries = query.IsSeries, IsSeries = query.IsSeries,
IncludeItemTypes = new[] { typeof(LiveTvChannel).Name }, IncludeItemTypes = new[] { nameof(LiveTvChannel) },
TopParentIds = new[] { topFolder.Id }, TopParentIds = new[] { topFolder.Id },
IsFavorite = query.IsFavorite, IsFavorite = query.IsFavorite,
IsLiked = query.IsLiked, IsLiked = query.IsLiked,
@ -808,7 +808,7 @@ namespace Emby.Server.Implementations.LiveTv
var internalQuery = new InternalItemsQuery(user) var internalQuery = new InternalItemsQuery(user)
{ {
IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, IncludeItemTypes = new[] { nameof(LiveTvProgram) },
MinEndDate = query.MinEndDate, MinEndDate = query.MinEndDate,
MinStartDate = query.MinStartDate, MinStartDate = query.MinStartDate,
MaxEndDate = query.MaxEndDate, MaxEndDate = query.MaxEndDate,
@ -872,7 +872,7 @@ namespace Emby.Server.Implementations.LiveTv
var internalQuery = new InternalItemsQuery(user) var internalQuery = new InternalItemsQuery(user)
{ {
IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, IncludeItemTypes = new[] { nameof(LiveTvProgram) },
IsAiring = query.IsAiring, IsAiring = query.IsAiring,
HasAired = query.HasAired, HasAired = query.HasAired,
IsNews = query.IsNews, IsNews = query.IsNews,
@ -1089,8 +1089,8 @@ namespace Emby.Server.Implementations.LiveTv
if (cleanDatabase) if (cleanDatabase)
{ {
CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { typeof(LiveTvChannel).Name }, progress, cancellationToken); CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { nameof(LiveTvChannel) }, progress, cancellationToken);
CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { typeof(LiveTvProgram).Name }, progress, cancellationToken); CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { nameof(LiveTvProgram) }, progress, cancellationToken);
} }
var coreService = _services.OfType<EmbyTV.EmbyTV>().FirstOrDefault(); var coreService = _services.OfType<EmbyTV.EmbyTV>().FirstOrDefault();
@ -1181,7 +1181,7 @@ namespace Emby.Server.Implementations.LiveTv
var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
{ {
IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name }, IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
ChannelIds = new Guid[] { currentChannel.Id }, ChannelIds = new Guid[] { currentChannel.Id },
DtoOptions = new DtoOptions(true) DtoOptions = new DtoOptions(true)
}).Cast<LiveTvProgram>().ToDictionary(i => i.Id); }).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
@ -1346,11 +1346,11 @@ namespace Emby.Server.Implementations.LiveTv
{ {
if (query.IsMovie.Value) if (query.IsMovie.Value)
{ {
includeItemTypes.Add(typeof(Movie).Name); includeItemTypes.Add(nameof(Movie));
} }
else else
{ {
excludeItemTypes.Add(typeof(Movie).Name); excludeItemTypes.Add(nameof(Movie));
} }
} }
@ -1358,11 +1358,11 @@ namespace Emby.Server.Implementations.LiveTv
{ {
if (query.IsSeries.Value) if (query.IsSeries.Value)
{ {
includeItemTypes.Add(typeof(Episode).Name); includeItemTypes.Add(nameof(Episode));
} }
else else
{ {
excludeItemTypes.Add(typeof(Episode).Name); excludeItemTypes.Add(nameof(Episode));
} }
} }
@ -1883,7 +1883,7 @@ namespace Emby.Server.Implementations.LiveTv
var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user) var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user)
{ {
IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, IncludeItemTypes = new[] { nameof(LiveTvProgram) },
ChannelIds = channelIds, ChannelIds = channelIds,
MaxStartDate = now, MaxStartDate = now,
MinEndDate = now, MinEndDate = now,

View File

@ -131,6 +131,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
await taskCompletionSource.Task.ConfigureAwait(false); await taskCompletionSource.Task.ConfigureAwait(false);
} }
public string GetFilePath()
{
return TempFilePath;
}
private Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) private Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{ {
return Task.Run(async () => return Task.Run(async () =>

View File

@ -65,7 +65,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{ {
var channelIdPrefix = GetFullChannelIdPrefix(info); 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) public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
@ -126,7 +128,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public async Task Validate(TunerHostInfo info) 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))
{ {
} }
} }

View File

@ -13,6 +13,7 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.LiveTv;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts namespace Emby.Server.Implementations.LiveTv.TunerHosts
@ -30,12 +31,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
_appHost = appHost; _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. // 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) using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url);
.GetStreamAsync(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:"; private const string ExtInfPrefix = "#EXTINF:";

View File

@ -55,7 +55,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
var typeName = GetType().Name; var typeName = GetType().Name;
Logger.LogInformation("Opening " + typeName + " Live stream from {0}", url); 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) .GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
.ConfigureAwait(false); .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) private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{ {
return Task.Run(async () => return Task.Run(async () =>

View File

@ -113,5 +113,7 @@
"TasksChannelsCategory": "Internetové kanály", "TasksChannelsCategory": "Internetové kanály",
"TasksApplicationCategory": "Aplikace", "TasksApplicationCategory": "Aplikace",
"TasksLibraryCategory": "Knihovna", "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"
} }

View File

@ -113,5 +113,7 @@
"TasksChannelsCategory": "Internet Kanäle", "TasksChannelsCategory": "Internet Kanäle",
"TasksApplicationCategory": "Anwendung", "TasksApplicationCategory": "Anwendung",
"TasksLibraryCategory": "Bibliothek", "TasksLibraryCategory": "Bibliothek",
"TasksMaintenanceCategory": "Wartung" "TasksMaintenanceCategory": "Wartung",
"TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.",
"TaskCleanActivityLog": "Aktivitätsprotokoll aufräumen"
} }

View File

@ -113,5 +113,7 @@
"TasksChannelsCategory": "Internet Channels", "TasksChannelsCategory": "Internet Channels",
"TasksApplicationCategory": "Application", "TasksApplicationCategory": "Application",
"TasksLibraryCategory": "Library", "TasksLibraryCategory": "Library",
"TasksMaintenanceCategory": "Maintenance" "TasksMaintenanceCategory": "Maintenance",
"TaskCleanActivityLogDescription": "Deletes activity log entries older than the configured age.",
"TaskCleanActivityLog": "Clean Activity Log"
} }

View File

@ -95,6 +95,8 @@
"TasksLibraryCategory": "Library", "TasksLibraryCategory": "Library",
"TasksApplicationCategory": "Application", "TasksApplicationCategory": "Application",
"TasksChannelsCategory": "Internet Channels", "TasksChannelsCategory": "Internet Channels",
"TaskCleanActivityLog": "Clean Activity Log",
"TaskCleanActivityLogDescription": "Deletes activity log entries older than the configured age.",
"TaskCleanCache": "Clean Cache Directory", "TaskCleanCache": "Clean Cache Directory",
"TaskCleanCacheDescription": "Deletes cache files no longer needed by the system.", "TaskCleanCacheDescription": "Deletes cache files no longer needed by the system.",
"TaskRefreshChapterImages": "Extract Chapter Images", "TaskRefreshChapterImages": "Extract Chapter Images",

View File

@ -77,7 +77,7 @@
"SubtitleDownloadFailureFromForItem": "Fallo de descarga de subtítulos desde {0} para {1}", "SubtitleDownloadFailureFromForItem": "Fallo de descarga de subtítulos desde {0} para {1}",
"Sync": "Sincronizar", "Sync": "Sincronizar",
"System": "Sistema", "System": "Sistema",
"TvShows": "Programas de televisión", "TvShows": "Series",
"User": "Usuario", "User": "Usuario",
"UserCreatedWithName": "El usuario {0} ha sido creado", "UserCreatedWithName": "El usuario {0} ha sido creado",
"UserDeletedWithName": "El usuario {0} ha sido borrado", "UserDeletedWithName": "El usuario {0} ha sido borrado",
@ -113,5 +113,7 @@
"TaskRefreshChannels": "Actualizar canales", "TaskRefreshChannels": "Actualizar canales",
"TaskRefreshChannelsDescription": "Actualiza la información de los canales de internet.", "TaskRefreshChannelsDescription": "Actualiza la información de los canales de internet.",
"TaskDownloadMissingSubtitles": "Descargar los subtítulos que faltan", "TaskDownloadMissingSubtitles": "Descargar los subtítulos que faltan",
"TaskDownloadMissingSubtitlesDescription": "Busca en internet los subtítulos que falten en el contenido de tus bibliotecas, basándose en la configuración de los metadatos." "TaskDownloadMissingSubtitlesDescription": "Busca en internet los subtítulos que falten en el contenido de tus bibliotecas, basándose en la configuración de los metadatos.",
"TaskCleanActivityLogDescription": "Elimina todos los registros de actividad anteriores a la fecha configurada.",
"TaskCleanActivityLog": "Limpiar registro de actividad"
} }

View File

@ -1,7 +1,7 @@
{ {
"HeaderLiveTV": "Live-TV", "HeaderLiveTV": "Live-TV",
"NewVersionIsAvailable": "Uusi versio Jellyfin palvelimesta on ladattavissa.", "NewVersionIsAvailable": "Uusi versio Jellyfin palvelimesta on ladattavissa.",
"NameSeasonUnknown": "Tuntematon Kausi", "NameSeasonUnknown": "Tuntematon kausi",
"NameSeasonNumber": "Kausi {0}", "NameSeasonNumber": "Kausi {0}",
"NameInstallFailed": "{0} asennus epäonnistui", "NameInstallFailed": "{0} asennus epäonnistui",
"MusicVideos": "Musiikkivideot", "MusicVideos": "Musiikkivideot",
@ -19,23 +19,23 @@
"ItemAddedWithName": "{0} lisättiin kirjastoon", "ItemAddedWithName": "{0} lisättiin kirjastoon",
"Inherit": "Periytyä", "Inherit": "Periytyä",
"HomeVideos": "Kotivideot", "HomeVideos": "Kotivideot",
"HeaderRecordingGroups": "Nauhoiteryhmät", "HeaderRecordingGroups": "Tallennusryhmät",
"HeaderNextUp": "Seuraavaksi", "HeaderNextUp": "Seuraavaksi",
"HeaderFavoriteSongs": "Lempikappaleet", "HeaderFavoriteSongs": "Suosikkikappaleet",
"HeaderFavoriteShows": "Lempisarjat", "HeaderFavoriteShows": "Suosikkisarjat",
"HeaderFavoriteEpisodes": "Lempijaksot", "HeaderFavoriteEpisodes": "Suosikkijaksot",
"HeaderFavoriteArtists": "Lempiartistit", "HeaderFavoriteArtists": "Suosikkiartistit",
"HeaderFavoriteAlbums": "Lempialbumit", "HeaderFavoriteAlbums": "Suosikkialbumit",
"HeaderContinueWatching": "Jatka katsomista", "HeaderContinueWatching": "Jatka katsomista",
"HeaderAlbumArtists": "Albumin esittäjä", "HeaderAlbumArtists": "Albumin artistit",
"Genres": "Tyylilajit", "Genres": "Tyylilajit",
"Folders": "Kansiot", "Folders": "Kansiot",
"Favorites": "Suosikit", "Favorites": "Suosikit",
"FailedLoginAttemptWithUserName": "Kirjautuminen epäonnistui kohteesta {0}", "FailedLoginAttemptWithUserName": "Kirjautuminen epäonnistui kohteesta {0}",
"DeviceOnlineWithName": "{0} on yhdistetty", "DeviceOnlineWithName": "{0} on yhdistetty",
"DeviceOfflineWithName": "{0} on katkaissut yhteytensä", "DeviceOfflineWithName": "{0} yhteys on katkaistu",
"Collections": "Kokoelmat", "Collections": "Kokoelmat",
"ChapterNameValue": "Luku: {0}", "ChapterNameValue": "Jakso: {0}",
"Channels": "Kanavat", "Channels": "Kanavat",
"CameraImageUploadedFrom": "Uusi kamerakuva on ladattu {0}", "CameraImageUploadedFrom": "Uusi kamerakuva on ladattu {0}",
"Books": "Kirjat", "Books": "Kirjat",
@ -61,25 +61,25 @@
"UserPolicyUpdatedWithName": "Käyttöoikeudet päivitetty käyttäjälle {0}", "UserPolicyUpdatedWithName": "Käyttöoikeudet päivitetty käyttäjälle {0}",
"UserPasswordChangedWithName": "Salasana vaihdettu käyttäjälle {0}", "UserPasswordChangedWithName": "Salasana vaihdettu käyttäjälle {0}",
"UserOnlineFromDevice": "{0} on paikalla osoitteesta {1}", "UserOnlineFromDevice": "{0} on paikalla osoitteesta {1}",
"UserOfflineFromDevice": "{0} yhteys katkaistu {1}", "UserOfflineFromDevice": "{0} yhteys katkaistu kohteesta {1}",
"UserLockedOutWithName": "Käyttäjä {0} lukittu", "UserLockedOutWithName": "Käyttäjä {0} lukittu",
"UserDownloadingItemWithValues": "{0} lataa {1}", "UserDownloadingItemWithValues": "{0} lataa {1}",
"UserDeletedWithName": "Käyttäjä {0} poistettu", "UserDeletedWithName": "Käyttäjä {0} poistettu",
"UserCreatedWithName": "Käyttäjä {0} luotu", "UserCreatedWithName": "Käyttäjä {0} luotu",
"TvShows": "TV-sarjat", "TvShows": "TV-ohjelmat",
"Sync": "Synkronoi", "Sync": "Synkronoi",
"SubtitleDownloadFailureFromForItem": "Tekstitysten lataus ({0} -> {1}) epäonnistui //this string would have to be generated for each provider and movie because of finnish cases, sorry", "SubtitleDownloadFailureFromForItem": "Tekstitystä ei voitu ladata osoitteesta {0} kohteelle {1}",
"StartupEmbyServerIsLoading": "Jellyfin palvelin latautuu. Kokeile hetken kuluttua uudelleen.", "StartupEmbyServerIsLoading": "Jellyfin palvelin latautuu. Yritä hetken kuluttua uudelleen.",
"Songs": "Kappaleet", "Songs": "Kappaleet",
"Shows": "Sarjat", "Shows": "Ohjelmat",
"ServerNameNeedsToBeRestarted": "{0} täytyy käynnistää uudelleen", "ServerNameNeedsToBeRestarted": "{0} on käynnistettävä uudelleen",
"ProviderValue": "Tarjoaja: {0}", "ProviderValue": "Tarjoaja: {0}",
"Plugin": "Liitännäinen", "Plugin": "Liitännäinen",
"NotificationOptionVideoPlaybackStopped": "Videon toisto pysäytetty", "NotificationOptionVideoPlaybackStopped": "Videon toisto pysäytetty",
"NotificationOptionVideoPlayback": "Videota toistetaan", "NotificationOptionVideoPlayback": "Videota toistetaan",
"NotificationOptionUserLockedOut": "Käyttäjä kirjautui ulos", "NotificationOptionUserLockedOut": "Käyttäjä kirjautui ulos",
"NotificationOptionTaskFailed": "Ajastettu tehtävä epäonnistui", "NotificationOptionTaskFailed": "Ajastettu tehtävä epäonnistui",
"NotificationOptionServerRestartRequired": "Palvelin pitää käynnistää uudelleen", "NotificationOptionServerRestartRequired": "Palvelin on käynnistettävä uudelleen",
"NotificationOptionPluginUpdateInstalled": "Liitännäinen päivitetty", "NotificationOptionPluginUpdateInstalled": "Liitännäinen päivitetty",
"NotificationOptionPluginUninstalled": "Liitännäinen poistettu", "NotificationOptionPluginUninstalled": "Liitännäinen poistettu",
"NotificationOptionPluginInstalled": "Liitännäinen asennettu", "NotificationOptionPluginInstalled": "Liitännäinen asennettu",
@ -104,10 +104,10 @@
"TaskRefreshPeople": "Päivitä henkilöt", "TaskRefreshPeople": "Päivitä henkilöt",
"TaskCleanLogsDescription": "Poistaa lokitiedostot jotka ovat yli {0} päivää vanhoja.", "TaskCleanLogsDescription": "Poistaa lokitiedostot jotka ovat yli {0} päivää vanhoja.",
"TaskCleanLogs": "Puhdista lokihakemisto", "TaskCleanLogs": "Puhdista lokihakemisto",
"TaskRefreshLibraryDescription": "Skannaa mediakirjastosi uusien tiedostojen varalle, sekä virkistää metatiedot.", "TaskRefreshLibraryDescription": "Skannaa mediakirjastosi uudet tiedostot ja päivittää metatiedot.",
"TaskRefreshLibrary": "Skannaa mediakirjasto", "TaskRefreshLibrary": "Skannaa mediakirjasto",
"TaskRefreshChapterImagesDescription": "Luo pienoiskuvat videoille joissa on lukuja.", "TaskRefreshChapterImagesDescription": "Luo pienoiskuvat videoille joissa on jaksoja.",
"TaskRefreshChapterImages": "Eristä lukujen kuvat", "TaskRefreshChapterImages": "Pura jakson kuvat",
"TaskCleanCacheDescription": "Poistaa järjestelmälle tarpeettomat väliaikaistiedostot.", "TaskCleanCacheDescription": "Poistaa järjestelmälle tarpeettomat väliaikaistiedostot.",
"TaskCleanCache": "Tyhjennä välimuisti-hakemisto", "TaskCleanCache": "Tyhjennä välimuisti-hakemisto",
"TasksChannelsCategory": "Internet kanavat", "TasksChannelsCategory": "Internet kanavat",

View File

@ -1,7 +1,7 @@
{ {
"VersionNumber": "Bersyon {0}", "VersionNumber": "Bersyon {0}",
"ValueSpecialEpisodeName": "Espesyal - {0}", "ValueSpecialEpisodeName": "Espesyal - {0}",
"ValueHasBeenAddedToLibrary": "Naidagdag na ang {0} sa iyong media library", "ValueHasBeenAddedToLibrary": "Naidagdag na ang {0} sa iyong librerya ng medya",
"UserStoppedPlayingItemWithValues": "Natapos ni {0} ang {1} sa {2}", "UserStoppedPlayingItemWithValues": "Natapos ni {0} ang {1} sa {2}",
"UserStartedPlayingItemWithValues": "Si {0} ay nagplaplay ng {1} sa {2}", "UserStartedPlayingItemWithValues": "Si {0} ay nagplaplay ng {1} sa {2}",
"UserPolicyUpdatedWithName": "Ang user policy ay naiupdate para kay {0}", "UserPolicyUpdatedWithName": "Ang user policy ay naiupdate para kay {0}",
@ -61,8 +61,8 @@
"Latest": "Pinakabago", "Latest": "Pinakabago",
"LabelRunningTimeValue": "Oras: {0}", "LabelRunningTimeValue": "Oras: {0}",
"LabelIpAddressValue": "Ang IP Address ay {0}", "LabelIpAddressValue": "Ang IP Address ay {0}",
"ItemRemovedWithName": "Naitanggal ang {0} sa library", "ItemRemovedWithName": "Naitanggal ang {0} sa librerya",
"ItemAddedWithName": "Naidagdag ang {0} sa library", "ItemAddedWithName": "Naidagdag ang {0} sa librerya",
"Inherit": "Manahin", "Inherit": "Manahin",
"HeaderRecordingGroups": "Pagtatalang Grupo", "HeaderRecordingGroups": "Pagtatalang Grupo",
"HeaderNextUp": "Susunod", "HeaderNextUp": "Susunod",
@ -90,12 +90,29 @@
"Application": "Aplikasyon", "Application": "Aplikasyon",
"AppDeviceValues": "Aplikasyon: {0}, Aparato: {1}", "AppDeviceValues": "Aplikasyon: {0}, Aparato: {1}",
"Albums": "Albums", "Albums": "Albums",
"TaskRefreshLibrary": "Suriin ang nasa librerya", "TaskRefreshLibrary": "Suriin and Librerya ng Medya",
"TaskRefreshChapterImagesDescription": "Gumawa ng larawan para sa mga pelikula na may kabanata", "TaskRefreshChapterImagesDescription": "Gumawa ng larawan para sa mga pelikula na may kabanata.",
"TaskRefreshChapterImages": "Kunin ang mga larawan ng kabanata", "TaskRefreshChapterImages": "Kunin ang mga larawan ng kabanata",
"TaskCleanCacheDescription": "Tanggalin ang mga cache file na hindi na kailangan ng systema.", "TaskCleanCacheDescription": "Tanggalin ang mga cache file na hindi na kailangan ng systema.",
"TasksChannelsCategory": "Palabas sa internet", "TasksChannelsCategory": "Palabas sa internet",
"TasksLibraryCategory": "Librerya", "TasksLibraryCategory": "Librerya",
"TasksMaintenanceCategory": "Pagpapanatili", "TasksMaintenanceCategory": "Pagpapanatili",
"HomeVideos": "Sariling pelikula" "HomeVideos": "Sariling pelikula",
"TaskRefreshPeopleDescription": "Ini-update ang metadata para sa mga aktor at direktor sa iyong librerya ng medya.",
"TaskRefreshPeople": "I-refresh ang Tauhan",
"TaskDownloadMissingSubtitlesDescription": "Hinahanap sa internet ang mga nawawalang subtiles base sa metadata configuration.",
"TaskDownloadMissingSubtitles": "I-download and nawawalang subtitles",
"TaskRefreshChannelsDescription": "Ni-rerefresh ang impormasyon sa internet channels.",
"TaskRefreshChannels": "I-refresh ang Channels",
"TaskCleanTranscodeDescription": "Binubura ang transcode files na mas matanda ng isang araw.",
"TaskUpdatePluginsDescription": "Nag download at install ng updates sa plugins na naka configure para sa automatikong pag update.",
"TaskUpdatePlugins": "I-update ang Plugins",
"TaskCleanLogsDescription": "Binubura and files ng talaan na mas mantanda ng {0} araw.",
"TaskCleanTranscode": "Linisin and Direktoryo ng Transcode",
"TaskCleanLogs": "Linisin and Direktoryo ng Talaan",
"TaskRefreshLibraryDescription": "Sinusuri ang iyong librerya ng medya para sa bagong files at irefresh ang metadata.",
"TaskCleanCache": "Linisin and Direktoryo ng Cache",
"TasksApplicationCategory": "Application",
"TaskCleanActivityLog": "Linisin ang Tala ng Aktibidad",
"TaskCleanActivityLogDescription": "Tanggalin ang mga tala ng aktibidad na mas matanda sa naka configure na edad."
} }

View File

@ -113,5 +113,7 @@
"TaskCleanCache": "Vider le répertoire cache", "TaskCleanCache": "Vider le répertoire cache",
"TasksApplicationCategory": "Application", "TasksApplicationCategory": "Application",
"TasksLibraryCategory": "Bibliothèque", "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é"
} }

View File

@ -0,0 +1,3 @@
{
"Albums": "आल्बुम्"
}

View File

@ -5,13 +5,13 @@
"Artists": "Izvođači", "Artists": "Izvođači",
"AuthenticationSucceededWithUserName": "{0} uspješno ovjerena", "AuthenticationSucceededWithUserName": "{0} uspješno ovjerena",
"Books": "Knjige", "Books": "Knjige",
"CameraImageUploadedFrom": "Nova fotografija sa kamere je uploadana iz {0}", "CameraImageUploadedFrom": "Nova fotografija sa kamere je učitana iz {0}",
"Channels": "Kanali", "Channels": "Kanali",
"ChapterNameValue": "Poglavlje {0}", "ChapterNameValue": "Poglavlje {0}",
"Collections": "Kolekcije", "Collections": "Kolekcije",
"DeviceOfflineWithName": "{0} se odspojilo", "DeviceOfflineWithName": "{0} je prekinuo vezu",
"DeviceOnlineWithName": "{0} je spojeno", "DeviceOnlineWithName": "{0} je povezan",
"FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave za {0}", "FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave od {0}",
"Favorites": "Favoriti", "Favorites": "Favoriti",
"Folders": "Mape", "Folders": "Mape",
"Genres": "Žanrovi", "Genres": "Žanrovi",
@ -23,95 +23,97 @@
"HeaderFavoriteShows": "Omiljene serije", "HeaderFavoriteShows": "Omiljene serije",
"HeaderFavoriteSongs": "Omiljene pjesme", "HeaderFavoriteSongs": "Omiljene pjesme",
"HeaderLiveTV": "TV uživo", "HeaderLiveTV": "TV uživo",
"HeaderNextUp": "Sljedeće je", "HeaderNextUp": "Slijedi",
"HeaderRecordingGroups": "Grupa snimka", "HeaderRecordingGroups": "Grupa snimka",
"HomeVideos": "Kućni videi", "HomeVideos": "Kućni video",
"Inherit": "Naslijedi", "Inherit": "Naslijedi",
"ItemAddedWithName": "{0} je dodano u biblioteku", "ItemAddedWithName": "{0} je dodano u biblioteku",
"ItemRemovedWithName": "{0} je uklonjen iz biblioteke", "ItemRemovedWithName": "{0} je uklonjeno iz biblioteke",
"LabelIpAddressValue": "IP adresa: {0}", "LabelIpAddressValue": "IP adresa: {0}",
"LabelRunningTimeValue": "Vrijeme rada: {0}", "LabelRunningTimeValue": "Vrijeme rada: {0}",
"Latest": "Najnovije", "Latest": "Najnovije",
"MessageApplicationUpdated": "Jellyfin Server je ažuriran", "MessageApplicationUpdated": "Jellyfin server je ažuriran",
"MessageApplicationUpdatedTo": "Jellyfin Server je ažuriran na {0}", "MessageApplicationUpdatedTo": "Jellyfin server je ažuriran na {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Odjeljak postavka servera {0} je ažuriran", "MessageNamedServerConfigurationUpdatedWithValue": "Dio konfiguracije servera {0} je ažuriran",
"MessageServerConfigurationUpdated": "Postavke servera su ažurirane", "MessageServerConfigurationUpdated": "Konfiguracija servera je ažurirana",
"MixedContent": "Miješani sadržaj", "MixedContent": "Miješani sadržaj",
"Movies": "Filmovi", "Movies": "Filmovi",
"Music": "Glazba", "Music": "Glazba",
"MusicVideos": "Glazbeni spotovi", "MusicVideos": "Glazbeni spotovi",
"NameInstallFailed": "{0} neuspješnih instalacija", "NameInstallFailed": "{0} neuspješnih instalacija",
"NameSeasonNumber": "Sezona {0}", "NameSeasonNumber": "Sezona {0}",
"NameSeasonUnknown": "Nepoznata sezona", "NameSeasonUnknown": "Sezona nepoznata",
"NewVersionIsAvailable": "Nova verzija Jellyfin servera je dostupna za preuzimanje.", "NewVersionIsAvailable": "Nova verzija Jellyfin servera je dostupna za preuzimanje.",
"NotificationOptionApplicationUpdateAvailable": "Dostupno ažuriranje aplikacije", "NotificationOptionApplicationUpdateAvailable": "Dostupno je ažuriranje aplikacije",
"NotificationOptionApplicationUpdateInstalled": "Instalirano ažuriranje aplikacije", "NotificationOptionApplicationUpdateInstalled": "Instalirano je ažuriranje aplikacije",
"NotificationOptionAudioPlayback": "Reprodukcija glazbe započeta", "NotificationOptionAudioPlayback": "Reprodukcija glazbe započela",
"NotificationOptionAudioPlaybackStopped": "Reprodukcija audiozapisa je zaustavljena", "NotificationOptionAudioPlaybackStopped": "Reprodukcija glazbe zaustavljena",
"NotificationOptionCameraImageUploaded": "Slike kamere preuzete", "NotificationOptionCameraImageUploaded": "Slika s kamere učitana",
"NotificationOptionInstallationFailed": "Instalacija neuspješna", "NotificationOptionInstallationFailed": "Instalacija nije uspjela",
"NotificationOptionNewLibraryContent": "Novi sadržaj je dodan", "NotificationOptionNewLibraryContent": "Novi sadržaj dodan",
"NotificationOptionPluginError": "Dodatak otkazao", "NotificationOptionPluginError": "Dodatak zakazao",
"NotificationOptionPluginInstalled": "Dodatak instaliran", "NotificationOptionPluginInstalled": "Dodatak instaliran",
"NotificationOptionPluginUninstalled": "Dodatak uklonjen", "NotificationOptionPluginUninstalled": "Dodatak deinstaliran",
"NotificationOptionPluginUpdateInstalled": "Instalirano ažuriranje za dodatak", "NotificationOptionPluginUpdateInstalled": "Instalirano ažuriranje dodatka",
"NotificationOptionServerRestartRequired": "Potrebno ponovo pokretanje servera", "NotificationOptionServerRestartRequired": "Ponovno pokrenite server",
"NotificationOptionTaskFailed": "Zakazan zadatak nije izvršen", "NotificationOptionTaskFailed": "Greška zakazanog zadatka",
"NotificationOptionUserLockedOut": "Korisnik zaključan", "NotificationOptionUserLockedOut": "Korisnik zaključan",
"NotificationOptionVideoPlayback": "Reprodukcija videa započeta", "NotificationOptionVideoPlayback": "Reprodukcija videa započela",
"NotificationOptionVideoPlaybackStopped": "Reprodukcija videozapisa je zaustavljena", "NotificationOptionVideoPlaybackStopped": "Reprodukcija videa zaustavljena",
"Photos": "Slike", "Photos": "Fotografije",
"Playlists": "Popis za reprodukciju", "Playlists": "Popisi za reprodukciju",
"Plugin": "Dodatak", "Plugin": "Dodatak",
"PluginInstalledWithName": "{0} je instalirano", "PluginInstalledWithName": "{0} je instalirano",
"PluginUninstalledWithName": "{0} je deinstalirano", "PluginUninstalledWithName": "{0} je deinstalirano",
"PluginUpdatedWithName": "{0} je ažurirano", "PluginUpdatedWithName": "{0} je ažurirano",
"ProviderValue": "Pružitelj: {0}", "ProviderValue": "Pružatelj: {0}",
"ScheduledTaskFailedWithName": "{0} neuspjelo", "ScheduledTaskFailedWithName": "{0} neuspjelo",
"ScheduledTaskStartedWithName": "{0} pokrenuto", "ScheduledTaskStartedWithName": "{0} pokrenuto",
"ServerNameNeedsToBeRestarted": "{0} treba biti ponovno pokrenuto", "ServerNameNeedsToBeRestarted": "{0} treba ponovno pokrenuti",
"Shows": "Serije", "Shows": "Serije",
"Songs": "Pjesme", "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}", "SubtitleDownloadFailureForItem": "Titlovi prijevoda nisu preuzeti za {0}",
"SubtitleDownloadFailureFromForItem": "Prijevodi nisu uspješno preuzeti {0} od {1}", "SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}",
"Sync": "Sink.", "Sync": "Sinkronizacija",
"System": "Sistem", "System": "Sustav",
"TvShows": "Serije", "TvShows": "Serije",
"User": "Korisnik", "User": "Korisnik",
"UserCreatedWithName": "Korisnik {0} je stvoren", "UserCreatedWithName": "Korisnik {0} je kreiran",
"UserDeletedWithName": "Korisnik {0} je obrisan", "UserDeletedWithName": "Korisnik {0} je obrisan",
"UserDownloadingItemWithValues": "{0} se preuzima {1}", "UserDownloadingItemWithValues": "{0} preuzima {1}",
"UserLockedOutWithName": "Korisnik {0} je zaključan", "UserLockedOutWithName": "Korisnik {0} je zaključan",
"UserOfflineFromDevice": "{0} se odspojilo od {1}", "UserOfflineFromDevice": "{0} prekinuo vezu od {1}",
"UserOnlineFromDevice": "{0} je online od {1}", "UserOnlineFromDevice": "{0} povezan od {1}",
"UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}", "UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}",
"UserPolicyUpdatedWithName": "Pravila za korisnika su ažurirana za {0}", "UserPolicyUpdatedWithName": "Pravila za korisnika ažurirana su za {0}",
"UserStartedPlayingItemWithValues": "{0} je pokrenuo {1}", "UserStartedPlayingItemWithValues": "{0} je pokrenuo reprodukciju {1} na {2}",
"UserStoppedPlayingItemWithValues": "{0} je zaustavio {1}", "UserStoppedPlayingItemWithValues": "{0} je zavio reprodukciju {1} na {2}",
"ValueHasBeenAddedToLibrary": "{0} je dodano u medijsku biblioteku", "ValueHasBeenAddedToLibrary": "{0} je dodano u medijsku biblioteku",
"ValueSpecialEpisodeName": "Specijal - {0}", "ValueSpecialEpisodeName": "Posebno - {0}",
"VersionNumber": "Verzija {0}", "VersionNumber": "Verzija {0}",
"TaskRefreshLibraryDescription": "Skenira vašu medijsku knjižnicu sa novim datotekama i osvježuje metapodatke.", "TaskRefreshLibraryDescription": "Skenira medijsku biblioteku radi novih datoteka i osvježava metapodatke.",
"TaskRefreshLibrary": "Skeniraj medijsku knjižnicu", "TaskRefreshLibrary": "Skeniraj medijsku biblioteku",
"TaskRefreshChapterImagesDescription": "Stvara sličice za videozapise koji imaju poglavlja.", "TaskRefreshChapterImagesDescription": "Kreira sličice za videozapise koji imaju poglavlja.",
"TaskRefreshChapterImages": "Raspakiraj slike poglavlja", "TaskRefreshChapterImages": "Izdvoji slike poglavlja",
"TaskCleanCacheDescription": "Briše priručne datoteke nepotrebne za sistem.", "TaskCleanCacheDescription": "Briše nepotrebne datoteke iz predmemorije.",
"TaskCleanCache": "Očisti priručnu memoriju", "TaskCleanCache": "Očisti mapu predmemorije",
"TasksApplicationCategory": "Aplikacija", "TasksApplicationCategory": "Aplikacija",
"TasksMaintenanceCategory": "Održavanje", "TasksMaintenanceCategory": "Održavanje",
"TaskDownloadMissingSubtitlesDescription": "Pretraživanje interneta za prijevodima koji nedostaju bazirano na konfiguraciji meta podataka.", "TaskDownloadMissingSubtitlesDescription": "Pretraži Internet za prijevodima koji nedostaju prema konfiguraciji metapodataka.",
"TaskDownloadMissingSubtitles": "Preuzimanje prijevoda koji nedostaju", "TaskDownloadMissingSubtitles": "Preuzmi prijevod koji nedostaje",
"TaskRefreshChannelsDescription": "Osvježava informacije o internet kanalima.", "TaskRefreshChannelsDescription": "Osvježava informacije Internet kanala.",
"TaskRefreshChannels": "Osvježi kanale", "TaskRefreshChannels": "Osvježi kanale",
"TaskCleanTranscodeDescription": "Briše transkodirane fajlove starije od jednog dana.", "TaskCleanTranscodeDescription": "Briše transkodirane datoteke starije od jednog dana.",
"TaskCleanTranscode": "Očisti direktorij za transkodiranje", "TaskCleanTranscode": "Očisti mapu transkodiranja",
"TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su podešeni da se ažuriraju automatski.", "TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su konfigurirani da se ažuriraju automatski.",
"TaskUpdatePlugins": "Ažuriraj dodatke", "TaskUpdatePlugins": "Ažuriraj dodatke",
"TaskRefreshPeopleDescription": "Ažurira meta podatke za glumce i redatelje u vašoj medijskoj biblioteci.", "TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i redatelje u medijskoj biblioteci.",
"TaskRefreshPeople": "Osvježi ljude", "TaskRefreshPeople": "Osvježi osobe",
"TaskCleanLogsDescription": "Briši logove koji su stariji od {0} dana.", "TaskCleanLogsDescription": "Briše zapise dnevnika koji su stariji od {0} dana.",
"TaskCleanLogs": "Očisti direktorij sa logovima", "TaskCleanLogs": "Očisti mapu dnevnika zapisa",
"TasksChannelsCategory": "Internet kanali", "TasksChannelsCategory": "Internet kanali",
"TasksLibraryCategory": "Biblioteka" "TasksLibraryCategory": "Biblioteka",
"TaskCleanActivityLogDescription": "Briše zapise dnevnika aktivnosti starije od navedenog vremena.",
"TaskCleanActivityLog": "Očisti dnevnik aktivnosti"
} }

View File

@ -113,5 +113,7 @@
"TasksChannelsCategory": "Canali su Internet", "TasksChannelsCategory": "Canali su Internet",
"TasksApplicationCategory": "Applicazione", "TasksApplicationCategory": "Applicazione",
"TasksLibraryCategory": "Libreria", "TasksLibraryCategory": "Libreria",
"TasksMaintenanceCategory": "Manutenzione" "TasksMaintenanceCategory": "Manutenzione",
"TaskCleanActivityLog": "Attività di Registro Completate",
"TaskCleanActivityLogDescription": "Elimina gli inserimenti nel registro delle attività più vecchie delletà configurata."
} }

View File

@ -96,7 +96,7 @@
"TaskRefreshLibraryDescription": "メディアライブラリをスキャンして新しいファイルを探し、メタデータをリフレッシュします。", "TaskRefreshLibraryDescription": "メディアライブラリをスキャンして新しいファイルを探し、メタデータをリフレッシュします。",
"TaskRefreshLibrary": "メディアライブラリのスキャン", "TaskRefreshLibrary": "メディアライブラリのスキャン",
"TaskCleanCacheDescription": "不要なキャッシュを消去します。", "TaskCleanCacheDescription": "不要なキャッシュを消去します。",
"TaskCleanCache": "キャッシュの掃除", "TaskCleanCache": "キャッシュを消去",
"TasksChannelsCategory": "ネットチャンネル", "TasksChannelsCategory": "ネットチャンネル",
"TasksApplicationCategory": "アプリケーション", "TasksApplicationCategory": "アプリケーション",
"TasksLibraryCategory": "ライブラリ", "TasksLibraryCategory": "ライブラリ",
@ -112,5 +112,7 @@
"TaskDownloadMissingSubtitlesDescription": "メタデータ構成に基づいて、欠落している字幕をインターネットで検索します。", "TaskDownloadMissingSubtitlesDescription": "メタデータ構成に基づいて、欠落している字幕をインターネットで検索します。",
"TaskRefreshChapterImagesDescription": "チャプターのあるビデオのサムネイルを作成します。", "TaskRefreshChapterImagesDescription": "チャプターのあるビデオのサムネイルを作成します。",
"TaskRefreshChapterImages": "チャプター画像を抽出する", "TaskRefreshChapterImages": "チャプター画像を抽出する",
"TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする" "TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする",
"TaskCleanActivityLogDescription": "設定された期間よりも古いアクティビティの履歴を削除します。",
"TaskCleanActivityLog": "アクティビティの履歴を消去"
} }

View File

@ -27,7 +27,7 @@
"HeaderRecordingGroups": "녹화 그룹", "HeaderRecordingGroups": "녹화 그룹",
"HomeVideos": "홈 비디오", "HomeVideos": "홈 비디오",
"Inherit": "상속", "Inherit": "상속",
"ItemAddedWithName": "{0}가 라이브러리에 추가", "ItemAddedWithName": "{0}가 라이브러리에 추가되었습니다",
"ItemRemovedWithName": "{0}가 라이브러리에서 제거됨", "ItemRemovedWithName": "{0}가 라이브러리에서 제거됨",
"LabelIpAddressValue": "IP 주소: {0}", "LabelIpAddressValue": "IP 주소: {0}",
"LabelRunningTimeValue": "상영 시간: {0}", "LabelRunningTimeValue": "상영 시간: {0}",
@ -113,5 +113,7 @@
"TaskCleanCacheDescription": "시스템에서 더 이상 필요하지 않은 캐시 파일을 삭제합니다.", "TaskCleanCacheDescription": "시스템에서 더 이상 필요하지 않은 캐시 파일을 삭제합니다.",
"TaskCleanCache": "캐시 폴더 청소", "TaskCleanCache": "캐시 폴더 청소",
"TasksChannelsCategory": "인터넷 채널", "TasksChannelsCategory": "인터넷 채널",
"TasksLibraryCategory": "라이브러리" "TasksLibraryCategory": "라이브러리",
"TaskCleanActivityLogDescription": "구성된 기간보다 오래된 활동내역 삭제.",
"TaskCleanActivityLog": "활동내역청소"
} }

View File

@ -113,5 +113,7 @@
"TasksChannelsCategory": "Internet Kanalen", "TasksChannelsCategory": "Internet Kanalen",
"TasksApplicationCategory": "Applicatie", "TasksApplicationCategory": "Applicatie",
"TasksLibraryCategory": "Bibliotheek", "TasksLibraryCategory": "Bibliotheek",
"TasksMaintenanceCategory": "Onderhoud" "TasksMaintenanceCategory": "Onderhoud",
"TaskCleanActivityLogDescription": "Verwijder activiteiten logs ouder dan de ingestelde tijd.",
"TaskCleanActivityLog": "Leeg activiteiten logboek"
} }

View File

@ -113,5 +113,7 @@
"TaskCleanLogsDescription": "Удаляются файлы журнала, возраст которых превышает {0} дн(я/ей).", "TaskCleanLogsDescription": "Удаляются файлы журнала, возраст которых превышает {0} дн(я/ей).",
"TaskRefreshLibraryDescription": "Сканируется медиатека на новые файлы и обновляются метаданные.", "TaskRefreshLibraryDescription": "Сканируется медиатека на новые файлы и обновляются метаданные.",
"TaskRefreshChapterImagesDescription": "Создаются эскизы для видео, которые содержат сцены.", "TaskRefreshChapterImagesDescription": "Создаются эскизы для видео, которые содержат сцены.",
"TaskCleanCacheDescription": "Удаляются файлы кэша, которые больше не нужны системе." "TaskCleanCacheDescription": "Удаляются файлы кэша, которые больше не нужны системе.",
"TaskCleanActivityLogDescription": "Удаляет записи журнала активности старше установленного возраста.",
"TaskCleanActivityLog": "Очистить журнал активности"
} }

View File

@ -3,20 +3,20 @@
"AppDeviceValues": "Aplikacija: {0}, Naprava: {1}", "AppDeviceValues": "Aplikacija: {0}, Naprava: {1}",
"Application": "Aplikacija", "Application": "Aplikacija",
"Artists": "Izvajalci", "Artists": "Izvajalci",
"AuthenticationSucceededWithUserName": "{0} preverjanje pristnosti uspešno", "AuthenticationSucceededWithUserName": "{0} se je uspešno prijavil",
"Books": "Knjige", "Books": "Knjige",
"CameraImageUploadedFrom": "Nova fotografija je bila naložena z {0}", "CameraImageUploadedFrom": "Nova fotografija je bila naložena iz {0}",
"Channels": "Kanali", "Channels": "Kanali",
"ChapterNameValue": "Poglavje {0}", "ChapterNameValue": "Poglavje {0}",
"Collections": "Zbirke", "Collections": "Zbirke",
"DeviceOfflineWithName": "{0} je prekinil povezavo", "DeviceOfflineWithName": "{0} je prekinil povezavo",
"DeviceOnlineWithName": "{0} je povezan", "DeviceOnlineWithName": "{0} je povezan",
"FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}", "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave iz {0}",
"Favorites": "Priljubljeno", "Favorites": "Priljubljeno",
"Folders": "Mape", "Folders": "Mape",
"Genres": "Zvrsti", "Genres": "Zvrsti",
"HeaderAlbumArtists": "Izvajalci albuma", "HeaderAlbumArtists": "Izvajalci albuma",
"HeaderContinueWatching": "Nadaljuj gledanje", "HeaderContinueWatching": "Nadaljuj z ogledom",
"HeaderFavoriteAlbums": "Priljubljeni albumi", "HeaderFavoriteAlbums": "Priljubljeni albumi",
"HeaderFavoriteArtists": "Priljubljeni izvajalci", "HeaderFavoriteArtists": "Priljubljeni izvajalci",
"HeaderFavoriteEpisodes": "Priljubljene epizode", "HeaderFavoriteEpisodes": "Priljubljene epizode",
@ -32,23 +32,23 @@
"LabelIpAddressValue": "IP naslov: {0}", "LabelIpAddressValue": "IP naslov: {0}",
"LabelRunningTimeValue": "Čas trajanja: {0}", "LabelRunningTimeValue": "Čas trajanja: {0}",
"Latest": "Najnovejše", "Latest": "Najnovejše",
"MessageApplicationUpdated": "Jellyfin Server je bil posodobljen", "MessageApplicationUpdated": "Jellyfin strežnik je bil posodobljen",
"MessageApplicationUpdatedTo": "Jellyfin Server je bil posodobljen na {0}", "MessageApplicationUpdatedTo": "Jellyfin strežnik je bil posodobljen na {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Oddelek nastavitve strežnika {0} je bil posodobljen", "MessageNamedServerConfigurationUpdatedWithValue": "Oddelek nastavitev {0} je bil posodobljen",
"MessageServerConfigurationUpdated": "Nastavitve strežnika so bile posodobljene", "MessageServerConfigurationUpdated": "Nastavitve strežnika so bile posodobljene",
"MixedContent": "Razne vsebine", "MixedContent": "Mešane vsebine",
"Movies": "Filmi", "Movies": "Filmi",
"Music": "Glasba", "Music": "Glasba",
"MusicVideos": "Glasbeni videi", "MusicVideos": "Glasbeni videi",
"NameInstallFailed": "{0} namestitev neuspešna", "NameInstallFailed": "{0} namestitev neuspešna",
"NameSeasonNumber": "Sezona {0}", "NameSeasonNumber": "Sezona {0}",
"NameSeasonUnknown": "Season neznana", "NameSeasonUnknown": "Neznana sezona",
"NewVersionIsAvailable": "Nova različica Jellyfin strežnika je na voljo za prenos.", "NewVersionIsAvailable": "Nova različica Jellyfin strežnika je na voljo za prenos.",
"NotificationOptionApplicationUpdateAvailable": "Posodobitev aplikacije je na voljo", "NotificationOptionApplicationUpdateAvailable": "Posodobitev aplikacije je na voljo",
"NotificationOptionApplicationUpdateInstalled": "Posodobitev aplikacije je bila nameščena", "NotificationOptionApplicationUpdateInstalled": "Posodobitev aplikacije je bila nameščena",
"NotificationOptionAudioPlayback": "Predvajanje zvoka začeto", "NotificationOptionAudioPlayback": "Predvajanje zvoka se je začelo",
"NotificationOptionAudioPlaybackStopped": "Predvajanje zvoka zaustavljeno", "NotificationOptionAudioPlaybackStopped": "Predvajanje zvoka se je ustavilo",
"NotificationOptionCameraImageUploaded": "Posnetek kamere naložen", "NotificationOptionCameraImageUploaded": "Fotografija naložena",
"NotificationOptionInstallationFailed": "Namestitev neuspešna", "NotificationOptionInstallationFailed": "Namestitev neuspešna",
"NotificationOptionNewLibraryContent": "Nove vsebine dodane", "NotificationOptionNewLibraryContent": "Nove vsebine dodane",
"NotificationOptionPluginError": "Napaka dodatka", "NotificationOptionPluginError": "Napaka dodatka",
@ -56,41 +56,41 @@
"NotificationOptionPluginUninstalled": "Dodatek odstranjen", "NotificationOptionPluginUninstalled": "Dodatek odstranjen",
"NotificationOptionPluginUpdateInstalled": "Posodobitev dodatka nameščena", "NotificationOptionPluginUpdateInstalled": "Posodobitev dodatka nameščena",
"NotificationOptionServerRestartRequired": "Potreben je ponovni zagon strežnika", "NotificationOptionServerRestartRequired": "Potreben je ponovni zagon strežnika",
"NotificationOptionTaskFailed": "Razporejena naloga neuspešna", "NotificationOptionTaskFailed": "Načrtovano opravilo neuspešno",
"NotificationOptionUserLockedOut": "Uporabnik zaklenjen", "NotificationOptionUserLockedOut": "Uporabnik zaklenjen",
"NotificationOptionVideoPlayback": "Predvajanje videa se je začelo", "NotificationOptionVideoPlayback": "Predvajanje videa se je začelo",
"NotificationOptionVideoPlaybackStopped": "Predvajanje videa se je ustavilo", "NotificationOptionVideoPlaybackStopped": "Predvajanje videa se je ustavilo",
"Photos": "Fotografije", "Photos": "Fotografije",
"Playlists": "Seznami predvajanja", "Playlists": "Seznami predvajanja",
"Plugin": "Plugin", "Plugin": "Dodatek",
"PluginInstalledWithName": "{0} je bil nameščen", "PluginInstalledWithName": "{0} je bil nameščen",
"PluginUninstalledWithName": "{0} je bil odstranjen", "PluginUninstalledWithName": "{0} je bil odstranjen",
"PluginUpdatedWithName": "{0} je bil posodobljen", "PluginUpdatedWithName": "{0} je bil posodobljen",
"ProviderValue": "Provider: {0}", "ProviderValue": "Ponudnik: {0}",
"ScheduledTaskFailedWithName": "{0} ni uspelo", "ScheduledTaskFailedWithName": "{0} ni uspelo",
"ScheduledTaskStartedWithName": "{0} začeto", "ScheduledTaskStartedWithName": "{0} začeto",
"ServerNameNeedsToBeRestarted": "{0} mora biti ponovno zagnan", "ServerNameNeedsToBeRestarted": "{0} mora biti ponovno zagnan",
"Shows": "Serije", "Shows": "Serije",
"Songs": "Pesmi", "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}", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "Neuspešen prenos podnapisov iz {0} za {1}", "SubtitleDownloadFailureFromForItem": "Neuspešen prenos podnapisov iz {0} za {1}",
"Sync": "Sinhroniziraj", "Sync": "Sinhroniziraj",
"System": "System", "System": "Sistem",
"TvShows": "TV serije", "TvShows": "TV serije",
"User": "User", "User": "Uporabnik",
"UserCreatedWithName": "Uporabnik {0} je bil ustvarjen", "UserCreatedWithName": "Uporabnik {0} je bil ustvarjen",
"UserDeletedWithName": "Uporabnik {0} je bil izbrisan", "UserDeletedWithName": "Uporabnik {0} je bil izbrisan",
"UserDownloadingItemWithValues": "{0} prenaša {1}", "UserDownloadingItemWithValues": "{0} prenaša {1}",
"UserLockedOutWithName": "Uporabnik {0} je bil zaklenjen", "UserLockedOutWithName": "Uporabnik {0} je bil zaklenjen",
"UserOfflineFromDevice": "{0} je prekinil povezavo z {1}", "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", "UserPasswordChangedWithName": "Geslo za uporabnika {0} je bilo spremenjeno",
"UserPolicyUpdatedWithName": "Pravilnik uporabe je bil posodobljen za uporabnika {0}", "UserPolicyUpdatedWithName": "Pravilnik uporabe je bil posodobljen za uporabnika {0}",
"UserStartedPlayingItemWithValues": "{0} predvaja {1} na {2}", "UserStartedPlayingItemWithValues": "{0} predvaja {1} na {2}",
"UserStoppedPlayingItemWithValues": "{0} je nehal predvajati {1} na {2}", "UserStoppedPlayingItemWithValues": "{0} je nehal predvajati {1} na {2}",
"ValueHasBeenAddedToLibrary": "{0} je bil dodan vaši knjižnici", "ValueHasBeenAddedToLibrary": "{0} je bil dodan vaši knjižnici",
"ValueSpecialEpisodeName": "Poseben - {0}", "ValueSpecialEpisodeName": "Posebna - {0}",
"VersionNumber": "Različica {0}", "VersionNumber": "Različica {0}",
"TaskDownloadMissingSubtitles": "Prenesi manjkajoče podnapise", "TaskDownloadMissingSubtitles": "Prenesi manjkajoče podnapise",
"TaskRefreshChannelsDescription": "Osveži podatke spletnih kanalov.", "TaskRefreshChannelsDescription": "Osveži podatke spletnih kanalov.",
@ -102,7 +102,7 @@
"TaskRefreshPeopleDescription": "Osveži metapodatke za igralce in režiserje v vaši knjižnici.", "TaskRefreshPeopleDescription": "Osveži metapodatke za igralce in režiserje v vaši knjižnici.",
"TaskRefreshPeople": "Osveži osebe", "TaskRefreshPeople": "Osveži osebe",
"TaskCleanLogsDescription": "Izbriše dnevniške datoteke starejše od {0} dni.", "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.", "TaskRefreshLibraryDescription": "Preišče vašo knjižnico za nove datoteke in osveži metapodatke.",
"TaskRefreshLibrary": "Preišči knjižnico predstavnosti", "TaskRefreshLibrary": "Preišči knjižnico predstavnosti",
"TaskRefreshChapterImagesDescription": "Ustvari sličice za poglavja videoposnetkov.", "TaskRefreshChapterImagesDescription": "Ustvari sličice za poglavja videoposnetkov.",

View File

@ -112,5 +112,5 @@
"Artists": "Artistë", "Artists": "Artistë",
"Application": "Aplikacioni", "Application": "Aplikacioni",
"AppDeviceValues": "Aplikacioni: {0}, Pajisja: {1}", "AppDeviceValues": "Aplikacioni: {0}, Pajisja: {1}",
"Albums": "Albumet" "Albums": "Albume"
} }

View File

@ -101,7 +101,7 @@
"UserOfflineFromDevice": "{0} இலிருந்து {1} துண்டிக்கப்பட்டுள்ளது", "UserOfflineFromDevice": "{0} இலிருந்து {1} துண்டிக்கப்பட்டுள்ளது",
"SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0} இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன", "SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0} இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
"TaskDownloadMissingSubtitlesDescription": "மீத்தரவு உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.", "TaskDownloadMissingSubtitlesDescription": "மீத்தரவு உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
"TaskCleanTranscodeDescription": "டிரான்ஸ்கோட் கோப்புகளை ஒரு நாளுக்கு மேல் பழையதாக நீக்குகிறது.", "TaskCleanTranscodeDescription": "ஒரு நாளைக்கு மேற்பட்ட பழைய டிரான்ஸ்கோட் கோப்புகளை நீக்குகிறது.",
"TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.", "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",
"TaskRefreshPeopleDescription": "உங்கள் ஊடக நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மீத்தரவை புதுப்பிக்கும்.", "TaskRefreshPeopleDescription": "உங்கள் ஊடக நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மீத்தரவை புதுப்பிக்கும்.",
"TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.", "TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.",

View File

@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi", "CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi",
"Channels": "Kanallar", "Channels": "Kanallar",
"ChapterNameValue": "Bölüm {0}", "ChapterNameValue": "Bölüm {0}",
"Collections": "Koleksiyonlar", "Collections": "Koleksiyon",
"DeviceOfflineWithName": "{0} bağlantısı kesildi", "DeviceOfflineWithName": "{0} bağlantısı kesildi",
"DeviceOnlineWithName": "{0} bağlı", "DeviceOnlineWithName": "{0} bağlı",
"FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu", "FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu",
@ -23,7 +23,7 @@
"HeaderFavoriteShows": "Favori Diziler", "HeaderFavoriteShows": "Favori Diziler",
"HeaderFavoriteSongs": "Favori Şarkılar", "HeaderFavoriteSongs": "Favori Şarkılar",
"HeaderLiveTV": "Canlı TV", "HeaderLiveTV": "Canlı TV",
"HeaderNextUp": "Sonraki hafta", "HeaderNextUp": "Gelecek Hafta",
"HeaderRecordingGroups": "Kayıt Grupları", "HeaderRecordingGroups": "Kayıt Grupları",
"HomeVideos": "Ev videoları", "HomeVideos": "Ev videoları",
"Inherit": "Devral", "Inherit": "Devral",
@ -113,5 +113,7 @@
"TaskRefreshLibrary": "Medya Kütüphanesini Tara", "TaskRefreshLibrary": "Medya Kütüphanesini Tara",
"TaskRefreshChapterImagesDescription": "Sahnelere ayrılmış videolar için küçük resimler oluştur.", "TaskRefreshChapterImagesDescription": "Sahnelere ayrılmış videolar için küçük resimler oluştur.",
"TaskRefreshChapterImages": "Bölüm Resimlerini Çıkar", "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."
} }

View File

@ -112,5 +112,7 @@
"Books": "Sách", "Books": "Sách",
"AuthenticationSucceededWithUserName": "{0} xác thực thành công", "AuthenticationSucceededWithUserName": "{0} xác thực thành công",
"Application": "Ứng Dụ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"
} }

View File

@ -113,5 +113,7 @@
"TaskCleanCacheDescription": "删除系统不再需要的缓存文件。", "TaskCleanCacheDescription": "删除系统不再需要的缓存文件。",
"TaskCleanCache": "清理缓存目录", "TaskCleanCache": "清理缓存目录",
"TasksApplicationCategory": "应用程序", "TasksApplicationCategory": "应用程序",
"TasksMaintenanceCategory": "维护" "TasksMaintenanceCategory": "维护",
"TaskCleanActivityLog": "清理程序日志",
"TaskCleanActivityLogDescription": "删除早于设置时间的活动日志条目。"
} }

View File

@ -112,5 +112,7 @@
"TaskRefreshChapterImagesDescription": "為有章節的影片建立縮圖。", "TaskRefreshChapterImagesDescription": "為有章節的影片建立縮圖。",
"TasksChannelsCategory": "網路頻道", "TasksChannelsCategory": "網路頻道",
"TasksApplicationCategory": "應用程式", "TasksApplicationCategory": "應用程式",
"TasksMaintenanceCategory": "維修" "TasksMaintenanceCategory": "維護",
"TaskCleanActivityLogDescription": "刪除超過所設時間的活動紀錄。",
"TaskCleanActivityLog": "清除活動紀錄"
} }

View File

@ -703,7 +703,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
MaxRuntimeTicks = info.MaxRuntimeTicks MaxRuntimeTicks = info.MaxRuntimeTicks
}; };
if (info.Type.Equals(typeof(DailyTrigger).Name, StringComparison.OrdinalIgnoreCase)) if (info.Type.Equals(nameof(DailyTrigger), StringComparison.OrdinalIgnoreCase))
{ {
if (!info.TimeOfDayTicks.HasValue) if (!info.TimeOfDayTicks.HasValue)
{ {
@ -717,7 +717,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
}; };
} }
if (info.Type.Equals(typeof(WeeklyTrigger).Name, StringComparison.OrdinalIgnoreCase)) if (info.Type.Equals(nameof(WeeklyTrigger), StringComparison.OrdinalIgnoreCase))
{ {
if (!info.TimeOfDayTicks.HasValue) if (!info.TimeOfDayTicks.HasValue)
{ {
@ -737,7 +737,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
}; };
} }
if (info.Type.Equals(typeof(IntervalTrigger).Name, StringComparison.OrdinalIgnoreCase)) if (info.Type.Equals(nameof(IntervalTrigger), StringComparison.OrdinalIgnoreCase))
{ {
if (!info.IntervalTicks.HasValue) if (!info.IntervalTicks.HasValue)
{ {
@ -751,7 +751,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
}; };
} }
if (info.Type.Equals(typeof(StartupTrigger).Name, StringComparison.OrdinalIgnoreCase)) if (info.Type.Equals(nameof(StartupTrigger), StringComparison.OrdinalIgnoreCase))
{ {
return new StartupTrigger(); return new StartupTrigger();
} }

View File

@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Tasks;
namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{
/// <summary>
/// Deletes old activity log entries.
/// </summary>
public class CleanActivityLogTask : IScheduledTask, IConfigurableScheduledTask
{
private readonly ILocalizationManager _localization;
private readonly IActivityManager _activityManager;
private readonly IServerConfigurationManager _serverConfigurationManager;
/// <summary>
/// Initializes a new instance of the <see cref="CleanActivityLogTask"/> class.
/// </summary>
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
/// <param name="activityManager">Instance of the <see cref="IActivityManager"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
public CleanActivityLogTask(
ILocalizationManager localization,
IActivityManager activityManager,
IServerConfigurationManager serverConfigurationManager)
{
_localization = localization;
_activityManager = activityManager;
_serverConfigurationManager = serverConfigurationManager;
}
/// <inheritdoc />
public string Name => _localization.GetLocalizedString("TaskCleanActivityLog");
/// <inheritdoc />
public string Key => "CleanActivityLog";
/// <inheritdoc />
public string Description => _localization.GetLocalizedString("TaskCleanActivityLogDescription");
/// <inheritdoc />
public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
/// <inheritdoc />
public bool IsHidden => false;
/// <inheritdoc />
public bool IsEnabled => true;
/// <inheritdoc />
public bool IsLogged => true;
/// <inheritdoc />
public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
{
var retentionDays = _serverConfigurationManager.Configuration.ActivityLogRetentionDays;
if (!retentionDays.HasValue || retentionDays <= 0)
{
throw new Exception($"Activity Log Retention days must be at least 0. Currently: {retentionDays}");
}
var startDate = DateTime.UtcNow.AddDays(retentionDays.Value * -1);
return _activityManager.CleanAsync(startDate);
}
/// <inheritdoc />
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
return Enumerable.Empty<TaskTriggerInfo>();
}
}
}

View File

@ -104,6 +104,6 @@ namespace Emby.Server.Implementations
public string InternalMetadataPath { get; set; } public string InternalMetadataPath { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public string VirtualInternalMetadataPath { get; } = "%MetadataPath%"; public string VirtualInternalMetadataPath => "%MetadataPath%";
} }
} }

View File

@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.TV
.GetItemList( .GetItemList(
new InternalItemsQuery(user) new InternalItemsQuery(user)
{ {
IncludeItemTypes = new[] { typeof(Episode).Name }, IncludeItemTypes = new[] { nameof(Episode) },
OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) }, OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) },
SeriesPresentationUniqueKey = presentationUniqueKey, SeriesPresentationUniqueKey = presentationUniqueKey,
Limit = limit, Limit = limit,
@ -214,7 +214,7 @@ namespace Emby.Server.Implementations.TV
{ {
AncestorWithPresentationUniqueKey = null, AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey, SeriesPresentationUniqueKey = seriesKey,
IncludeItemTypes = new[] { typeof(Episode).Name }, IncludeItemTypes = new[] { nameof(Episode) },
OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) }, OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) },
Limit = 1, Limit = 1,
IsPlayed = false, IsPlayed = false,

View File

@ -10,19 +10,21 @@ using System.Runtime.Serialization;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Events;
using MediaBrowser.Common; using MediaBrowser.Common;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates; using MediaBrowser.Common.Updates;
using MediaBrowser.Common.System; using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Events.Updates;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Updates; using MediaBrowser.Model.Updates;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MediaBrowser.Model.System;
namespace Emby.Server.Implementations.Updates namespace Emby.Server.Implementations.Updates
{ {
@ -36,6 +38,7 @@ namespace Emby.Server.Implementations.Updates
/// </summary> /// </summary>
private readonly ILogger<InstallationManager> _logger; private readonly ILogger<InstallationManager> _logger;
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly IEventManager _eventManager;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly IJsonSerializer _jsonSerializer; private readonly IJsonSerializer _jsonSerializer;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
@ -45,7 +48,7 @@ namespace Emby.Server.Implementations.Updates
/// Gets the application host. /// Gets the application host.
/// </summary> /// </summary>
/// <value>The application host.</value> /// <value>The application host.</value>
private readonly IApplicationHost _applicationHost; private readonly IServerApplicationHost _applicationHost;
private readonly IZipClient _zipClient; private readonly IZipClient _zipClient;
@ -63,25 +66,22 @@ namespace Emby.Server.Implementations.Updates
public InstallationManager( public InstallationManager(
ILogger<InstallationManager> logger, ILogger<InstallationManager> logger,
IApplicationHost appHost, IServerApplicationHost appHost,
IApplicationPaths appPaths, IApplicationPaths appPaths,
IEventManager eventManager,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
IJsonSerializer jsonSerializer, IJsonSerializer jsonSerializer,
IServerConfigurationManager config, IServerConfigurationManager config,
IFileSystem fileSystem, IFileSystem fileSystem,
IZipClient zipClient) IZipClient zipClient)
{ {
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
_currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>(); _currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>();
_completedInstallationsInternal = new ConcurrentBag<InstallationInfo>(); _completedInstallationsInternal = new ConcurrentBag<InstallationInfo>();
_logger = logger; _logger = logger;
_applicationHost = appHost; _applicationHost = appHost;
_appPaths = appPaths; _appPaths = appPaths;
_eventManager = eventManager;
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_jsonSerializer = jsonSerializer; _jsonSerializer = jsonSerializer;
_config = config; _config = config;
@ -89,27 +89,6 @@ namespace Emby.Server.Implementations.Updates
_zipClient = zipClient; _zipClient = zipClient;
} }
/// <inheritdoc />
public event EventHandler<InstallationInfo> PackageInstalling;
/// <inheritdoc />
public event EventHandler<InstallationInfo> PackageInstallationCompleted;
/// <inheritdoc />
public event EventHandler<InstallationFailedEventArgs> PackageInstallationFailed;
/// <inheritdoc />
public event EventHandler<InstallationInfo> PackageInstallationCancelled;
/// <inheritdoc />
public event EventHandler<IPlugin> PluginUninstalled;
/// <inheritdoc />
public event EventHandler<InstallationInfo> PluginUpdated;
/// <inheritdoc />
public event EventHandler<InstallationInfo> PluginInstalled;
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal; public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
@ -237,7 +216,8 @@ namespace Emby.Server.Implementations.Updates
private IEnumerable<InstallationInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog) 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 compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version);
var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version); var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version);
@ -268,11 +248,11 @@ namespace Emby.Server.Implementations.Updates
var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, innerCancellationTokenSource.Token).Token; var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, innerCancellationTokenSource.Token).Token;
PackageInstalling?.Invoke(this, package); await _eventManager.PublishAsync(new PluginInstallingEventArgs(package)).ConfigureAwait(false);
try try
{ {
await InstallPackageInternal(package, linkedToken).ConfigureAwait(false); var isUpdate = await InstallPackageInternal(package, linkedToken).ConfigureAwait(false);
lock (_currentInstallationsLock) lock (_currentInstallationsLock)
{ {
@ -280,8 +260,11 @@ namespace Emby.Server.Implementations.Updates
} }
_completedInstallationsInternal.Add(package); _completedInstallationsInternal.Add(package);
await _eventManager.PublishAsync(isUpdate
? (GenericEventArgs<InstallationInfo>)new PluginUpdatedEventArgs(package)
: new PluginInstalledEventArgs(package)).ConfigureAwait(false);
PackageInstallationCompleted?.Invoke(this, package); _applicationHost.NotifyPendingRestart();
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@ -292,7 +275,7 @@ namespace Emby.Server.Implementations.Updates
_logger.LogInformation("Package installation cancelled: {0} {1}", package.Name, package.Version); _logger.LogInformation("Package installation cancelled: {0} {1}", package.Name, package.Version);
PackageInstallationCancelled?.Invoke(this, package); await _eventManager.PublishAsync(new PluginInstallationCancelledEventArgs(package)).ConfigureAwait(false);
throw; throw;
} }
@ -305,11 +288,11 @@ namespace Emby.Server.Implementations.Updates
_currentInstallations.Remove(tuple); _currentInstallations.Remove(tuple);
} }
PackageInstallationFailed?.Invoke(this, new InstallationFailedEventArgs await _eventManager.PublishAsync(new InstallationFailedEventArgs
{ {
InstallationInfo = package, InstallationInfo = package,
Exception = ex Exception = ex
}); }).ConfigureAwait(false);
throw; throw;
} }
@ -326,7 +309,7 @@ namespace Emby.Server.Implementations.Updates
/// <param name="package">The package.</param> /// <param name="package">The package.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns><see cref="Task" />.</returns> /// <returns><see cref="Task" />.</returns>
private async Task InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken) private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
{ {
// Set last update time if we were installed before // Set last update time if we were installed before
IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => p.Id == package.Guid) IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => p.Id == package.Guid)
@ -336,20 +319,9 @@ namespace Emby.Server.Implementations.Updates
await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false); await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
// Do plugin-specific processing // Do plugin-specific processing
if (plugin == null) _logger.LogInformation(plugin == null ? "New plugin installed: {0} {1}" : "Plugin updated: {0} {1}", package.Name, package.Version);
{
_logger.LogInformation("New plugin installed: {0} {1}", package.Name, package.Version);
PluginInstalled?.Invoke(this, package); return plugin != null;
}
else
{
_logger.LogInformation("Plugin updated: {0} {1}", package.Name, package.Version);
PluginUpdated?.Invoke(this, package);
}
_applicationHost.NotifyPendingRestart();
} }
private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken) private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken)
@ -467,7 +439,7 @@ namespace Emby.Server.Implementations.Updates
_config.SaveConfiguration(); _config.SaveConfiguration();
} }
PluginUninstalled?.Invoke(this, plugin); _eventManager.Publish(new PluginUninstalledEventArgs(plugin));
_applicationHost.NotifyPendingRestart(); _applicationHost.NotifyPendingRestart();
} }

View File

@ -50,6 +50,13 @@ namespace Jellyfin.Api.Auth
bool localAccessOnly = false, bool localAccessOnly = false,
bool requiredDownloadPermission = false) bool requiredDownloadPermission = false)
{ {
// ApiKey is currently global admin, always allow.
var isApiKey = ClaimHelpers.GetIsApiKey(claimsPrincipal);
if (isApiKey)
{
return true;
}
// Ensure claim has userId. // Ensure claim has userId.
var userId = ClaimHelpers.GetUserId(claimsPrincipal); var userId = ClaimHelpers.GetUserId(claimsPrincipal);
if (!userId.HasValue) if (!userId.HasValue)

View File

@ -1,10 +1,10 @@
using System.Globalization; using System.Globalization;
using System.Security.Authentication;
using System.Security.Claims; using System.Security.Claims;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -43,24 +43,23 @@ namespace Jellyfin.Api.Auth
try try
{ {
var authorizationInfo = _authService.Authenticate(Request); 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()); role = UserRoles.Administrator;
// TODO return when legacy API is removed.
// Don't spam the log with "Invalid User"
// return Task.FromResult(AuthenticateResult.Fail("Invalid user"));
} }
var claims = new[] var claims = new[]
{ {
new Claim(ClaimTypes.Name, authorizationInfo.User.Username), new Claim(ClaimTypes.Name, authorizationInfo.User?.Username ?? string.Empty),
new Claim(ClaimTypes.Role, authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User), new Claim(ClaimTypes.Role, role),
new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)), new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)),
new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId), new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId),
new Claim(InternalClaimTypes.Device, authorizationInfo.Device), new Claim(InternalClaimTypes.Device, authorizationInfo.Device),
new Claim(InternalClaimTypes.Client, authorizationInfo.Client), new Claim(InternalClaimTypes.Client, authorizationInfo.Client),
new Claim(InternalClaimTypes.Version, authorizationInfo.Version), new Claim(InternalClaimTypes.Version, authorizationInfo.Version),
new Claim(InternalClaimTypes.Token, authorizationInfo.Token), new Claim(InternalClaimTypes.Token, authorizationInfo.Token),
new Claim(InternalClaimTypes.IsApiKey, authorizationInfo.IsApiKey.ToString(CultureInfo.InvariantCulture))
}; };
var identity = new ClaimsIdentity(claims, Scheme.Name); var identity = new ClaimsIdentity(claims, Scheme.Name);

View File

@ -29,13 +29,15 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement) protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement)
{ {
var validated = ValidateClaims(context.User); var validated = ValidateClaims(context.User);
if (!validated) if (validated)
{
context.Succeed(requirement);
}
else
{ {
context.Fail(); context.Fail();
return Task.CompletedTask;
} }
context.Succeed(requirement);
return Task.CompletedTask; return Task.CompletedTask;
} }
} }

View File

@ -29,13 +29,15 @@ namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement) protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement)
{ {
var validated = ValidateClaims(context.User, ignoreSchedule: true); var validated = ValidateClaims(context.User, ignoreSchedule: true);
if (!validated) if (validated)
{
context.Succeed(requirement);
}
else
{ {
context.Fail(); context.Fail();
return Task.CompletedTask;
} }
context.Succeed(requirement);
return Task.CompletedTask; return Task.CompletedTask;
} }
} }

View File

@ -29,13 +29,13 @@ namespace Jellyfin.Api.Auth.LocalAccessPolicy
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessRequirement requirement) protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessRequirement requirement)
{ {
var validated = ValidateClaims(context.User, localAccessOnly: true); var validated = ValidateClaims(context.User, localAccessOnly: true);
if (!validated) if (validated)
{ {
context.Fail(); context.Succeed(requirement);
} }
else else
{ {
context.Succeed(requirement); context.Fail();
} }
return Task.CompletedTask; return Task.CompletedTask;

View File

@ -34,5 +34,10 @@
/// Token. /// Token.
/// </summary> /// </summary>
public const string Token = "Jellyfin-Token"; public const string Token = "Jellyfin-Token";
/// <summary>
/// Is Api Key.
/// </summary>
public const string IsApiKey = "Jellyfin-IsApiKey";
} }
} }

View File

@ -1,135 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// The albums controller.
/// </summary>
[Route("")]
public class AlbumsController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
private readonly ILibraryManager _libraryManager;
private readonly IDtoService _dtoService;
/// <summary>
/// Initializes a new instance of the <see cref="AlbumsController"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
public AlbumsController(
IUserManager userManager,
ILibraryManager libraryManager,
IDtoService dtoService)
{
_userManager = userManager;
_libraryManager = libraryManager;
_dtoService = dtoService;
}
/// <summary>
/// Finds albums similar to a given album.
/// </summary>
/// <param name="albumId">The album id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="excludeArtistIds">Optional. Ids of artists to exclude.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <response code="200">Similar albums returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with similar albums.</returns>
[HttpGet("Albums/{albumId}/Similar")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarAlbums(
[FromRoute, Required] string albumId,
[FromQuery] Guid? userId,
[FromQuery] string? excludeArtistIds,
[FromQuery] int? limit)
{
var dtoOptions = new DtoOptions().AddClientFields(Request);
return SimilarItemsHelper.GetSimilarItemsResult(
dtoOptions,
_userManager,
_libraryManager,
_dtoService,
userId,
albumId,
excludeArtistIds,
limit,
new[] { typeof(MusicAlbum) },
GetAlbumSimilarityScore);
}
/// <summary>
/// Finds artists similar to a given artist.
/// </summary>
/// <param name="artistId">The artist id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="excludeArtistIds">Optional. Ids of artists to exclude.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <response code="200">Similar artists returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with similar artists.</returns>
[HttpGet("Artists/{artistId}/Similar")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarArtists(
[FromRoute, Required] string artistId,
[FromQuery] Guid? userId,
[FromQuery] string? excludeArtistIds,
[FromQuery] int? limit)
{
var dtoOptions = new DtoOptions().AddClientFields(Request);
return SimilarItemsHelper.GetSimilarItemsResult(
dtoOptions,
_userManager,
_libraryManager,
_dtoService,
userId,
artistId,
excludeArtistIds,
limit,
new[] { typeof(MusicArtist) },
SimilarItemsHelper.GetSimiliarityScore);
}
/// <summary>
/// Gets a similairty score of two albums.
/// </summary>
/// <param name="item1">The first item.</param>
/// <param name="item1People">The item1 people.</param>
/// <param name="allPeople">All people.</param>
/// <param name="item2">The second item.</param>
/// <returns>System.Int32.</returns>
private int GetAlbumSimilarityScore(BaseItem item1, List<PersonInfo> item1People, List<PersonInfo> allPeople, BaseItem item2)
{
var points = SimilarItemsHelper.GetSimiliarityScore(item1, item1People, allPeople, item2);
var album1 = (MusicAlbum)item1;
var album2 = (MusicAlbum)item2;
var artists1 = album1
.GetAllArtists()
.DistinctNames()
.ToList();
var artists2 = new HashSet<string>(
album2.GetAllArtists().DistinctNames(),
StringComparer.OrdinalIgnoreCase);
return points + artists1.Where(artists2.Contains).Sum(i => 5);
}
}
}

View File

@ -4,11 +4,13 @@ using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -51,7 +53,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">Optional. Search term.</param> /// <param name="searchTerm">Optional. Search term.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> /// <param name="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="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="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="filters">Optional. Specify additional filters to apply.</param>
@ -86,10 +88,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery] string? includeItemTypes,
[FromQuery] ItemFilter[] filters, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes, [FromQuery] string? mediaTypes,
[FromQuery] string? genres, [FromQuery] string? genres,
@ -99,7 +101,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? years, [FromQuery] string? years,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person, [FromQuery] string? person,
[FromQuery] string? personIds, [FromQuery] string? personIds,
[FromQuery] string? personTypes, [FromQuery] string? personTypes,
@ -112,8 +114,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages = true, [FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true) [FromQuery] bool enableTotalRecordCount = true)
{ {
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@ -145,9 +146,9 @@ namespace Jellyfin.Api.Controllers
NameLessThan = nameLessThan, NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith, NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater, NameStartsWithOrGreater = nameStartsWithOrGreater,
Tags = RequestHelpers.Split(tags, ',', true), Tags = RequestHelpers.Split(tags, '|', true),
OfficialRatings = RequestHelpers.Split(officialRatings, ',', true), OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
Genres = RequestHelpers.Split(genres, ',', true), Genres = RequestHelpers.Split(genres, '|', true),
GenreIds = RequestHelpers.GetGuids(genreIds), GenreIds = RequestHelpers.GetGuids(genreIds),
StudioIds = RequestHelpers.GetGuids(studioIds), StudioIds = RequestHelpers.GetGuids(studioIds),
Person = person, Person = person,
@ -260,7 +261,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">Optional. Search term.</param> /// <param name="searchTerm">Optional. Search term.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> /// <param name="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="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="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="filters">Optional. Specify additional filters to apply.</param>
@ -295,10 +296,10 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery] string? includeItemTypes,
[FromQuery] ItemFilter[] filters, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes, [FromQuery] string? mediaTypes,
[FromQuery] string? genres, [FromQuery] string? genres,
@ -308,7 +309,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? years, [FromQuery] string? years,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person, [FromQuery] string? person,
[FromQuery] string? personIds, [FromQuery] string? personIds,
[FromQuery] string? personTypes, [FromQuery] string? personTypes,
@ -321,8 +322,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages = true, [FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true) [FromQuery] bool enableTotalRecordCount = true)
{ {
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@ -354,9 +354,9 @@ namespace Jellyfin.Api.Controllers
NameLessThan = nameLessThan, NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith, NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater, NameStartsWithOrGreater = nameStartsWithOrGreater,
Tags = RequestHelpers.Split(tags, ',', true), Tags = RequestHelpers.Split(tags, '|', true),
OfficialRatings = RequestHelpers.Split(officialRatings, ',', true), OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
Genres = RequestHelpers.Split(genres, ',', true), Genres = RequestHelpers.Split(genres, '|', true),
GenreIds = RequestHelpers.GetGuids(genreIds), GenreIds = RequestHelpers.GetGuids(genreIds),
StudioIds = RequestHelpers.GetGuids(studioIds), StudioIds = RequestHelpers.GetGuids(studioIds),
Person = person, Person = person,

View File

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -107,7 +108,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="sortOrder">Optional. Sort Order - Ascending,Descending.</param> /// <param name="sortOrder">Optional. Sort Order - Ascending,Descending.</param>
/// <param name="filters">Optional. Specify additional filters to apply.</param> /// <param name="filters">Optional. Specify additional filters to apply.</param>
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param> /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <response code="200">Channel items returned.</response> /// <response code="200">Channel items returned.</response>
/// <returns> /// <returns>
/// A <see cref="Task"/> representing the request to get the channel items. /// A <see cref="Task"/> representing the request to get the channel items.
@ -121,9 +122,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? sortOrder, [FromQuery] string? sortOrder,
[FromQuery] ItemFilter[] filters, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] string? sortBy, [FromQuery] string? sortBy,
[FromQuery] string? fields) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
{ {
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value) ? _userManager.GetUserById(userId.Value)
@ -136,8 +137,7 @@ namespace Jellyfin.Api.Controllers
ChannelIds = new[] { channelId }, ChannelIds = new[] { channelId },
ParentId = folderId ?? Guid.Empty, ParentId = folderId ?? Guid.Empty,
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder), OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
DtoOptions = new DtoOptions() DtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
}; };
foreach (var filter in filters) foreach (var filter in filters)
@ -184,7 +184,7 @@ namespace Jellyfin.Api.Controllers
/// <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="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="limit">Optional. The maximum number of records to return.</param>
/// <param name="filters">Optional. Specify additional filters to apply.</param> /// <param name="filters">Optional. Specify additional filters to apply.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="channelIds">Optional. Specify one or more channel id's, comma delimited.</param> /// <param name="channelIds">Optional. Specify one or more channel id's, comma delimited.</param>
/// <response code="200">Latest channel items returned.</response> /// <response code="200">Latest channel items returned.</response>
/// <returns> /// <returns>
@ -196,8 +196,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] ItemFilter[] filters, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? channelIds) [FromQuery] string? channelIds)
{ {
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
@ -213,8 +213,7 @@ namespace Jellyfin.Api.Controllers
.Where(i => !string.IsNullOrWhiteSpace(i)) .Where(i => !string.IsNullOrWhiteSpace(i))
.Select(i => new Guid(i)) .Select(i => new Guid(i))
.ToArray(), .ToArray(),
DtoOptions = new DtoOptions() DtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
}; };
foreach (var filter in filters) foreach (var filter in filters)

View File

@ -83,14 +83,14 @@ namespace Jellyfin.Api.Controllers
/// Adds items to a collection. /// Adds items to a collection.
/// </summary> /// </summary>
/// <param name="collectionId">The collection id.</param> /// <param name="collectionId">The collection id.</param>
/// <param name="itemIds">Item ids, comma delimited.</param> /// <param name="ids">Item ids, comma delimited.</param>
/// <response code="204">Items added to collection.</response> /// <response code="204">Items added to collection.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("{collectionId}/Items")] [HttpPost("{collectionId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> AddToCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string itemIds) public async Task<ActionResult> AddToCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string ids)
{ {
await _collectionManager.AddToCollectionAsync(collectionId, RequestHelpers.GetGuids(itemIds)).ConfigureAwait(true); await _collectionManager.AddToCollectionAsync(collectionId, RequestHelpers.GetGuids(ids)).ConfigureAwait(true);
return NoContent(); return NoContent();
} }
@ -98,14 +98,14 @@ namespace Jellyfin.Api.Controllers
/// Removes items from a collection. /// Removes items from a collection.
/// </summary> /// </summary>
/// <param name="collectionId">The collection id.</param> /// <param name="collectionId">The collection id.</param>
/// <param name="itemIds">Item ids, comma delimited.</param> /// <param name="ids">Item ids, comma delimited.</param>
/// <response code="204">Items removed from collection.</response> /// <response code="204">Items removed from collection.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpDelete("{collectionId}/Items")] [HttpDelete("{collectionId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> RemoveFromCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string itemIds) public async Task<ActionResult> RemoveFromCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string ids)
{ {
await _collectionManager.RemoveFromCollectionAsync(collectionId, RequestHelpers.GetGuids(itemIds)).ConfigureAwait(false); await _collectionManager.RemoveFromCollectionAsync(collectionId, RequestHelpers.GetGuids(ids)).ConfigureAwait(false);
return NoContent(); return NoContent();
} }
} }

View File

@ -15,7 +15,7 @@ namespace Jellyfin.Api.Controllers
/// <summary> /// <summary>
/// Devices Controller. /// Devices Controller.
/// </summary> /// </summary>
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.RequiresElevation)]
public class DevicesController : BaseJellyfinApiController public class DevicesController : BaseJellyfinApiController
{ {
private readonly IDeviceManager _deviceManager; private readonly IDeviceManager _deviceManager;
@ -46,7 +46,6 @@ namespace Jellyfin.Api.Controllers
/// <response code="200">Devices retrieved.</response> /// <response code="200">Devices retrieved.</response>
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns> /// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
[HttpGet] [HttpGet]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) 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> /// <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> /// <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")] [HttpGet("Info")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string id) public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string id)
@ -84,7 +82,6 @@ namespace Jellyfin.Api.Controllers
/// <response code="404">Device not found.</response> /// <response code="404">Device not found.</response>
/// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns> /// <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")] [HttpGet("Options")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string id) public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string id)
@ -107,7 +104,6 @@ namespace Jellyfin.Api.Controllers
/// <response code="404">Device not found.</response> /// <response code="404">Device not found.</response>
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns> /// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
[HttpPost("Options")] [HttpPost("Options")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateDeviceOptions( public ActionResult UpdateDeviceOptions(

View File

@ -81,6 +81,9 @@ namespace Jellyfin.Api.Controllers
dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture); dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture);
dto.CustomPrefs["tvhome"] = displayPreferences.TvHome; dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
// This will essentially be a noop if no changes have been made, but new prefs must be saved at least.
_displayPreferencesManager.SaveChanges();
return dto; return dto;
} }

View File

@ -77,6 +77,7 @@ namespace Jellyfin.Api.Controllers
/// Gets Dlna media receiver registrar xml. /// Gets Dlna media receiver registrar xml.
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Dlna media receiver registrar xml returned.</response>
/// <returns>Dlna media receiver registrar xml.</returns> /// <returns>Dlna media receiver registrar xml.</returns>
[HttpGet("{serverId}/MediaReceiverRegistrar")] [HttpGet("{serverId}/MediaReceiverRegistrar")]
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")] [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")]
@ -94,6 +95,7 @@ namespace Jellyfin.Api.Controllers
/// Gets Dlna media receiver registrar xml. /// Gets Dlna media receiver registrar xml.
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Dlna media receiver registrar xml returned.</response>
/// <returns>Dlna media receiver registrar xml.</returns> /// <returns>Dlna media receiver registrar xml.</returns>
[HttpGet("{serverId}/ConnectionManager")] [HttpGet("{serverId}/ConnectionManager")]
[HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")] [HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")]
@ -111,8 +113,12 @@ namespace Jellyfin.Api.Controllers
/// Process a content directory control request. /// Process a content directory control request.
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response>
/// <returns>Control response.</returns> /// <returns>Control response.</returns>
[HttpPost("{serverId}/ContentDirectory/Control")] [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) public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId)
{ {
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false); return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
@ -122,8 +128,12 @@ namespace Jellyfin.Api.Controllers
/// Process a connection manager control request. /// Process a connection manager control request.
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response>
/// <returns>Control response.</returns> /// <returns>Control response.</returns>
[HttpPost("{serverId}/ConnectionManager/Control")] [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) public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId)
{ {
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false); return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
@ -133,8 +143,12 @@ namespace Jellyfin.Api.Controllers
/// Process a media receiver registrar control request. /// Process a media receiver registrar control request.
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response>
/// <returns>Control response.</returns> /// <returns>Control response.</returns>
[HttpPost("{serverId}/MediaReceiverRegistrar/Control")] [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) public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId)
{ {
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false); return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
@ -144,11 +158,15 @@ namespace Jellyfin.Api.Controllers
/// Processes an event subscription request. /// Processes an event subscription request.
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response>
/// <returns>Event subscription response.</returns> /// <returns>Event subscription response.</returns>
[HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")] [HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")]
[HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")] [HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")]
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId) public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId)
{ {
return ProcessEventRequest(_mediaReceiverRegistrar); return ProcessEventRequest(_mediaReceiverRegistrar);
@ -158,11 +176,15 @@ namespace Jellyfin.Api.Controllers
/// Processes an event subscription request. /// Processes an event subscription request.
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response>
/// <returns>Event subscription response.</returns> /// <returns>Event subscription response.</returns>
[HttpSubscribe("{serverId}/ContentDirectory/Events")] [HttpSubscribe("{serverId}/ContentDirectory/Events")]
[HttpUnsubscribe("{serverId}/ContentDirectory/Events")] [HttpUnsubscribe("{serverId}/ContentDirectory/Events")]
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId) public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId)
{ {
return ProcessEventRequest(_contentDirectory); return ProcessEventRequest(_contentDirectory);
@ -172,11 +194,15 @@ namespace Jellyfin.Api.Controllers
/// Processes an event subscription request. /// Processes an event subscription request.
/// </summary> /// </summary>
/// <param name="serverId">Server UUID.</param> /// <param name="serverId">Server UUID.</param>
/// <response code="200">Request processed.</response>
/// <returns>Event subscription response.</returns> /// <returns>Event subscription response.</returns>
[HttpSubscribe("{serverId}/ConnectionManager/Events")] [HttpSubscribe("{serverId}/ConnectionManager/Events")]
[HttpUnsubscribe("{serverId}/ConnectionManager/Events")] [HttpUnsubscribe("{serverId}/ConnectionManager/Events")]
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs [ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
[ProducesResponseType(StatusCodes.Status200OK)]
[Produces(MediaTypeNames.Text.Xml)]
[ProducesFile(MediaTypeNames.Text.Xml)]
public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId) public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId)
{ {
return ProcessEventRequest(_connectionManager); return ProcessEventRequest(_connectionManager);

View File

@ -295,6 +295,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param> /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</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="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="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param> /// <param name="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] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate, [FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth, [FromQuery] int? maxAudioBitDepth,
[FromQuery] int? maxStreamingBitrate,
[FromQuery] int? audioBitRate, [FromQuery] int? audioBitRate,
[FromQuery] int? audioChannels, [FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels, [FromQuery] int? maxAudioChannels,
@ -403,7 +405,7 @@ namespace Jellyfin.Api.Controllers
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate, AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels, MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate, AudioBitRate = audioBitRate ?? maxStreamingBitrate,
MaxAudioBitDepth = maxAudioBitDepth, MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = audioChannels, AudioChannels = audioChannels,
Profile = profile, Profile = profile,
@ -623,6 +625,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param> /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</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="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="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param> /// <param name="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] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate, [FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth, [FromQuery] int? maxAudioBitDepth,
[FromQuery] int? maxStreamingBitrate,
[FromQuery] int? audioBitRate, [FromQuery] int? audioBitRate,
[FromQuery] int? audioChannels, [FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels, [FromQuery] int? maxAudioChannels,
@ -729,7 +733,7 @@ namespace Jellyfin.Api.Controllers
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate, AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels, MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate, AudioBitRate = audioBitRate ?? maxStreamingBitrate,
MaxAudioBitDepth = maxAudioBitDepth, MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = audioChannels, AudioChannels = audioChannels,
Profile = profile, Profile = profile,
@ -959,6 +963,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param> /// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param> /// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</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="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="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param> /// <param name="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] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate, [FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth, [FromQuery] int? maxAudioBitDepth,
[FromQuery] int? maxStreamingBitrate,
[FromQuery] int? audioBitRate, [FromQuery] int? audioBitRate,
[FromQuery] int? audioChannels, [FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels, [FromQuery] int? maxAudioChannels,
@ -1069,7 +1075,7 @@ namespace Jellyfin.Api.Controllers
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false, BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate, AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels, MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate, AudioBitRate = audioBitRate ?? maxStreamingBitrate,
MaxAudioBitDepth = maxAudioBitDepth, MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = audioChannels, AudioChannels = audioChannels,
Profile = profile, Profile = profile,

View File

@ -1,15 +1,16 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -47,30 +48,16 @@ namespace Jellyfin.Api.Controllers
/// <summary> /// <summary>
/// Gets all genres from a given item, folder, or the entire library. /// Gets all genres from a given item, folder, or the entire library.
/// </summary> /// </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="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="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">The search term.</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="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> /// <param name="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="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="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="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="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="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="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="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="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
@ -82,30 +69,16 @@ namespace Jellyfin.Api.Controllers
[HttpGet] [HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetGenres( public ActionResult<QueryResult<BaseItemDto>> GetGenres(
[FromQuery] double? minCommunityRating,
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery] string? includeItemTypes,
[FromQuery] ItemFilter[] filters,
[FromQuery] bool? isFavorite, [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] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
[FromQuery] string? personIds,
[FromQuery] string? personTypes,
[FromQuery] string? studios,
[FromQuery] string? studioIds,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith, [FromQuery] string? nameStartsWith,
@ -113,45 +86,24 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages = true, [FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true) [FromQuery] bool enableTotalRecordCount = true)
{ {
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
User? user = null; User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null;
BaseItem parentItem;
if (userId.HasValue && !userId.Equals(Guid.Empty)) var parentItem = _libraryManager.GetParentItem(parentId, userId);
{
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 query = new InternalItemsQuery(user)
{ {
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
StartIndex = startIndex, StartIndex = startIndex,
Limit = limit, Limit = limit,
IsFavorite = isFavorite, IsFavorite = isFavorite,
NameLessThan = nameLessThan, NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith, NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater, 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, DtoOptions = dtoOptions,
SearchTerm = searchTerm, SearchTerm = searchTerm,
EnableTotalRecordCount = enableTotalRecordCount EnableTotalRecordCount = enableTotalRecordCount
@ -169,87 +121,20 @@ namespace Jellyfin.Api.Controllers
} }
} }
// Studios QueryResult<(BaseItem, ItemCounts)> result;
if (!string.IsNullOrEmpty(studios)) 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('|') result = _libraryManager.GetMusicGenres(query);
.Select(i => }
{ else
try {
{ result = _libraryManager.GetGenres(query);
return _libraryManager.GetStudio(i);
}
catch
{
return null;
}
}).Where(i => i != null)
.Select(i => i!.Id)
.ToArray();
} }
foreach (var filter in filters) var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes);
{ return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
switch (filter)
{
case ItemFilter.Dislikes:
query.IsLiked = false;
break;
case ItemFilter.IsFavorite:
query.IsFavorite = true;
break;
case ItemFilter.IsFavoriteOrLikes:
query.IsFavoriteOrLiked = true;
break;
case ItemFilter.IsFolder:
query.IsFolder = true;
break;
case ItemFilter.IsNotFolder:
query.IsFolder = false;
break;
case ItemFilter.IsPlayed:
query.IsPlayed = true;
break;
case ItemFilter.IsResumable:
query.IsResumable = true;
break;
case ItemFilter.IsUnplayed:
query.IsPlayed = false;
break;
case ItemFilter.Likes:
query.IsLiked = true;
break;
}
}
var result = new QueryResult<(BaseItem, ItemCounts)>();
var dtos = result.Items.Select(i =>
{
var (baseItem, counts) = i;
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
if (!string.IsNullOrWhiteSpace(includeItemTypes))
{
dto.ChildCount = counts.ItemCount;
dto.ProgramCount = counts.ProgramCount;
dto.SeriesCount = counts.SeriesCount;
dto.EpisodeCount = counts.EpisodeCount;
dto.MovieCount = counts.MovieCount;
dto.TrailerCount = counts.TrailerCount;
dto.AlbumCount = counts.AlbumCount;
dto.SongCount = counts.SongCount;
dto.ArtistCount = counts.ArtistCount;
}
return dto;
});
return new QueryResult<BaseItemDto>
{
Items = dtos.ToArray(),
TotalRecordCount = result.TotalRecordCount
};
} }
/// <summary> /// <summary>

View File

@ -109,7 +109,7 @@ namespace Jellyfin.Api.Controllers
var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username); var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
if (user.ProfileImage != null) if (user.ProfileImage != null)
{ {
_userManager.ClearProfileImage(user); await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
} }
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType))); user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
@ -138,7 +138,7 @@ namespace Jellyfin.Api.Controllers
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult DeleteUserImage( public async Task<ActionResult> DeleteUserImage(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromRoute, Required] ImageType imageType, [FromRoute, Required] ImageType imageType,
[FromRoute] int? index = null) [FromRoute] int? index = null)
@ -158,7 +158,7 @@ namespace Jellyfin.Api.Controllers
_logger.LogError(e, "Error deleting user profile image:"); _logger.LogError(e, "Error deleting user profile image:");
} }
_userManager.ClearProfileImage(user); await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
return NoContent(); return NoContent();
} }

View File

@ -4,12 +4,14 @@ using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -54,7 +56,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="id">The item id.</param> /// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param> /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="enableImages">Optional. Include image information in output.</param> /// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="enableUserData">Optional. Include user data.</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="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@ -67,18 +69,17 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid id, [FromRoute, Required] Guid id,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{ {
var item = _libraryManager.GetItemById(id); var item = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value) ? _userManager.GetUserById(userId.Value)
: null; : null;
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
@ -91,7 +92,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="id">The item id.</param> /// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param> /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="enableImages">Optional. Include image information in output.</param> /// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="enableUserData">Optional. Include user data.</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="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@ -104,18 +105,17 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid id, [FromRoute, Required] Guid id,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{ {
var album = _libraryManager.GetItemById(id); var album = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value) ? _userManager.GetUserById(userId.Value)
: null; : null;
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions); var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
@ -128,7 +128,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="id">The item id.</param> /// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param> /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="enableImages">Optional. Include image information in output.</param> /// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="enableUserData">Optional. Include user data.</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="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@ -141,18 +141,17 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid id, [FromRoute, Required] Guid id,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{ {
var playlist = (Playlist)_libraryManager.GetItemById(id); var playlist = (Playlist)_libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value) ? _userManager.GetUserById(userId.Value)
: null; : null;
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions); var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
@ -165,7 +164,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="name">The genre name.</param> /// <param name="name">The genre name.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param> /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="enableImages">Optional. Include image information in output.</param> /// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="enableUserData">Optional. Include user data.</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="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@ -178,17 +177,16 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name, [FromRoute, Required] string name,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{ {
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value) ? _userManager.GetUserById(userId.Value)
: null; : null;
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions); var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions);
@ -201,7 +199,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="id">The item id.</param> /// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param> /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="enableImages">Optional. Include image information in output.</param> /// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="enableUserData">Optional. Include user data.</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="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@ -214,18 +212,17 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid id, [FromRoute, Required] Guid id,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{ {
var item = _libraryManager.GetItemById(id); var item = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value) ? _userManager.GetUserById(userId.Value)
: null; : null;
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
@ -238,7 +235,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="id">The item id.</param> /// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param> /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="enableImages">Optional. Include image information in output.</param> /// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="enableUserData">Optional. Include user data.</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="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@ -251,18 +248,17 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid id, [FromRoute, Required] Guid id,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{ {
var item = _libraryManager.GetItemById(id); var item = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value) ? _userManager.GetUserById(userId.Value)
: null; : null;
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
@ -275,7 +271,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="id">The item id.</param> /// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param> /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param> /// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="enableImages">Optional. Include image information in output.</param> /// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="enableUserData">Optional. Include user data.</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="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@ -288,18 +284,17 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid id, [FromRoute, Required] Guid id,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{ {
var item = _libraryManager.GetItemById(id); var item = _libraryManager.GetItemById(id);
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value) ? _userManager.GetUserById(userId.Value)
: null; : null;
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
@ -315,9 +310,9 @@ namespace Jellyfin.Api.Controllers
TotalRecordCount = list.Count 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); var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user);

View File

@ -5,6 +5,7 @@ using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -159,7 +160,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isHd, [FromQuery] bool? isHd,
[FromQuery] bool? is4K, [FromQuery] bool? is4K,
[FromQuery] string? locationTypes, [FromQuery] string? locationTypes,
[FromQuery] LocationType[] excludeLocationTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing, [FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired, [FromQuery] bool? isUnaired,
[FromQuery] double? minCommunityRating, [FromQuery] double? minCommunityRating,
@ -179,13 +180,13 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? sortOrder, [FromQuery] string? sortOrder,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery] string? includeItemTypes,
[FromQuery] ItemFilter[] filters, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes, [FromQuery] string? mediaTypes,
[FromQuery] string? imageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery] string? sortBy, [FromQuery] string? sortBy,
[FromQuery] bool? isPlayed, [FromQuery] bool? isPlayed,
[FromQuery] string? genres, [FromQuery] string? genres,
@ -194,7 +195,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? years, [FromQuery] string? years,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person, [FromQuery] string? person,
[FromQuery] string? personIds, [FromQuery] string? personIds,
[FromQuery] string? personTypes, [FromQuery] string? personTypes,
@ -233,8 +234,7 @@ namespace Jellyfin.Api.Controllers
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value) ? _userManager.GetUserById(userId.Value)
: null; : null;
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@ -342,7 +342,7 @@ namespace Jellyfin.Api.Controllers
PersonIds = RequestHelpers.GetGuids(personIds), PersonIds = RequestHelpers.GetGuids(personIds),
PersonTypes = RequestHelpers.Split(personTypes, ',', true), PersonTypes = RequestHelpers.Split(personTypes, ',', true),
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(), Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
ImageTypes = RequestHelpers.Split(imageTypes, ',', true).Select(v => Enum.Parse<ImageType>(v, true)).ToArray(), ImageTypes = imageTypes,
VideoTypes = RequestHelpers.Split(videoTypes, ',', true).Select(v => Enum.Parse<VideoType>(v, true)).ToArray(), VideoTypes = RequestHelpers.Split(videoTypes, ',', true).Select(v => Enum.Parse<VideoType>(v, true)).ToArray(),
AdjacentTo = adjacentTo, AdjacentTo = adjacentTo,
ItemIds = RequestHelpers.GetGuids(ids), ItemIds = RequestHelpers.GetGuids(ids),
@ -532,11 +532,11 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? mediaTypes, [FromQuery] string? mediaTypes,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? excludeItemTypes, [FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery] string? includeItemTypes,
[FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool enableTotalRecordCount = true,
@ -544,8 +544,7 @@ namespace Jellyfin.Api.Controllers
{ {
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId);
var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId); var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);

View File

@ -12,6 +12,7 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LibraryDtos; using Jellyfin.Api.Models.LibraryDtos;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using MediaBrowser.Common.Progress; using MediaBrowser.Common.Progress;
@ -680,12 +681,12 @@ namespace Jellyfin.Api.Controllers
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
/// <response code="200">Similar items returned.</response> /// <response code="200">Similar items returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> containing the similar items.</returns> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> containing the similar items.</returns>
[HttpGet("Artists/{itemId}/Similar", Name = "GetSimilarArtists2")] [HttpGet("Artists/{itemId}/Similar")]
[HttpGet("Items/{itemId}/Similar")] [HttpGet("Items/{itemId}/Similar")]
[HttpGet("Albums/{itemId}/Similar", Name = "GetSimilarAlbums2")] [HttpGet("Albums/{itemId}/Similar")]
[HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows2")] [HttpGet("Shows/{itemId}/Similar")]
[HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies2")] [HttpGet("Movies/{itemId}/Similar")]
[HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers2")] [HttpGet("Trailers/{itemId}/Similar")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems( public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
@ -693,7 +694,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? excludeArtistIds, [FromQuery] string? excludeArtistIds,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? fields) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
{ {
var item = itemId.Equals(Guid.Empty) var item = itemId.Equals(Guid.Empty)
? (!userId.Equals(Guid.Empty) ? (!userId.Equals(Guid.Empty)
@ -701,33 +702,71 @@ namespace Jellyfin.Api.Controllers
: _libraryManager.RootFolder) : _libraryManager.RootFolder)
: _libraryManager.GetItemById(itemId); : _libraryManager.GetItemById(itemId);
var program = item as IHasProgramAttributes; if (item is Episode || (item is IItemByName && !(item is MusicArtist)))
var isMovie = item is MediaBrowser.Controller.Entities.Movies.Movie || (program != null && program.IsMovie) || item is Trailer;
if (program != null && program.IsSeries)
{
return GetSimilarItemsResult(
item,
excludeArtistIds,
userId,
limit,
fields,
new[] { nameof(Series) },
false);
}
if (item is MediaBrowser.Controller.Entities.TV.Episode || (item is IItemByName && !(item is MusicArtist)))
{ {
return new QueryResult<BaseItemDto>(); return new QueryResult<BaseItemDto>();
} }
return GetSimilarItemsResult( var user = userId.HasValue && !userId.Equals(Guid.Empty)
item, ? _userManager.GetUserById(userId.Value)
excludeArtistIds, : null;
userId, var dtoOptions = new DtoOptions { Fields = fields }
limit, .AddClientFields(Request);
fields,
new[] { item.GetType().Name }, var program = item as IHasProgramAttributes;
isMovie); bool? isMovie = item is Movie || (program != null && program.IsMovie) || item is Trailer;
bool? isSeries = item is Series || (program != null && program.IsSeries);
var includeItemTypes = new List<string>();
if (isMovie.Value)
{
includeItemTypes.Add(nameof(Movie));
if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
{
includeItemTypes.Add(nameof(Trailer));
includeItemTypes.Add(nameof(LiveTvProgram));
}
}
else if (isSeries.Value)
{
includeItemTypes.Add(nameof(Series));
}
else
{
// For non series and movie types these columns are typically null
isSeries = null;
isMovie = null;
includeItemTypes.Add(item.GetType().Name);
}
var query = new InternalItemsQuery(user)
{
Limit = limit,
IncludeItemTypes = includeItemTypes.ToArray(),
IsMovie = isMovie,
IsSeries = isSeries,
SimilarTo = item,
DtoOptions = dtoOptions,
EnableTotalRecordCount = !isMovie ?? true,
EnableGroupByMetadataKey = isMovie ?? false,
MinSimilarityScore = 2 // A remnant from album/artist scoring
};
// ExcludeArtistIds
if (!string.IsNullOrEmpty(excludeArtistIds))
{
query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds);
}
List<BaseItem> itemsResult = _libraryManager.GetItemList(query);
var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
return new QueryResult<BaseItemDto>
{
Items = returnList,
TotalRecordCount = itemsResult.Count
};
} }
/// <summary> /// <summary>
@ -880,75 +919,6 @@ namespace Jellyfin.Api.Controllers
} }
} }
private QueryResult<BaseItemDto> GetSimilarItemsResult(
BaseItem item,
string? excludeArtistIds,
Guid? userId,
int? limit,
string? fields,
string[] includeItemTypes,
bool isMovie)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value)
: null;
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
.AddClientFields(Request);
var query = new InternalItemsQuery(user)
{
Limit = limit,
IncludeItemTypes = includeItemTypes,
IsMovie = isMovie,
SimilarTo = item,
DtoOptions = dtoOptions,
EnableTotalRecordCount = !isMovie,
EnableGroupByMetadataKey = isMovie
};
// ExcludeArtistIds
if (!string.IsNullOrEmpty(excludeArtistIds))
{
query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds);
}
List<BaseItem> itemsResult;
if (isMovie)
{
var itemTypes = new List<string> { nameof(MediaBrowser.Controller.Entities.Movies.Movie) };
if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
{
itemTypes.Add(nameof(Trailer));
itemTypes.Add(nameof(LiveTvProgram));
}
query.IncludeItemTypes = itemTypes.ToArray();
itemsResult = _libraryManager.GetArtists(query).Items.Select(i => i.Item1).ToList();
}
else if (item is MusicArtist)
{
query.IncludeItemTypes = Array.Empty<string>();
itemsResult = _libraryManager.GetArtists(query).Items.Select(i => i.Item1).ToList();
}
else
{
itemsResult = _libraryManager.GetItemList(query);
}
var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
var result = new QueryResult<BaseItemDto>
{
Items = returnList,
TotalRecordCount = itemsResult.Count
};
return result;
}
private static string[] GetRepresentativeItemTypes(string? contentType) private static string[] GetRepresentativeItemTypes(string? contentType)
{ {
return contentType switch return contentType switch

View File

@ -7,6 +7,7 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LibraryStructureDto; using Jellyfin.Api.Models.LibraryStructureDto;
using MediaBrowser.Common.Progress; using MediaBrowser.Common.Progress;
using MediaBrowser.Controller; using MediaBrowser.Controller;
@ -75,7 +76,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> AddVirtualFolder( public async Task<ActionResult> AddVirtualFolder(
[FromQuery] string? name, [FromQuery] string? name,
[FromQuery] string? collectionType, [FromQuery] string? collectionType,
[FromQuery] string[] paths, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
[FromBody] AddVirtualFolderDto? libraryOptionsDto, [FromBody] AddVirtualFolderDto? libraryOptionsDto,
[FromQuery] bool refreshLibrary = false) [FromQuery] bool refreshLibrary = false)
{ {

View File

@ -14,6 +14,7 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LiveTvDtos; using Jellyfin.Api.Models.LiveTvDtos;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Common; using MediaBrowser.Common;
@ -26,6 +27,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
@ -117,7 +119,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="enableImages">Optional. Include image information in output.</param> /// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</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="enableImageTypes">"Optional. The image types to include in the output.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="enableUserData">Optional. Include user data.</param> /// <param name="enableUserData">Optional. Include user data.</param>
/// <param name="sortBy">Optional. Key to sort by.</param> /// <param name="sortBy">Optional. Key to sort by.</param>
/// <param name="sortOrder">Optional. Sort order.</param> /// <param name="sortOrder">Optional. Sort order.</param>
@ -145,16 +147,15 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isDisliked, [FromQuery] bool? isDisliked,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] string? sortBy, [FromQuery] string? sortBy,
[FromQuery] SortOrder? sortOrder, [FromQuery] SortOrder? sortOrder,
[FromQuery] bool enableFavoriteSorting = false, [FromQuery] bool enableFavoriteSorting = false,
[FromQuery] bool addCurrentProgram = true) [FromQuery] bool addCurrentProgram = true)
{ {
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@ -238,7 +239,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="enableImages">Optional. Include image information in output.</param> /// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</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="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="enableUserData">Optional. Include user data.</param> /// <param name="enableUserData">Optional. Include user data.</param>
/// <param name="isMovie">Optional. Filter for movies.</param> /// <param name="isMovie">Optional. Filter for movies.</param>
/// <param name="isSeries">Optional. Filter for series.</param> /// <param name="isSeries">Optional. Filter for series.</param>
@ -262,8 +263,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? seriesTimerId, [FromQuery] string? seriesTimerId,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] bool? isMovie, [FromQuery] bool? isMovie,
[FromQuery] bool? isSeries, [FromQuery] bool? isSeries,
@ -273,8 +274,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isLibraryItem, [FromQuery] bool? isLibraryItem,
[FromQuery] bool enableTotalRecordCount = true) [FromQuery] bool enableTotalRecordCount = true)
{ {
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
@ -295,7 +295,7 @@ namespace Jellyfin.Api.Controllers
IsKids = isKids, IsKids = isKids,
IsSports = isSports, IsSports = isSports,
IsLibraryItem = isLibraryItem, IsLibraryItem = isLibraryItem,
Fields = RequestHelpers.GetItemFields(fields), Fields = fields,
ImageTypeLimit = imageTypeLimit, ImageTypeLimit = imageTypeLimit,
EnableImages = enableImages EnableImages = enableImages
}, dtoOptions); }, dtoOptions);
@ -315,7 +315,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="enableImages">Optional. Include image information in output.</param> /// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</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="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="enableUserData">Optional. Include user data.</param> /// <param name="enableUserData">Optional. Include user data.</param>
/// <param name="enableTotalRecordCount">Optional. Return total record count.</param> /// <param name="enableTotalRecordCount">Optional. Return total record count.</param>
/// <response code="200">Live tv recordings returned.</response> /// <response code="200">Live tv recordings returned.</response>
@ -349,8 +349,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? seriesTimerId, [FromQuery] string? seriesTimerId,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true) [FromQuery] bool enableTotalRecordCount = true)
{ {
@ -529,7 +529,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="enableUserData">Optional. Include user data.</param> /// <param name="enableUserData">Optional. Include user data.</param>
/// <param name="seriesTimerId">Optional. Filter by series timer id.</param> /// <param name="seriesTimerId">Optional. Filter by series timer id.</param>
/// <param name="librarySeriesId">Optional. Filter by library series id.</param> /// <param name="librarySeriesId">Optional. Filter by library series id.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="enableTotalRecordCount">Retrieve total record count.</param> /// <param name="enableTotalRecordCount">Retrieve total record count.</param>
/// <response code="200">Live tv epgs returned.</response> /// <response code="200">Live tv epgs returned.</response>
/// <returns> /// <returns>
@ -560,11 +560,11 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? genreIds, [FromQuery] string? genreIds,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] string? seriesTimerId, [FromQuery] string? seriesTimerId,
[FromQuery] Guid? librarySeriesId, [FromQuery] Guid? librarySeriesId,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool enableTotalRecordCount = true) [FromQuery] bool enableTotalRecordCount = true)
{ {
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
@ -591,7 +591,7 @@ namespace Jellyfin.Api.Controllers
IsKids = isKids, IsKids = isKids,
IsSports = isSports, IsSports = isSports,
SeriesTimerId = seriesTimerId, SeriesTimerId = seriesTimerId,
Genres = RequestHelpers.Split(genres, ',', true), Genres = RequestHelpers.Split(genres, '|', true),
GenreIds = RequestHelpers.GetGuids(genreIds) GenreIds = RequestHelpers.GetGuids(genreIds)
}; };
@ -605,8 +605,7 @@ namespace Jellyfin.Api.Controllers
} }
} }
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false); return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
@ -647,7 +646,7 @@ namespace Jellyfin.Api.Controllers
IsKids = body.IsKids, IsKids = body.IsKids,
IsSports = body.IsSports, IsSports = body.IsSports,
SeriesTimerId = body.SeriesTimerId, SeriesTimerId = body.SeriesTimerId,
Genres = RequestHelpers.Split(body.Genres, ',', true), Genres = RequestHelpers.Split(body.Genres, '|', true),
GenreIds = RequestHelpers.GetGuids(body.GenreIds) GenreIds = RequestHelpers.GetGuids(body.GenreIds)
}; };
@ -661,8 +660,7 @@ namespace Jellyfin.Api.Controllers
} }
} }
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = body.Fields }
.AddItemFields(body.Fields)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes); .AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes);
return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false); return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
@ -684,7 +682,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</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="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="genreIds">The genres to return guide information for.</param> /// <param name="genreIds">The genres to return guide information for.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="enableUserData">Optional. include user data.</param> /// <param name="enableUserData">Optional. include user data.</param>
/// <param name="enableTotalRecordCount">Retrieve total record count.</param> /// <param name="enableTotalRecordCount">Retrieve total record count.</param>
/// <response code="200">Recommended epgs returned.</response> /// <response code="200">Recommended epgs returned.</response>
@ -704,9 +702,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isSports, [FromQuery] bool? isSports,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? genreIds, [FromQuery] string? genreIds,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true) [FromQuery] bool enableTotalRecordCount = true)
{ {
@ -728,8 +726,7 @@ namespace Jellyfin.Api.Controllers
GenreIds = RequestHelpers.GetGuids(genreIds) GenreIds = RequestHelpers.GetGuids(genreIds)
}; };
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
return _liveTvManager.GetRecommendedPrograms(query, dtoOptions, CancellationToken.None); return _liveTvManager.GetRecommendedPrograms(query, dtoOptions, CancellationToken.None);
@ -1219,11 +1216,8 @@ namespace Jellyfin.Api.Controllers
return NotFound(); return NotFound();
} }
await using var memoryStream = new MemoryStream(); var liveStream = new ProgressiveFileStream(liveStreamInfo.GetFilePath(), null, _transcodingJobHelper);
await new ProgressiveFileCopier(liveStreamInfo, null, _transcodingJobHelper, CancellationToken.None) return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container));
.WriteToAsync(memoryStream, CancellationToken.None)
.ConfigureAwait(false);
return File(memoryStream, MimeTypes.GetMimeType("file." + container));
} }
private void AssertUserCanManageLiveTv() private void AssertUserCanManageLiveTv()

View File

@ -104,7 +104,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo( public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] long? maxStreamingBitrate, [FromQuery] int? maxStreamingBitrate,
[FromQuery] long? startTimeTicks, [FromQuery] long? startTimeTicks,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex, [FromQuery] int? subtitleStreamIndex,
@ -234,7 +234,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? openToken, [FromQuery] string? openToken,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] string? playSessionId, [FromQuery] string? playSessionId,
[FromQuery] long? maxStreamingBitrate, [FromQuery] int? maxStreamingBitrate,
[FromQuery] long? startTimeTicks, [FromQuery] long? startTimeTicks,
[FromQuery] int? audioStreamIndex, [FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex, [FromQuery] int? subtitleStreamIndex,

View File

@ -4,6 +4,7 @@ using System.Globalization;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
@ -65,15 +66,14 @@ namespace Jellyfin.Api.Controllers
public ActionResult<IEnumerable<RecommendationDto>> GetMovieRecommendations( public ActionResult<IEnumerable<RecommendationDto>> GetMovieRecommendations(
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] int categoryLimit = 5, [FromQuery] int categoryLimit = 5,
[FromQuery] int itemLimit = 8) [FromQuery] int itemLimit = 8)
{ {
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value) ? _userManager.GetUserById(userId.Value)
: null; : null;
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
.AddClientFields(Request); .AddClientFields(Request);
var categories = new List<RecommendationDto>(); var categories = new List<RecommendationDto>();
@ -85,8 +85,8 @@ namespace Jellyfin.Api.Controllers
IncludeItemTypes = new[] IncludeItemTypes = new[]
{ {
nameof(Movie), nameof(Movie),
// typeof(Trailer).Name, // nameof(Trailer),
// typeof(LiveTvProgram).Name // nameof(LiveTvProgram)
}, },
// IsMovie = true // IsMovie = true
OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(), OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),

View File

@ -1,16 +1,17 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -47,30 +48,16 @@ namespace Jellyfin.Api.Controllers
/// <summary> /// <summary>
/// Gets all music genres from a given item, folder, or the entire library. /// Gets all music genres from a given item, folder, or the entire library.
/// </summary> /// </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="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="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">The search term.</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="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> /// <param name="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="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="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="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="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="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="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="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="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
@ -80,31 +67,18 @@ namespace Jellyfin.Api.Controllers
/// <response code="200">Music genres returned.</response> /// <response code="200">Music genres returned.</response>
/// <returns>An <see cref="OkResult"/> containing the queryresult of music genres.</returns> /// <returns>An <see cref="OkResult"/> containing the queryresult of music genres.</returns>
[HttpGet] [HttpGet]
[Obsolete("Use GetGenres instead")]
public ActionResult<QueryResult<BaseItemDto>> GetMusicGenres( public ActionResult<QueryResult<BaseItemDto>> GetMusicGenres(
[FromQuery] double? minCommunityRating,
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery] string? includeItemTypes,
[FromQuery] ItemFilter[] filters,
[FromQuery] bool? isFavorite, [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] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
[FromQuery] string? personIds,
[FromQuery] string? personTypes,
[FromQuery] string? studios,
[FromQuery] string? studioIds,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith, [FromQuery] string? nameStartsWith,
@ -112,45 +86,24 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages = true, [FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true) [FromQuery] bool enableTotalRecordCount = true)
{ {
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
User? user = null; User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null;
BaseItem parentItem;
if (userId.HasValue && !userId.Equals(Guid.Empty)) var parentItem = _libraryManager.GetParentItem(parentId, userId);
{
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 query = new InternalItemsQuery(user)
{ {
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
StartIndex = startIndex, StartIndex = startIndex,
Limit = limit, Limit = limit,
IsFavorite = isFavorite, IsFavorite = isFavorite,
NameLessThan = nameLessThan, NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith, NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater, 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, DtoOptions = dtoOptions,
SearchTerm = searchTerm, SearchTerm = searchTerm,
EnableTotalRecordCount = enableTotalRecordCount EnableTotalRecordCount = enableTotalRecordCount
@ -168,87 +121,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 result = _libraryManager.GetMusicGenres(query);
var dtos = result.Items.Select(i => var shouldIncludeItemTypes = !string.IsNullOrWhiteSpace(includeItemTypes);
{ return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
var (baseItem, counts) = i;
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
if (!string.IsNullOrWhiteSpace(includeItemTypes))
{
dto.ChildCount = counts.ItemCount;
dto.ProgramCount = counts.ProgramCount;
dto.SeriesCount = counts.SeriesCount;
dto.EpisodeCount = counts.EpisodeCount;
dto.MovieCount = counts.MovieCount;
dto.TrailerCount = counts.TrailerCount;
dto.AlbumCount = counts.AlbumCount;
dto.SongCount = counts.SongCount;
dto.ArtistCount = counts.ArtistCount;
}
return dto;
});
return new QueryResult<BaseItemDto>
{
Items = dtos.ToArray(),
TotalRecordCount = result.TotalRecordCount
};
} }
/// <summary> /// <summary>

View File

@ -1,15 +1,16 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -26,6 +27,7 @@ namespace Jellyfin.Api.Controllers
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IDtoService _dtoService; private readonly IDtoService _dtoService;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataManager;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PersonsController"/> class. /// Initializes a new instance of the <see cref="PersonsController"/> class.
@ -33,221 +35,81 @@ namespace Jellyfin.Api.Controllers
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> 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="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
public PersonsController( public PersonsController(
ILibraryManager libraryManager, ILibraryManager libraryManager,
IDtoService dtoService, IDtoService dtoService,
IUserManager userManager) IUserManager userManager,
IUserDataManager userDataManager)
{ {
_libraryManager = libraryManager; _libraryManager = libraryManager;
_dtoService = dtoService; _dtoService = dtoService;
_userManager = userManager; _userManager = userManager;
_userDataManager = userDataManager;
} }
/// <summary> /// <summary>
/// Gets all persons from a given item, folder, or the entire library. /// Gets all persons.
/// </summary> /// </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="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">The search term.</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="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> /// <param name="filters">Optional. Specify additional filters to apply.</param>
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. 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="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
/// <param name="enableUserData">Optional, include user data.</param> /// <param name="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</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="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="excludePersonTypes">Optional. If specified results will be filtered to exclude those containing the specified PersonType. Allows multiple, comma-delimited.</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 results will be filtered to include only those containing the specified PersonType. Allows multiple, comma-delimited.</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="appearsInItemId">Optional. If specified, person results will be filtered on items related to said persons.</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="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="enableImages">Optional, include image information in output.</param>
/// <param name="enableTotalRecordCount">Optional. Include total record count.</param>
/// <response code="200">Persons returned.</response> /// <response code="200">Persons returned.</response>
/// <returns>An <see cref="OkResult"/> containing the queryresult of persons.</returns> /// <returns>An <see cref="OkResult"/> containing the queryresult of persons.</returns>
[HttpGet] [HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetPersons( public ActionResult<QueryResult<BaseItemDto>> GetPersons(
[FromQuery] double? minCommunityRating,
[FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? parentId, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
[FromQuery] ItemFilter[] filters,
[FromQuery] bool? isFavorite, [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] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person, [FromQuery] string? excludePersonTypes,
[FromQuery] string? personIds,
[FromQuery] string? personTypes, [FromQuery] string? personTypes,
[FromQuery] string? studios, [FromQuery] string? appearsInItemId,
[FromQuery] string? studioIds,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater, [FromQuery] bool? enableImages = true)
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{ {
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
User? user = null; User? user = null;
BaseItem parentItem;
if (userId.HasValue && !userId.Equals(Guid.Empty)) if (userId.HasValue && !userId.Equals(Guid.Empty))
{ {
user = _userManager.GetUserById(userId.Value); 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), PersonTypes = RequestHelpers.Split(personTypes, ',', true),
Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(), ExcludePersonTypes = RequestHelpers.Split(excludePersonTypes, ',', true),
MinCommunityRating = minCommunityRating, NameContains = searchTerm,
DtoOptions = dtoOptions, User = user,
SearchTerm = searchTerm, IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite,
EnableTotalRecordCount = enableTotalRecordCount AppearsInItemId = string.IsNullOrEmpty(appearsInItemId) ? Guid.Empty : Guid.Parse(appearsInItemId),
}; Limit = limit ?? 0
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;
}); });
return new QueryResult<BaseItemDto> return new QueryResult<BaseItemDto>
{ {
Items = dtos.ToArray(), Items = peopleItems.Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user)).ToArray(),
TotalRecordCount = result.TotalRecordCount TotalRecordCount = peopleItems.Count
}; };
} }

View File

@ -5,11 +5,13 @@ using System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.PlaylistDtos; using Jellyfin.Api.Models.PlaylistDtos;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Playlists; using MediaBrowser.Model.Playlists;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -133,7 +135,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="userId">User id.</param> /// <param name="userId">User id.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> /// <param name="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="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="enableImages">Optional. Include image information in output.</param> /// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="enableUserData">Optional. Include user data.</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="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@ -147,11 +149,11 @@ namespace Jellyfin.Api.Controllers
[FromQuery, Required] Guid userId, [FromQuery, Required] Guid userId,
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{ {
var playlist = (Playlist)_libraryManager.GetItemById(playlistId); var playlist = (Playlist)_libraryManager.GetItemById(playlistId);
if (playlist == null) if (playlist == null)
@ -175,8 +177,7 @@ namespace Jellyfin.Api.Controllers
items = items.Take(limit.Value).ToArray(); items = items.Take(limit.Value).ToArray();
} }
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);

View File

@ -36,7 +36,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>The list of scheduled tasks.</returns> /// <returns>The list of scheduled tasks.</returns>
[HttpGet] [HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public IEnumerable<IScheduledTaskWorker> GetTasks( public IEnumerable<TaskInfo> GetTasks(
[FromQuery] bool? isHidden, [FromQuery] bool? isHidden,
[FromQuery] bool? isEnabled) [FromQuery] bool? isEnabled)
{ {
@ -57,7 +57,7 @@ namespace Jellyfin.Api.Controllers
} }
} }
yield return task; yield return ScheduledTaskHelpers.GetTaskInfo(task);
} }
} }

View File

@ -379,7 +379,7 @@ namespace Jellyfin.Api.Controllers
public ActionResult PostCapabilities( public ActionResult PostCapabilities(
[FromQuery] string? id, [FromQuery] string? id,
[FromQuery] string? playableMediaTypes, [FromQuery] string? playableMediaTypes,
[FromQuery] GeneralCommandType[] supportedCommands, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
[FromQuery] bool supportsMediaControl = false, [FromQuery] bool supportsMediaControl = false,
[FromQuery] bool supportsSync = false, [FromQuery] bool supportsSync = false,
[FromQuery] bool supportsPersistentIdentifier = true) [FromQuery] bool supportsPersistentIdentifier = true)

View File

@ -1,14 +1,15 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -45,30 +46,17 @@ namespace Jellyfin.Api.Controllers
/// <summary> /// <summary>
/// Gets all studios from a given item, folder, or the entire library. /// Gets all studios from a given item, folder, or the entire library.
/// </summary> /// </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="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="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">Optional. Search term.</param> /// <param name="searchTerm">Optional. Search term.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> /// <param name="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="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="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="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="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</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="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="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="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="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
@ -80,30 +68,17 @@ namespace Jellyfin.Api.Controllers
[HttpGet] [HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetStudios( public ActionResult<QueryResult<BaseItemDto>> GetStudios(
[FromQuery] double? minCommunityRating,
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery] string? includeItemTypes,
[FromQuery] ItemFilter[] filters,
[FromQuery] bool? isFavorite, [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] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person,
[FromQuery] string? personIds,
[FromQuery] string? personTypes,
[FromQuery] string? studios,
[FromQuery] string? studioIds,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith, [FromQuery] string? nameStartsWith,
@ -111,49 +86,27 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages = true, [FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true) [FromQuery] bool enableTotalRecordCount = true)
{ {
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
User? user = null; User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null;
BaseItem parentItem;
if (userId.HasValue && !userId.Equals(Guid.Empty)) var parentItem = _libraryManager.GetParentItem(parentId, userId);
{
user = _userManager.GetUserById(userId.Value);
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
}
else
{
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
}
var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true); var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true); var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
var query = new InternalItemsQuery(user) var query = new InternalItemsQuery(user)
{ {
ExcludeItemTypes = excludeItemTypesArr, ExcludeItemTypes = excludeItemTypesArr,
IncludeItemTypes = includeItemTypesArr, IncludeItemTypes = includeItemTypesArr,
MediaTypes = mediaTypesArr,
StartIndex = startIndex, StartIndex = startIndex,
Limit = limit, Limit = limit,
IsFavorite = isFavorite, IsFavorite = isFavorite,
NameLessThan = nameLessThan, NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith, NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater, 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, DtoOptions = dtoOptions,
SearchTerm = searchTerm, SearchTerm = searchTerm,
EnableTotalRecordCount = enableTotalRecordCount EnableTotalRecordCount = enableTotalRecordCount
@ -171,84 +124,9 @@ namespace Jellyfin.Api.Controllers
} }
} }
// Studios var result = _libraryManager.GetStudios(query);
if (!string.IsNullOrEmpty(studios)) var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes);
{ return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
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
};
} }
/// <summary> /// <summary>

View File

@ -11,6 +11,9 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes; using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.SubtitleDtos;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
@ -21,6 +24,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers; using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Subtitles;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -34,6 +38,7 @@ namespace Jellyfin.Api.Controllers
[Route("")] [Route("")]
public class SubtitleController : BaseJellyfinApiController public class SubtitleController : BaseJellyfinApiController
{ {
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly ISubtitleManager _subtitleManager; private readonly ISubtitleManager _subtitleManager;
private readonly ISubtitleEncoder _subtitleEncoder; private readonly ISubtitleEncoder _subtitleEncoder;
@ -46,6 +51,7 @@ namespace Jellyfin.Api.Controllers
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SubtitleController"/> class. /// Initializes a new instance of the <see cref="SubtitleController"/> class.
/// </summary> /// </summary>
/// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param> /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
/// <param name="subtitleManager">Instance of <see cref="ISubtitleManager"/> interface.</param> /// <param name="subtitleManager">Instance of <see cref="ISubtitleManager"/> interface.</param>
/// <param name="subtitleEncoder">Instance of <see cref="ISubtitleEncoder"/> 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="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param>
/// <param name="logger">Instance of <see cref="ILogger{SubtitleController}"/> interface.</param> /// <param name="logger">Instance of <see cref="ILogger{SubtitleController}"/> interface.</param>
public SubtitleController( public SubtitleController(
IServerConfigurationManager serverConfigurationManager,
ILibraryManager libraryManager, ILibraryManager libraryManager,
ISubtitleManager subtitleManager, ISubtitleManager subtitleManager,
ISubtitleEncoder subtitleEncoder, ISubtitleEncoder subtitleEncoder,
@ -64,6 +71,7 @@ namespace Jellyfin.Api.Controllers
IAuthorizationContext authContext, IAuthorizationContext authContext,
ILogger<SubtitleController> logger) ILogger<SubtitleController> logger)
{ {
_serverConfigurationManager = serverConfigurationManager;
_libraryManager = libraryManager; _libraryManager = libraryManager;
_subtitleManager = subtitleManager; _subtitleManager = subtitleManager;
_subtitleEncoder = subtitleEncoder; _subtitleEncoder = subtitleEncoder;
@ -319,6 +327,33 @@ namespace Jellyfin.Api.Controllers
return File(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); 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> /// <summary>
/// Encodes a subtitle in the specified format. /// Encodes a subtitle in the specified format.
/// </summary> /// </summary>
@ -351,5 +386,95 @@ namespace Jellyfin.Api.Controllers
copyTimestamps, copyTimestamps,
CancellationToken.None); 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();
}
} }
} }

View File

@ -1,6 +1,7 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
@ -9,6 +10,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -18,6 +20,7 @@ namespace Jellyfin.Api.Controllers
/// The suggestions controller. /// The suggestions controller.
/// </summary> /// </summary>
[Route("")] [Route("")]
[Authorize(Policy = Policies.DefaultAuthorization)]
public class SuggestionsController : BaseJellyfinApiController public class SuggestionsController : BaseJellyfinApiController
{ {
private readonly IDtoService _dtoService; private readonly IDtoService _dtoService;

View File

@ -1,5 +1,6 @@
using System; using System;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.ModelBinders;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
@ -125,7 +126,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isHd, [FromQuery] bool? isHd,
[FromQuery] bool? is4K, [FromQuery] bool? is4K,
[FromQuery] string? locationTypes, [FromQuery] string? locationTypes,
[FromQuery] LocationType[] excludeLocationTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing, [FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired, [FromQuery] bool? isUnaired,
[FromQuery] double? minCommunityRating, [FromQuery] double? minCommunityRating,
@ -145,12 +146,12 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? sortOrder, [FromQuery] string? sortOrder,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery] string? excludeItemTypes,
[FromQuery] ItemFilter[] filters, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes, [FromQuery] string? mediaTypes,
[FromQuery] string? imageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery] string? sortBy, [FromQuery] string? sortBy,
[FromQuery] bool? isPlayed, [FromQuery] bool? isPlayed,
[FromQuery] string? genres, [FromQuery] string? genres,
@ -159,7 +160,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? years, [FromQuery] string? years,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person, [FromQuery] string? person,
[FromQuery] string? personIds, [FromQuery] string? personIds,
[FromQuery] string? personTypes, [FromQuery] string? personTypes,

View File

@ -5,6 +5,7 @@ using System.Globalization;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
@ -13,6 +14,7 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.TV; using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -57,7 +59,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="userId">The user id of the user to get the next up episodes for.</param> /// <param name="userId">The user id of the user to get the next up episodes for.</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="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="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="seriesId">Optional. Filter by series id.</param> /// <param name="seriesId">Optional. Filter by series id.</param>
/// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param> /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="enableImges">Optional. Include image information in output.</param> /// <param name="enableImges">Optional. Include image information in output.</param>
@ -72,17 +74,16 @@ namespace Jellyfin.Api.Controllers
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? seriesId, [FromQuery] string? seriesId,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery] bool? enableImges, [FromQuery] bool? enableImges,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true) [FromQuery] bool enableTotalRecordCount = true)
{ {
var options = new DtoOptions() var options = new DtoOptions { Fields = fields }
.AddItemFields(fields!)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!); .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!);
@ -117,7 +118,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="userId">The user id of the user to get the upcoming episodes for.</param> /// <param name="userId">The user id of the user to get the upcoming episodes for.</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="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="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param> /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="enableImges">Optional. Include image information in output.</param> /// <param name="enableImges">Optional. Include image information in output.</param>
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param> /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
@ -130,11 +131,11 @@ namespace Jellyfin.Api.Controllers
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery] bool? enableImges, [FromQuery] bool? enableImges,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData) [FromQuery] bool? enableUserData)
{ {
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
@ -145,8 +146,7 @@ namespace Jellyfin.Api.Controllers
var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId); var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
var options = new DtoOptions() var options = new DtoOptions { Fields = fields }
.AddItemFields(fields!)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!); .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!);
@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers
public ActionResult<QueryResult<BaseItemDto>> GetEpisodes( public ActionResult<QueryResult<BaseItemDto>> GetEpisodes(
[FromRoute, Required] string seriesId, [FromRoute, Required] string seriesId,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] int? season, [FromQuery] int? season,
[FromQuery] string? seasonId, [FromQuery] string? seasonId,
[FromQuery] bool? isMissing, [FromQuery] bool? isMissing,
@ -206,7 +206,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] string? sortBy) [FromQuery] string? sortBy)
{ {
@ -216,8 +216,7 @@ namespace Jellyfin.Api.Controllers
List<BaseItem> episodes; List<BaseItem> episodes;
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields!)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
@ -319,13 +318,13 @@ namespace Jellyfin.Api.Controllers
public ActionResult<QueryResult<BaseItemDto>> GetSeasons( public ActionResult<QueryResult<BaseItemDto>> GetSeasons(
[FromRoute, Required] string seriesId, [FromRoute, Required] string seriesId,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? isSpecialSeason, [FromQuery] bool? isSpecialSeason,
[FromQuery] bool? isMissing, [FromQuery] bool? isMissing,
[FromQuery] string? adjacentTo, [FromQuery] string? adjacentTo,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData) [FromQuery] bool? enableUserData)
{ {
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
@ -344,8 +343,7 @@ namespace Jellyfin.Api.Controllers
AdjacentTo = adjacentTo AdjacentTo = adjacentTo
}); });
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);

View File

@ -76,6 +76,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxAudioChannels">Optional. The maximum number of audio channels.</param> /// <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="transcodingAudioChannels">Optional. The number of how many audio channels to transcode to.</param>
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</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="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="transcodingContainer">Optional. The container to transcode to.</param>
/// <param name="transcodingProtocol">Optional. The transcoding protocol.</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> /// <response code="302">Redirected to remote audio stream.</response>
/// <returns>A <see cref="Task"/> containing the audio file.</returns> /// <returns>A <see cref="Task"/> containing the audio file.</returns>
[HttpGet("Audio/{itemId}/universal")] [HttpGet("Audio/{itemId}/universal")]
[HttpGet("Audio/{itemId}/universal.{container}", Name = "GetUniversalAudioStream_2")]
[HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")] [HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")]
[HttpHead("Audio/{itemId}/universal.{container}", Name = "HeadUniversalAudioStream_2")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status302Found)] [ProducesResponseType(StatusCodes.Status302Found)]
[ProducesAudioFile] [ProducesAudioFile]
public async Task<ActionResult> GetUniversalAudioStream( public async Task<ActionResult> GetUniversalAudioStream(
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
[FromRoute] string? container, [FromQuery] string? container,
[FromQuery] string? mediaSourceId, [FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId, [FromQuery] string? deviceId,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] string? audioCodec, [FromQuery] string? audioCodec,
[FromQuery] int? maxAudioChannels, [FromQuery] int? maxAudioChannels,
[FromQuery] int? transcodingAudioChannels, [FromQuery] int? transcodingAudioChannels,
[FromQuery] long? maxStreamingBitrate, [FromQuery] int? maxStreamingBitrate,
[FromQuery] int? audioBitRate,
[FromQuery] long? startTimeTicks, [FromQuery] long? startTimeTicks,
[FromQuery] string? transcodingContainer, [FromQuery] string? transcodingContainer,
[FromQuery] string? transcodingProtocol, [FromQuery] string? transcodingProtocol,
@ -212,7 +212,7 @@ namespace Jellyfin.Api.Controllers
AudioSampleRate = maxAudioSampleRate, AudioSampleRate = maxAudioSampleRate,
MaxAudioChannels = maxAudioChannels, MaxAudioChannels = maxAudioChannels,
MaxAudioBitDepth = maxAudioBitDepth, MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)), AudioBitRate = audioBitRate ?? maxStreamingBitrate,
StartTimeTicks = startTimeTicks, StartTimeTicks = startTimeTicks,
SubtitleMethod = SubtitleDeliveryMethod.Hls, SubtitleMethod = SubtitleDeliveryMethod.Hls,
RequireAvc = true, RequireAvc = true,
@ -244,7 +244,7 @@ namespace Jellyfin.Api.Controllers
BreakOnNonKeyFrames = breakOnNonKeyFrames, BreakOnNonKeyFrames = breakOnNonKeyFrames,
AudioSampleRate = maxAudioSampleRate, AudioSampleRate = maxAudioSampleRate,
MaxAudioChannels = maxAudioChannels, MaxAudioChannels = maxAudioChannels,
AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)), AudioBitRate = isStatic ? (int?)null : (audioBitRate ?? maxStreamingBitrate),
MaxAudioBitDepth = maxAudioBitDepth, MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = maxAudioChannels, AudioChannels = maxAudioChannels,
CopyTimestamps = true, CopyTimestamps = true,
@ -270,20 +270,24 @@ namespace Jellyfin.Api.Controllers
{ {
var deviceProfile = new DeviceProfile(); var deviceProfile = new DeviceProfile();
var directPlayProfiles = new List<DirectPlayProfile>();
var containers = RequestHelpers.Split(container, ',', true); var containers = RequestHelpers.Split(container, ',', true);
int len = containers.Length;
foreach (var cont in containers) 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[] deviceProfile.TranscodingProfiles = new[]
{ {

View File

@ -381,17 +381,13 @@ namespace Jellyfin.Api.Controllers
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId);
if (string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal)) if (!string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal))
{
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
_userManager.UpdateConfiguration(user.Id, updateUser.Configuration);
}
else
{ {
await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false); await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false);
_userManager.UpdateConfiguration(updateUser.Id, updateUser.Configuration);
} }
await _userManager.UpdateConfigurationAsync(user.Id, updateUser.Configuration).ConfigureAwait(false);
return NoContent(); return NoContent();
} }
@ -409,7 +405,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult UpdateUserPolicy( public async Task<ActionResult> UpdateUserPolicy(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromBody] UserPolicy newPolicy) [FromBody] UserPolicy newPolicy)
{ {
@ -447,7 +443,7 @@ namespace Jellyfin.Api.Controllers
_sessionManager.RevokeUserTokens(user.Id, currentToken); _sessionManager.RevokeUserTokens(user.Id, currentToken);
} }
_userManager.UpdatePolicy(userId, newPolicy); await _userManager.UpdatePolicyAsync(userId, newPolicy).ConfigureAwait(false);
return NoContent(); return NoContent();
} }
@ -464,7 +460,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult UpdateUserConfiguration( public async Task<ActionResult> UpdateUserConfiguration(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromBody] UserConfiguration userConfig) [FromBody] UserConfiguration userConfig)
{ {
@ -473,7 +469,7 @@ namespace Jellyfin.Api.Controllers
return Forbid("User configuration update not allowed"); return Forbid("User configuration update not allowed");
} }
_userManager.UpdateConfiguration(userId, userConfig); await _userManager.UpdateConfigurationAsync(userId, userConfig).ConfigureAwait(false);
return NoContent(); return NoContent();
} }
@ -534,6 +530,33 @@ namespace Jellyfin.Api.Controllers
return result; 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) private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
{ {
var users = _userManager.Users; var users = _userManager.Users;

View File

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -251,7 +252,7 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="userId">User id.</param> /// <param name="userId">User id.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, SortName, Studios, Taglines.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param> /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param>
/// <param name="isPlayed">Filter by items that are played, or not.</param> /// <param name="isPlayed">Filter by items that are played, or not.</param>
/// <param name="enableImages">Optional. include image information in output.</param> /// <param name="enableImages">Optional. include image information in output.</param>
@ -267,12 +268,12 @@ namespace Jellyfin.Api.Controllers
public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia( public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromQuery] Guid? parentId, [FromQuery] Guid? parentId,
[FromQuery] string? fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? includeItemTypes, [FromQuery] string? includeItemTypes,
[FromQuery] bool? isPlayed, [FromQuery] bool? isPlayed,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int limit = 20, [FromQuery] int limit = 20,
[FromQuery] bool groupItems = true) [FromQuery] bool groupItems = true)
@ -287,8 +288,7 @@ namespace Jellyfin.Api.Controllers
} }
} }
var dtoOptions = new DtoOptions() var dtoOptions = new DtoOptions { Fields = fields }
.AddItemFields(fields)
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);

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