Merge branch 'master' into Comment1

pull/4136/head
BaronGreenback 4 years ago committed by GitHub
commit 27bb17ef9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

@ -28,15 +28,13 @@ jobs:
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"
## Generate npm api client
# Unstable
- task: CmdLine@2
displayName: 'Build unstable typescript axios client'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
## Authenticate with npm registry
- task: npmAuthenticate@0
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
displayName: 'Build stable typescript axios client'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
@ -51,17 +49,6 @@ jobs:
workingDir: ./apiclient/generated/typescript/axios
## 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
displayName: 'Publish stable typescript axios client'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')

@ -63,6 +63,7 @@ jobs:
sshEndpoint: repository
sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
contents: '**'
targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
- job: OpenAPISpec
dependsOn: Test
@ -166,7 +167,7 @@ jobs:
inputs:
sshEndpoint: repository
runOptions: 'commands'
commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable &
- task: SSH@0
displayName: 'Update Stable Repository'
@ -175,7 +176,7 @@ jobs:
inputs:
sshEndpoint: repository
runOptions: 'commands'
commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
- job: PublishNuget
displayName: 'Publish NuGet packages'

@ -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

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

@ -123,7 +123,7 @@ namespace Emby.Dlna.Didl
{
foreach (var att in profile.XmlRootAttributes)
{
var parts = att.Name.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries);
var parts = att.Name.Split(':', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2)
{
writer.WriteAttributeString(parts[0], parts[1], null, att.Value);

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

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

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

@ -326,7 +326,7 @@ namespace Emby.Dlna.PlayTo
public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken)
{
_logger.LogDebug("{0} - Received PlayRequest: {1}", this._session.DeviceName, command.PlayCommand);
_logger.LogDebug("{0} - Received PlayRequest: {1}", _session.DeviceName, command.PlayCommand);
var user = command.ControllingUserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(command.ControllingUserId);
@ -339,7 +339,7 @@ namespace Emby.Dlna.PlayTo
var startIndex = command.StartIndex ?? 0;
if (startIndex > 0)
{
items = items.Skip(startIndex).ToList();
items = items.GetRange(startIndex, items.Count - startIndex);
}
var playlist = new List<PlaylistItem>();

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

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

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

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

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

@ -250,21 +250,16 @@ namespace Emby.Server.Implementations.Channels
var all = channels;
var totalCount = all.Count;
if (query.StartIndex.HasValue)
if (query.StartIndex.HasValue || query.Limit.HasValue)
{
all = all.Skip(query.StartIndex.Value).ToList();
int startIndex = query.StartIndex ?? 0;
int count = query.Limit == null ? totalCount - startIndex : Math.Min(query.Limit.Value, totalCount - startIndex);
all = all.GetRange(startIndex, count);
}
if (query.Limit.HasValue)
{
all = all.Take(query.Limit.Value).ToList();
}
var returnItems = all.ToArray();
if (query.RefreshLatestChannelItems)
{
foreach (var item in returnItems)
foreach (var item in all)
{
RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult();
}
@ -272,7 +267,7 @@ namespace Emby.Server.Implementations.Channels
return new QueryResult<Channel>
{
Items = returnItems,
Items = all,
TotalRecordCount = totalCount
};
}
@ -543,7 +538,7 @@ namespace Emby.Server.Implementations.Channels
return _libraryManager.GetItemIds(
new InternalItemsQuery
{
IncludeItemTypes = new[] { typeof(Channel).Name },
IncludeItemTypes = new[] { nameof(Channel) },
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
}).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray();
}

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

@ -2403,11 +2403,11 @@ namespace Emby.Server.Implementations.Data
if (string.IsNullOrEmpty(item.OfficialRating))
{
builder.Append("((OfficialRating is null) * 10)");
builder.Append("(OfficialRating is null * 10)");
}
else
{
builder.Append("((OfficialRating=@ItemOfficialRating) * 10)");
builder.Append("(OfficialRating=@ItemOfficialRating * 10)");
}
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 )");
}
//// genres, tags
builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId)) * 10)");
// genres, tags, studios, person, year?
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");
@ -3914,7 +3932,7 @@ namespace Emby.Server.Implementations.Data
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
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)
{
@ -4755,29 +4773,29 @@ namespace Emby.Server.Implementations.Data
{
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;
@ -4832,12 +4850,12 @@ namespace Emby.Server.Implementations.Data
var types = new[]
{
typeof(Episode).Name,
typeof(Video).Name,
typeof(Movie).Name,
typeof(MusicVideo).Name,
typeof(Series).Name,
typeof(Season).Name
nameof(Episode),
nameof(Video),
nameof(Movie),
nameof(MusicVideo),
nameof(Series),
nameof(Season)
};
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();
var commandText = "select Distinct Name from People";
var commandText = new StringBuilder("select Distinct p.Name from People p");
if (query.User != null && query.IsFavorite.HasValue)
{
commandText.Append(" LEFT JOIN TypedBaseItems tbi ON tbi.Name=p.Name AND tbi.Type='");
commandText.Append(typeof(Person).FullName);
commandText.Append("' LEFT JOIN UserDatas ON tbi.UserDataKey=key AND userId=@UserId");
}
var whereClauses = GetPeopleWhereClauses(query, null);
if (whereClauses.Count != 0)
{
commandText += " where " + string.Join(" AND ", whereClauses);
commandText.Append(" where ").Append(string.Join(" AND ", whereClauses));
}
commandText += " order by ListOrder";
commandText.Append(" order by ListOrder");
if (query.Limit > 0)
{
commandText += " LIMIT " + query.Limit;
commandText.Append(" LIMIT ").Append(query.Limit);
}
using (var connection = GetConnection(true))
{
var list = new List<string>();
using (var statement = PrepareStatement(connection, commandText))
using (var statement = PrepareStatement(connection, commandText.ToString()))
{
// Run this again to bind the params
GetPeopleWhereClauses(query, statement);
@ -5045,7 +5070,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
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);
@ -5087,19 +5112,13 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (!query.ItemId.Equals(Guid.Empty))
{
whereClauses.Add("ItemId=@ItemId");
if (statement != null)
{
statement.TryBind("@ItemId", query.ItemId.ToByteArray());
}
statement?.TryBind("@ItemId", query.ItemId.ToByteArray());
}
if (!query.AppearsInItemId.Equals(Guid.Empty))
{
whereClauses.Add("Name in (Select Name from People where ItemId=@AppearsInItemId)");
if (statement != null)
{
statement.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray());
}
whereClauses.Add("p.Name in (Select Name from People where ItemId=@AppearsInItemId)");
statement?.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray());
}
var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList();
@ -5107,10 +5126,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (queryPersonTypes.Count == 1)
{
whereClauses.Add("PersonType=@PersonType");
if (statement != null)
{
statement.TryBind("@PersonType", queryPersonTypes[0]);
}
statement?.TryBind("@PersonType", queryPersonTypes[0]);
}
else if (queryPersonTypes.Count > 1)
{
@ -5124,10 +5140,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (queryExcludePersonTypes.Count == 1)
{
whereClauses.Add("PersonType<>@PersonType");
if (statement != null)
{
statement.TryBind("@PersonType", queryExcludePersonTypes[0]);
}
statement?.TryBind("@PersonType", queryExcludePersonTypes[0]);
}
else if (queryExcludePersonTypes.Count > 1)
{
@ -5139,19 +5152,24 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
if (query.MaxListOrder.HasValue)
{
whereClauses.Add("ListOrder<=@MaxListOrder");
if (statement != null)
{
statement.TryBind("@MaxListOrder", query.MaxListOrder.Value);
}
statement?.TryBind("@MaxListOrder", query.MaxListOrder.Value);
}
if (!string.IsNullOrWhiteSpace(query.NameContains))
{
whereClauses.Add("Name like @NameContains");
if (statement != null)
{
statement.TryBind("@NameContains", "%" + query.NameContains + "%");
}
whereClauses.Add("p.Name like @NameContains");
statement?.TryBind("@NameContains", "%" + query.NameContains + "%");
}
if (query.IsFavorite.HasValue)
{
whereClauses.Add("isFavorite=@IsFavorite");
statement?.TryBind("@IsFavorite", query.IsFavorite.Value);
}
if (query.User != null)
{
statement?.TryBind("@UserId", query.User.InternalId);
}
return whereClauses;
@ -5420,6 +5438,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
NameStartsWithOrGreater = query.NameStartsWithOrGreater,
Tags = query.Tags,
OfficialRatings = query.OfficialRatings,
StudioIds = query.StudioIds,
GenreIds = query.GenreIds,
Genres = query.Genres,
Years = query.Years,

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

@ -1,6 +1,7 @@
#pragma warning disable CS1591
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Http;
@ -19,12 +20,12 @@ namespace Emby.Server.Implementations.HttpServer.Security
public AuthorizationInfo Authenticate(HttpRequest request)
{
var auth = _authorizationContext.GetAuthorizationInfo(request);
if (auth?.User == null)
if (!auth.IsAuthenticated)
{
return null;
throw new AuthenticationException("Invalid token.");
}
if (auth.User.HasPermission(PermissionKind.IsDisabled))
if (auth.User?.HasPermission(PermissionKind.IsDisabled) ?? false)
{
throw new SecurityException("User account has been disabled.");
}

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

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

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

@ -2440,6 +2440,21 @@ namespace Emby.Server.Implementations.Library
new SubtitleResolver(BaseItem.LocalizationManager).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files);
}
public BaseItem GetParentItem(string parentId, Guid? userId)
{
if (!string.IsNullOrEmpty(parentId))
{
return GetItemById(new Guid(parentId));
}
if (userId.HasValue && userId != Guid.Empty)
{
return GetUserRootFolder();
}
return RootFolder;
}
/// <inheritdoc />
public bool IsVideoFile(string path)
{

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

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

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

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

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

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

@ -15,6 +15,7 @@ using System.Threading.Tasks;
using MediaBrowser.Common;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
@ -33,17 +34,20 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private readonly IHttpClientFactory _httpClientFactory;
private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
private readonly IApplicationHost _appHost;
private readonly ICryptoProvider _cryptoProvider;
public SchedulesDirect(
ILogger<SchedulesDirect> logger,
IJsonSerializer jsonSerializer,
IHttpClientFactory httpClientFactory,
IApplicationHost appHost)
IApplicationHost appHost,
ICryptoProvider cryptoProvider)
{
_logger = logger;
_jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
_appHost = appHost;
_cryptoProvider = cryptoProvider;
}
private string UserAgent => _appHost.ApplicationUserAgent;
@ -642,7 +646,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
CancellationToken cancellationToken)
{
using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token");
options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
var hashedPasswordBytes = _cryptoProvider.ComputeHash("SHA1", Encoding.ASCII.GetBytes(password), Array.Empty<byte>());
string hashedPassword = Hex.Encode(hashedPasswordBytes);
options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + hashedPassword + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json);
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);

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

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

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

@ -65,7 +65,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
var channelIdPrefix = GetFullChannelIdPrefix(info);
return await new M3uParser(Logger, _httpClientFactory, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false);
return await new M3uParser(Logger, _httpClientFactory, _appHost)
.Parse(info, channelIdPrefix, cancellationToken)
.ConfigureAwait(false);
}
public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken)
@ -126,7 +128,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public async Task Validate(TunerHostInfo info)
{
using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false))
using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info, CancellationToken.None).ConfigureAwait(false))
{
}
}

@ -13,6 +13,7 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.LiveTv;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts
@ -30,12 +31,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
_appHost = appHost;
}
public async Task<List<ChannelInfo>> Parse(string url, string channelIdPrefix, string tunerHostId, CancellationToken cancellationToken)
public async Task<List<ChannelInfo>> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken)
{
// Read the file and display it line by line.
using (var reader = new StreamReader(await GetListingsStream(url, cancellationToken).ConfigureAwait(false)))
using (var reader = new StreamReader(await GetListingsStream(info, cancellationToken).ConfigureAwait(false)))
{
return GetChannels(reader, channelIdPrefix, tunerHostId);
return GetChannels(reader, channelIdPrefix, info.Id);
}
}
@ -48,15 +49,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
}
}
public Task<Stream> GetListingsStream(string url, CancellationToken cancellationToken)
public async Task<Stream> GetListingsStream(TunerHostInfo info, CancellationToken cancellationToken)
{
if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
if (info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
return _httpClientFactory.CreateClient(NamedClient.Default)
.GetStreamAsync(url);
using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url);
if (!string.IsNullOrEmpty(info.UserAgent))
{
requestMessage.Headers.UserAgent.TryParseAdd(info.UserAgent);
}
var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.SendAsync(requestMessage, cancellationToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
}
return Task.FromResult((Stream)File.OpenRead(url));
return File.OpenRead(info.Url);
}
private const string ExtInfPrefix = "#EXTINF:";

@ -55,7 +55,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
var typeName = GetType().Name;
Logger.LogInformation("Opening " + typeName + " Live stream from {0}", url);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
// Response stream is disposed manually.
var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
.ConfigureAwait(false);
@ -121,6 +122,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
}
}
public string GetFilePath()
{
return TempFilePath;
}
private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
{
return Task.Run(async () =>

@ -113,5 +113,7 @@
"TasksChannelsCategory": "Internetové kanály",
"TasksApplicationCategory": "Aplikace",
"TasksLibraryCategory": "Knihovna",
"TasksMaintenanceCategory": "Údržba"
"TasksMaintenanceCategory": "Údržba",
"TaskCleanActivityLogDescription": "Smazat záznamy o aktivitě, které jsou starší než zadaná doba.",
"TaskCleanActivityLog": "Smazat záznam aktivity"
}

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

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

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

@ -77,7 +77,7 @@
"SubtitleDownloadFailureFromForItem": "Fallo de descarga de subtítulos desde {0} para {1}",
"Sync": "Sincronizar",
"System": "Sistema",
"TvShows": "Programas de televisión",
"TvShows": "Series",
"User": "Usuario",
"UserCreatedWithName": "El usuario {0} ha sido creado",
"UserDeletedWithName": "El usuario {0} ha sido borrado",
@ -113,5 +113,7 @@
"TaskRefreshChannels": "Actualizar canales",
"TaskRefreshChannelsDescription": "Actualiza la información de los canales de internet.",
"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"
}

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

@ -1,7 +1,7 @@
{
"VersionNumber": "Bersyon {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}",
"UserStartedPlayingItemWithValues": "Si {0} ay nagplaplay ng {1} sa {2}",
"UserPolicyUpdatedWithName": "Ang user policy ay naiupdate para kay {0}",
@ -61,8 +61,8 @@
"Latest": "Pinakabago",
"LabelRunningTimeValue": "Oras: {0}",
"LabelIpAddressValue": "Ang IP Address ay {0}",
"ItemRemovedWithName": "Naitanggal ang {0} sa library",
"ItemAddedWithName": "Naidagdag ang {0} sa library",
"ItemRemovedWithName": "Naitanggal ang {0} sa librerya",
"ItemAddedWithName": "Naidagdag ang {0} sa librerya",
"Inherit": "Manahin",
"HeaderRecordingGroups": "Pagtatalang Grupo",
"HeaderNextUp": "Susunod",
@ -90,12 +90,29 @@
"Application": "Aplikasyon",
"AppDeviceValues": "Aplikasyon: {0}, Aparato: {1}",
"Albums": "Albums",
"TaskRefreshLibrary": "Suriin ang nasa librerya",
"TaskRefreshChapterImagesDescription": "Gumawa ng larawan para sa mga pelikula na may kabanata",
"TaskRefreshLibrary": "Suriin and Librerya ng Medya",
"TaskRefreshChapterImagesDescription": "Gumawa ng larawan para sa mga pelikula na may kabanata.",
"TaskRefreshChapterImages": "Kunin ang mga larawan ng kabanata",
"TaskCleanCacheDescription": "Tanggalin ang mga cache file na hindi na kailangan ng systema.",
"TasksChannelsCategory": "Palabas sa internet",
"TasksLibraryCategory": "Librerya",
"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."
}

@ -113,5 +113,7 @@
"TaskCleanCache": "Vider le répertoire cache",
"TasksApplicationCategory": "Application",
"TasksLibraryCategory": "Bibliothèque",
"TasksMaintenanceCategory": "Maintenance"
"TasksMaintenanceCategory": "Maintenance",
"TaskCleanActivityLogDescription": "Supprime les entrées du journal d'activité antérieures à l'âge configuré.",
"TaskCleanActivityLog": "Nettoyer le journal d'activité"
}

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

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

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

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

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

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

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

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

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

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

@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi",
"Channels": "Kanallar",
"ChapterNameValue": "Bölüm {0}",
"Collections": "Koleksiyonlar",
"Collections": "Koleksiyon",
"DeviceOfflineWithName": "{0} bağlantısı kesildi",
"DeviceOnlineWithName": "{0} bağlı",
"FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu",
@ -23,7 +23,7 @@
"HeaderFavoriteShows": "Favori Diziler",
"HeaderFavoriteSongs": "Favori Şarkılar",
"HeaderLiveTV": "Canlı TV",
"HeaderNextUp": "Sonraki hafta",
"HeaderNextUp": "Gelecek Hafta",
"HeaderRecordingGroups": "Kayıt Grupları",
"HomeVideos": "Ev videoları",
"Inherit": "Devral",
@ -113,5 +113,7 @@
"TaskRefreshLibrary": "Medya Kütüphanesini Tara",
"TaskRefreshChapterImagesDescription": "Sahnelere ayrılmış videolar için küçük resimler oluştur.",
"TaskRefreshChapterImages": "Bölüm Resimlerini Çıkar",
"TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler."
"TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler.",
"TaskCleanActivityLog": "İşlem Günlüğünü Temizle",
"TaskCleanActivityLogDescription": "Belirtilen sureden daha eski etkinlik log kayıtları silindi."
}

@ -112,5 +112,7 @@
"Books": "Sách",
"AuthenticationSucceededWithUserName": "{0} xác thực thành công",
"Application": "Ứng Dụng",
"AppDeviceValues": "Ứng Dụng: {0}, Thiết Bị: {1}"
"AppDeviceValues": "Ứng Dụng: {0}, Thiết Bị: {1}",
"TaskCleanActivityLogDescription": "Xóa các mục nhật ký hoạt động cũ hơn độ tuổi đã cài đặt.",
"TaskCleanActivityLog": "Xóa Nhật Ký Hoạt Động"
}

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

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

@ -703,7 +703,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
MaxRuntimeTicks = info.MaxRuntimeTicks
};
if (info.Type.Equals(typeof(DailyTrigger).Name, StringComparison.OrdinalIgnoreCase))
if (info.Type.Equals(nameof(DailyTrigger), StringComparison.OrdinalIgnoreCase))
{
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)
{
@ -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)
{
@ -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();
}

@ -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>();
}
}
}

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

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

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

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

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

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

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

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

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

@ -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);
}
}
}

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

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@ -107,7 +108,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="sortOrder">Optional. Sort Order - Ascending,Descending.</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="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>
/// <returns>
/// 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? limit,
[FromQuery] string? sortOrder,
[FromQuery] ItemFilter[] filters,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] string? sortBy,
[FromQuery] string? fields)
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value)
@ -136,8 +137,7 @@ namespace Jellyfin.Api.Controllers
ChannelIds = new[] { channelId },
ParentId = folderId ?? Guid.Empty,
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
DtoOptions = new DtoOptions()
.AddItemFields(fields)
DtoOptions = new DtoOptions { Fields = fields }
};
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="limit">Optional. The maximum number of records to return.</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>
/// <response code="200">Latest channel items returned.</response>
/// <returns>
@ -196,8 +196,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] ItemFilter[] filters,
[FromQuery] string? fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? channelIds)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@ -213,8 +213,7 @@ namespace Jellyfin.Api.Controllers
.Where(i => !string.IsNullOrWhiteSpace(i))
.Select(i => new Guid(i))
.ToArray(),
DtoOptions = new DtoOptions()
.AddItemFields(fields)
DtoOptions = new DtoOptions { Fields = fields }
};
foreach (var filter in filters)

@ -83,14 +83,14 @@ namespace Jellyfin.Api.Controllers
/// Adds items to a collection.
/// </summary>
/// <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>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("{collectionId}/Items")]
[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();
}
@ -98,14 +98,14 @@ namespace Jellyfin.Api.Controllers
/// Removes items from a collection.
/// </summary>
/// <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>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpDelete("{collectionId}/Items")]
[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();
}
}

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

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

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

@ -295,6 +295,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
@ -351,6 +352,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
[FromQuery] int? maxStreamingBitrate,
[FromQuery] int? audioBitRate,
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
@ -403,7 +405,7 @@ namespace Jellyfin.Api.Controllers
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate,
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = audioChannels,
Profile = profile,
@ -623,6 +625,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
@ -677,6 +680,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
[FromQuery] int? maxStreamingBitrate,
[FromQuery] int? audioBitRate,
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
@ -729,7 +733,7 @@ namespace Jellyfin.Api.Controllers
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate,
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = audioChannels,
Profile = profile,
@ -959,6 +963,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
@ -1017,6 +1022,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? breakOnNonKeyFrames,
[FromQuery] int? audioSampleRate,
[FromQuery] int? maxAudioBitDepth,
[FromQuery] int? maxStreamingBitrate,
[FromQuery] int? audioBitRate,
[FromQuery] int? audioChannels,
[FromQuery] int? maxAudioChannels,
@ -1069,7 +1075,7 @@ namespace Jellyfin.Api.Controllers
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
AudioSampleRate = audioSampleRate,
MaxAudioChannels = maxAudioChannels,
AudioBitRate = audioBitRate,
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = audioChannels,
Profile = profile,

@ -1,15 +1,16 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@ -47,30 +48,16 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Gets all genres from a given item, folder, or the entire library.
/// </summary>
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">The search term.</param>
/// <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="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
/// <param name="filters">Optional. Specify additional filters to apply.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
/// <param name="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
/// <param name="userId">User id.</param>
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
@ -82,30 +69,16 @@ namespace Jellyfin.Api.Controllers
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetGenres(
[FromQuery] double? minCommunityRating,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
[FromQuery] string? parentId,
[FromQuery] string? fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
[FromQuery] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes,
[FromQuery] string? genres,
[FromQuery] string? genreIds,
[FromQuery] string? officialRatings,
[FromQuery] string? tags,
[FromQuery] string? years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes,
[FromQuery] string? person,
[FromQuery] string? personIds,
[FromQuery] string? personTypes,
[FromQuery] string? studios,
[FromQuery] string? studioIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
@ -113,45 +86,24 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
User? user = null;
BaseItem parentItem;
User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null;
if (userId.HasValue && !userId.Equals(Guid.Empty))
{
user = _userManager.GetUserById(userId.Value);
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
}
else
{
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
}
var parentItem = _libraryManager.GetParentItem(parentId, userId);
var query = new InternalItemsQuery(user)
{
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
Tags = RequestHelpers.Split(tags, '|', true),
OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
Genres = RequestHelpers.Split(genres, '|', true),
GenreIds = RequestHelpers.GetGuids(genreIds),
StudioIds = RequestHelpers.GetGuids(studioIds),
Person = person,
PersonIds = RequestHelpers.GetGuids(personIds),
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(),
MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
EnableTotalRecordCount = enableTotalRecordCount
@ -169,87 +121,20 @@ namespace Jellyfin.Api.Controllers
}
}
// Studios
if (!string.IsNullOrEmpty(studios))
QueryResult<(BaseItem, ItemCounts)> result;
if (parentItem is ICollectionFolder parentCollectionFolder
&& (string.Equals(parentCollectionFolder.CollectionType, CollectionType.Music, StringComparison.Ordinal)
|| string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal)))
{
query.StudioIds = studios.Split('|')
.Select(i =>
{
try
{
return _libraryManager.GetStudio(i);
}
catch
{
return null;
}
}).Where(i => i != null)
.Select(i => i!.Id)
.ToArray();
result = _libraryManager.GetMusicGenres(query);
}
foreach (var filter in filters)
else
{
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;
}
result = _libraryManager.GetGenres(query);
}
var result = new QueryResult<(BaseItem, ItemCounts)>();
var dtos = result.Items.Select(i =>
{
var (baseItem, counts) = i;
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
if (!string.IsNullOrWhiteSpace(includeItemTypes))
{
dto.ChildCount = counts.ItemCount;
dto.ProgramCount = counts.ProgramCount;
dto.SeriesCount = counts.SeriesCount;
dto.EpisodeCount = counts.EpisodeCount;
dto.MovieCount = counts.MovieCount;
dto.TrailerCount = counts.TrailerCount;
dto.AlbumCount = counts.AlbumCount;
dto.SongCount = counts.SongCount;
dto.ArtistCount = counts.ArtistCount;
}
return dto;
});
return new QueryResult<BaseItemDto>
{
Items = dtos.ToArray(),
TotalRecordCount = result.TotalRecordCount
};
var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes);
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
}
/// <summary>

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

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

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

@ -12,6 +12,7 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.LibraryDtos;
using Jellyfin.Data.Entities;
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>
/// <response code="200">Similar items returned.</response>
/// <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("Albums/{itemId}/Similar", Name = "GetSimilarAlbums2")]
[HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows2")]
[HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies2")]
[HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers2")]
[HttpGet("Albums/{itemId}/Similar")]
[HttpGet("Shows/{itemId}/Similar")]
[HttpGet("Movies/{itemId}/Similar")]
[HttpGet("Trailers/{itemId}/Similar")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
@ -693,7 +694,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? excludeArtistIds,
[FromQuery] Guid? userId,
[FromQuery] int? limit,
[FromQuery] string? fields)
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
{
var item = itemId.Equals(Guid.Empty)
? (!userId.Equals(Guid.Empty)
@ -701,33 +702,71 @@ namespace Jellyfin.Api.Controllers
: _libraryManager.RootFolder)
: _libraryManager.GetItemById(itemId);
if (item is Episode || (item is IItemByName && !(item is MusicArtist)))
{
return new QueryResult<BaseItemDto>();
}
var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value)
: null;
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request);
var program = item as IHasProgramAttributes;
var isMovie = item is MediaBrowser.Controller.Entities.Movies.Movie || (program != null && program.IsMovie) || item is Trailer;
if (program != null && program.IsSeries)
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)
{
return GetSimilarItemsResult(
item,
excludeArtistIds,
userId,
limit,
fields,
new[] { nameof(Series) },
false);
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);
}
if (item is MediaBrowser.Controller.Entities.TV.Episode || (item is IItemByName && !(item is MusicArtist)))
var query = new InternalItemsQuery(user)
{
return new QueryResult<BaseItemDto>();
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);
}
return GetSimilarItemsResult(
item,
excludeArtistIds,
userId,
limit,
fields,
new[] { item.GetType().Name },
isMovie);
List<BaseItem> itemsResult = _libraryManager.GetItemList(query);
var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
return new QueryResult<BaseItemDto>
{
Items = returnList,
TotalRecordCount = itemsResult.Count
};
}
/// <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)
{
return contentType switch

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

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

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

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

@ -1,16 +1,17 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@ -47,30 +48,16 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Gets all music genres from a given item, folder, or the entire library.
/// </summary>
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">The search term.</param>
/// <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="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
/// <param name="filters">Optional. Specify additional filters to apply.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
/// <param name="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
/// <param name="userId">User id.</param>
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
@ -80,31 +67,18 @@ namespace Jellyfin.Api.Controllers
/// <response code="200">Music genres returned.</response>
/// <returns>An <see cref="OkResult"/> containing the queryresult of music genres.</returns>
[HttpGet]
[Obsolete("Use GetGenres instead")]
public ActionResult<QueryResult<BaseItemDto>> GetMusicGenres(
[FromQuery] double? minCommunityRating,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
[FromQuery] string? parentId,
[FromQuery] string? fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
[FromQuery] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes,
[FromQuery] string? genres,
[FromQuery] string? genreIds,
[FromQuery] string? officialRatings,
[FromQuery] string? tags,
[FromQuery] string? years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes,
[FromQuery] string? person,
[FromQuery] string? personIds,
[FromQuery] string? personTypes,
[FromQuery] string? studios,
[FromQuery] string? studioIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
@ -112,45 +86,24 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
User? user = null;
BaseItem parentItem;
User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null;
if (userId.HasValue && !userId.Equals(Guid.Empty))
{
user = _userManager.GetUserById(userId.Value);
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
}
else
{
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
}
var parentItem = _libraryManager.GetParentItem(parentId, userId);
var query = new InternalItemsQuery(user)
{
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
Tags = RequestHelpers.Split(tags, '|', true),
OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
Genres = RequestHelpers.Split(genres, '|', true),
GenreIds = RequestHelpers.GetGuids(genreIds),
StudioIds = RequestHelpers.GetGuids(studioIds),
Person = person,
PersonIds = RequestHelpers.GetGuids(personIds),
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(),
MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
EnableTotalRecordCount = enableTotalRecordCount
@ -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 dtos = result.Items.Select(i =>
{
var (baseItem, counts) = i;
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
if (!string.IsNullOrWhiteSpace(includeItemTypes))
{
dto.ChildCount = counts.ItemCount;
dto.ProgramCount = counts.ProgramCount;
dto.SeriesCount = counts.SeriesCount;
dto.EpisodeCount = counts.EpisodeCount;
dto.MovieCount = counts.MovieCount;
dto.TrailerCount = counts.TrailerCount;
dto.AlbumCount = counts.AlbumCount;
dto.SongCount = counts.SongCount;
dto.ArtistCount = counts.ArtistCount;
}
return dto;
});
return new QueryResult<BaseItemDto>
{
Items = dtos.ToArray(),
TotalRecordCount = result.TotalRecordCount
};
var shouldIncludeItemTypes = !string.IsNullOrWhiteSpace(includeItemTypes);
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
}
/// <summary>

@ -1,15 +1,16 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@ -26,6 +27,7 @@ namespace Jellyfin.Api.Controllers
private readonly ILibraryManager _libraryManager;
private readonly IDtoService _dtoService;
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataManager;
/// <summary>
/// Initializes a new instance of the <see cref="PersonsController"/> class.
@ -33,221 +35,81 @@ namespace Jellyfin.Api.Controllers
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
public PersonsController(
ILibraryManager libraryManager,
IDtoService dtoService,
IUserManager userManager)
IUserManager userManager,
IUserDataManager userDataManager)
{
_libraryManager = libraryManager;
_dtoService = dtoService;
_userManager = userManager;
_userDataManager = userDataManager;
}
/// <summary>
/// Gets all persons from a given item, folder, or the entire library.
/// Gets all persons.
/// </summary>
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">The search term.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</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. userId is required.</param>
/// <param name="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
/// <param name="excludePersonTypes">Optional. If specified results will be filtered to exclude those containing the specified PersonType. Allows multiple, comma-delimited.</param>
/// <param name="personTypes">Optional. If specified results will be filtered to include only those containing the specified PersonType. Allows multiple, comma-delimited.</param>
/// <param name="appearsInItemId">Optional. If specified, person results will be filtered on items related to said persons.</param>
/// <param name="userId">User id.</param>
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
/// <param name="enableTotalRecordCount">Optional. Include total record count.</param>
/// <response code="200">Persons returned.</response>
/// <returns>An <see cref="OkResult"/> containing the queryresult of persons.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetPersons(
[FromQuery] double? minCommunityRating,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
[FromQuery] string? parentId,
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
[FromQuery] ItemFilter[] filters,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes,
[FromQuery] string? genres,
[FromQuery] string? genreIds,
[FromQuery] string? officialRatings,
[FromQuery] string? tags,
[FromQuery] string? years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes,
[FromQuery] string? person,
[FromQuery] string? personIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? excludePersonTypes,
[FromQuery] string? personTypes,
[FromQuery] string? studios,
[FromQuery] string? studioIds,
[FromQuery] string? appearsInItemId,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
[FromQuery] bool? enableImages = true)
{
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
User? user = null;
BaseItem parentItem;
if (userId.HasValue && !userId.Equals(Guid.Empty))
{
user = _userManager.GetUserById(userId.Value);
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
}
else
{
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
}
var query = new InternalItemsQuery(user)
var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite);
var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery
{
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
Tags = RequestHelpers.Split(tags, '|', true),
OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
Genres = RequestHelpers.Split(genres, '|', true),
GenreIds = RequestHelpers.GetGuids(genreIds),
StudioIds = RequestHelpers.GetGuids(studioIds),
Person = person,
PersonIds = RequestHelpers.GetGuids(personIds),
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(),
MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
EnableTotalRecordCount = enableTotalRecordCount
};
if (!string.IsNullOrWhiteSpace(parentId))
{
if (parentItem is Folder)
{
query.AncestorIds = new[] { new Guid(parentId) };
}
else
{
query.ItemIds = new[] { new Guid(parentId) };
}
}
// Studios
if (!string.IsNullOrEmpty(studios))
{
query.StudioIds = studios.Split('|')
.Select(i =>
{
try
{
return _libraryManager.GetStudio(i);
}
catch
{
return null;
}
}).Where(i => i != null)
.Select(i => i!.Id)
.ToArray();
}
foreach (var filter in filters)
{
switch (filter)
{
case ItemFilter.Dislikes:
query.IsLiked = false;
break;
case ItemFilter.IsFavorite:
query.IsFavorite = true;
break;
case ItemFilter.IsFavoriteOrLikes:
query.IsFavoriteOrLiked = true;
break;
case ItemFilter.IsFolder:
query.IsFolder = true;
break;
case ItemFilter.IsNotFolder:
query.IsFolder = false;
break;
case ItemFilter.IsPlayed:
query.IsPlayed = true;
break;
case ItemFilter.IsResumable:
query.IsResumable = true;
break;
case ItemFilter.IsUnplayed:
query.IsPlayed = false;
break;
case ItemFilter.Likes:
query.IsLiked = true;
break;
}
}
var result = new QueryResult<(BaseItem, ItemCounts)>();
var dtos = result.Items.Select(i =>
{
var (baseItem, counts) = i;
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
if (!string.IsNullOrWhiteSpace(includeItemTypes))
{
dto.ChildCount = counts.ItemCount;
dto.ProgramCount = counts.ProgramCount;
dto.SeriesCount = counts.SeriesCount;
dto.EpisodeCount = counts.EpisodeCount;
dto.MovieCount = counts.MovieCount;
dto.TrailerCount = counts.TrailerCount;
dto.AlbumCount = counts.AlbumCount;
dto.SongCount = counts.SongCount;
dto.ArtistCount = counts.ArtistCount;
}
return dto;
ExcludePersonTypes = RequestHelpers.Split(excludePersonTypes, ',', true),
NameContains = searchTerm,
User = user,
IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite,
AppearsInItemId = string.IsNullOrEmpty(appearsInItemId) ? Guid.Empty : Guid.Parse(appearsInItemId),
Limit = limit ?? 0
});
return new QueryResult<BaseItemDto>
{
Items = dtos.ToArray(),
TotalRecordCount = result.TotalRecordCount
Items = peopleItems.Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user)).ToArray(),
TotalRecordCount = peopleItems.Count
};
}

@ -5,11 +5,13 @@ using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.PlaylistDtos;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Playlists;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
@ -133,7 +135,7 @@ namespace Jellyfin.Api.Controllers
/// <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="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="enableUserData">Optional. Include user data.</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] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes)
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
{
var playlist = (Playlist)_libraryManager.GetItemById(playlistId);
if (playlist == null)
@ -175,8 +177,7 @@ namespace Jellyfin.Api.Controllers
items = items.Take(limit.Value).ToArray();
}
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);

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

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

@ -1,14 +1,15 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@ -45,30 +46,17 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Gets all studios from a given item, folder, or the entire library.
/// </summary>
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">Optional. Search term.</param>
/// <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="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
/// <param name="filters">Optional. Specify additional filters to apply.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
/// <param name="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param>
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
/// <param name="userId">User id.</param>
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
@ -80,30 +68,17 @@ namespace Jellyfin.Api.Controllers
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetStudios(
[FromQuery] double? minCommunityRating,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? searchTerm,
[FromQuery] string? parentId,
[FromQuery] string? fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
[FromQuery] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes,
[FromQuery] string? genres,
[FromQuery] string? genreIds,
[FromQuery] string? officialRatings,
[FromQuery] string? tags,
[FromQuery] string? years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes,
[FromQuery] string? person,
[FromQuery] string? personIds,
[FromQuery] string? personTypes,
[FromQuery] string? studios,
[FromQuery] string? studioIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith,
@ -111,49 +86,27 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
User? user = null;
BaseItem parentItem;
User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null;
if (userId.HasValue && !userId.Equals(Guid.Empty))
{
user = _userManager.GetUserById(userId.Value);
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
}
else
{
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
}
var parentItem = _libraryManager.GetParentItem(parentId, userId);
var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
var query = new InternalItemsQuery(user)
{
ExcludeItemTypes = excludeItemTypesArr,
IncludeItemTypes = includeItemTypesArr,
MediaTypes = mediaTypesArr,
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
Tags = RequestHelpers.Split(tags, ',', true),
OfficialRatings = RequestHelpers.Split(officialRatings, ',', true),
Genres = RequestHelpers.Split(genres, ',', true),
GenreIds = RequestHelpers.GetGuids(genreIds),
StudioIds = RequestHelpers.GetGuids(studioIds),
Person = person,
PersonIds = RequestHelpers.GetGuids(personIds),
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
EnableTotalRecordCount = enableTotalRecordCount
@ -171,84 +124,9 @@ namespace Jellyfin.Api.Controllers
}
}
// Studios
if (!string.IsNullOrEmpty(studios))
{
query.StudioIds = studios.Split('|').Select(i =>
{
try
{
return _libraryManager.GetStudio(i);
}
catch
{
return null;
}
}).Where(i => i != null).Select(i => i!.Id)
.ToArray();
}
foreach (var filter in filters)
{
switch (filter)
{
case ItemFilter.Dislikes:
query.IsLiked = false;
break;
case ItemFilter.IsFavorite:
query.IsFavorite = true;
break;
case ItemFilter.IsFavoriteOrLikes:
query.IsFavoriteOrLiked = true;
break;
case ItemFilter.IsFolder:
query.IsFolder = true;
break;
case ItemFilter.IsNotFolder:
query.IsFolder = false;
break;
case ItemFilter.IsPlayed:
query.IsPlayed = true;
break;
case ItemFilter.IsResumable:
query.IsResumable = true;
break;
case ItemFilter.IsUnplayed:
query.IsPlayed = false;
break;
case ItemFilter.Likes:
query.IsLiked = true;
break;
}
}
var result = new QueryResult<(BaseItem, ItemCounts)>();
var dtos = result.Items.Select(i =>
{
var (baseItem, itemCounts) = i;
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
if (!string.IsNullOrWhiteSpace(includeItemTypes))
{
dto.ChildCount = itemCounts.ItemCount;
dto.ProgramCount = itemCounts.ProgramCount;
dto.SeriesCount = itemCounts.SeriesCount;
dto.EpisodeCount = itemCounts.EpisodeCount;
dto.MovieCount = itemCounts.MovieCount;
dto.TrailerCount = itemCounts.TrailerCount;
dto.AlbumCount = itemCounts.AlbumCount;
dto.SongCount = itemCounts.SongCount;
dto.ArtistCount = itemCounts.ArtistCount;
}
return dto;
});
return new QueryResult<BaseItemDto>
{
Items = dtos.ToArray(),
TotalRecordCount = result.TotalRecordCount
};
var result = _libraryManager.GetStudios(query);
var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes);
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
}
/// <summary>

@ -11,6 +11,9 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.SubtitleDtos;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
@ -21,6 +24,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Subtitles;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@ -34,6 +38,7 @@ namespace Jellyfin.Api.Controllers
[Route("")]
public class SubtitleController : BaseJellyfinApiController
{
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly ILibraryManager _libraryManager;
private readonly ISubtitleManager _subtitleManager;
private readonly ISubtitleEncoder _subtitleEncoder;
@ -46,6 +51,7 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Initializes a new instance of the <see cref="SubtitleController"/> class.
/// </summary>
/// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
/// <param name="subtitleManager">Instance of <see cref="ISubtitleManager"/> interface.</param>
/// <param name="subtitleEncoder">Instance of <see cref="ISubtitleEncoder"/> interface.</param>
@ -55,6 +61,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param>
/// <param name="logger">Instance of <see cref="ILogger{SubtitleController}"/> interface.</param>
public SubtitleController(
IServerConfigurationManager serverConfigurationManager,
ILibraryManager libraryManager,
ISubtitleManager subtitleManager,
ISubtitleEncoder subtitleEncoder,
@ -64,6 +71,7 @@ namespace Jellyfin.Api.Controllers
IAuthorizationContext authContext,
ILogger<SubtitleController> logger)
{
_serverConfigurationManager = serverConfigurationManager;
_libraryManager = libraryManager;
_subtitleManager = subtitleManager;
_subtitleEncoder = subtitleEncoder;
@ -319,6 +327,33 @@ namespace Jellyfin.Api.Controllers
return File(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
}
/// <summary>
/// Upload an external subtitle file.
/// </summary>
/// <param name="itemId">The item the subtitle belongs to.</param>
/// <param name="body">The request body.</param>
/// <response code="204">Subtitle uploaded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Videos/{itemId}/Subtitles")]
public async Task<ActionResult> UploadSubtitle(
[FromRoute, Required] Guid itemId,
[FromBody, Required] UploadSubtitleDto body)
{
var video = (Video)_libraryManager.GetItemById(itemId);
var data = Convert.FromBase64String(body.Data);
await using var memoryStream = new MemoryStream(data);
await _subtitleManager.UploadSubtitle(
video,
new SubtitleResponse
{
Format = body.Format,
Language = body.Language,
IsForced = body.IsForced,
Stream = memoryStream
}).ConfigureAwait(false);
return NoContent();
}
/// <summary>
/// Encodes a subtitle in the specified format.
/// </summary>
@ -351,5 +386,95 @@ namespace Jellyfin.Api.Controllers
copyTimestamps,
CancellationToken.None);
}
/// <summary>
/// Gets a list of available fallback font files.
/// </summary>
/// <response code="200">Information retrieved.</response>
/// <returns>An array of <see cref="FontFile"/> with the available font files.</returns>
[HttpGet("FallbackFont/Fonts")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
public IEnumerable<FontFile> GetFallbackFontList()
{
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
var fallbackFontPath = encodingOptions.FallbackFontPath;
if (!string.IsNullOrEmpty(fallbackFontPath))
{
var files = _fileSystem.GetFiles(fallbackFontPath, new[] { ".woff", ".woff2", ".ttf", ".otf" }, false, false);
var fontFiles = files
.Select(i => new FontFile
{
Name = i.Name,
Size = i.Length,
DateCreated = _fileSystem.GetCreationTimeUtc(i),
DateModified = _fileSystem.GetLastWriteTimeUtc(i)
})
.OrderBy(i => i.Size)
.ThenBy(i => i.Name)
.ThenByDescending(i => i.DateModified)
.ThenByDescending(i => i.DateCreated);
// max total size 20M
const int MaxSize = 20971520;
var sizeCounter = 0L;
foreach (var fontFile in fontFiles)
{
sizeCounter += fontFile.Size;
if (sizeCounter >= MaxSize)
{
_logger.LogWarning("Some fonts will not be sent due to size limitations");
yield break;
}
yield return fontFile;
}
}
else
{
_logger.LogWarning("The path of fallback font folder has not been set");
encodingOptions.EnableFallbackFont = false;
}
}
/// <summary>
/// Gets a fallback font file.
/// </summary>
/// <param name="name">The name of the fallback font file to get.</param>
/// <response code="200">Fallback font file retrieved.</response>
/// <returns>The fallback font file.</returns>
[HttpGet("FallbackFont/Fonts/{name}")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult GetFallbackFont([FromRoute, Required] string name)
{
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
var fallbackFontPath = encodingOptions.FallbackFontPath;
if (!string.IsNullOrEmpty(fallbackFontPath))
{
var fontFile = _fileSystem.GetFiles(fallbackFontPath)
.First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
var fileSize = fontFile?.Length;
if (fontFile != null && fileSize != null && fileSize > 0)
{
_logger.LogDebug("Fallback font size is {fileSize} Bytes", fileSize);
return PhysicalFile(fontFile.FullName, MimeTypes.GetMimeType(fontFile.FullName));
}
else
{
_logger.LogWarning("The selected font is null or empty");
}
}
else
{
_logger.LogWarning("The path of fallback font folder has not been set");
encodingOptions.EnableFallbackFont = false;
}
// returning HTTP 204 will break the SubtitlesOctopus
return Ok();
}
}
}

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

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

@ -5,6 +5,7 @@ using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dto;
@ -13,6 +14,7 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
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="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="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="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>
@ -72,17 +74,16 @@ namespace Jellyfin.Api.Controllers
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? seriesId,
[FromQuery] string? parentId,
[FromQuery] bool? enableImges,
[FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true)
{
var options = new DtoOptions()
.AddItemFields(fields!)
var options = new DtoOptions { Fields = fields }
.AddClientFields(Request)
.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="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="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="enableImges">Optional. Include image information in output.</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] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? parentId,
[FromQuery] bool? enableImges,
[FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData)
{
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 options = new DtoOptions()
.AddItemFields(fields!)
var options = new DtoOptions { Fields = fields }
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!);
@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers
public ActionResult<QueryResult<BaseItemDto>> GetEpisodes(
[FromRoute, Required] string seriesId,
[FromQuery] Guid? userId,
[FromQuery] string? fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] int? season,
[FromQuery] string? seasonId,
[FromQuery] bool? isMissing,
@ -206,7 +206,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] string? sortBy)
{
@ -216,8 +216,7 @@ namespace Jellyfin.Api.Controllers
List<BaseItem> episodes;
var dtoOptions = new DtoOptions()
.AddItemFields(fields!)
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
@ -319,13 +318,13 @@ namespace Jellyfin.Api.Controllers
public ActionResult<QueryResult<BaseItemDto>> GetSeasons(
[FromRoute, Required] string seriesId,
[FromQuery] Guid? userId,
[FromQuery] string? fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? isSpecialSeason,
[FromQuery] bool? isMissing,
[FromQuery] string? adjacentTo,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData)
{
var user = userId.HasValue && !userId.Equals(Guid.Empty)
@ -344,8 +343,7 @@ namespace Jellyfin.Api.Controllers
AdjacentTo = adjacentTo
});
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);

@ -76,6 +76,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxAudioChannels">Optional. The maximum number of audio channels.</param>
/// <param name="transcodingAudioChannels">Optional. The number of how many audio channels to transcode to.</param>
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
/// <param name="transcodingContainer">Optional. The container to transcode to.</param>
/// <param name="transcodingProtocol">Optional. The transcoding protocol.</param>
@ -88,23 +89,22 @@ namespace Jellyfin.Api.Controllers
/// <response code="302">Redirected to remote audio stream.</response>
/// <returns>A <see cref="Task"/> containing the audio file.</returns>
[HttpGet("Audio/{itemId}/universal")]
[HttpGet("Audio/{itemId}/universal.{container}", Name = "GetUniversalAudioStream_2")]
[HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")]
[HttpHead("Audio/{itemId}/universal.{container}", Name = "HeadUniversalAudioStream_2")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status302Found)]
[ProducesAudioFile]
public async Task<ActionResult> GetUniversalAudioStream(
[FromRoute, Required] Guid itemId,
[FromRoute] string? container,
[FromQuery] string? container,
[FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId,
[FromQuery] Guid? userId,
[FromQuery] string? audioCodec,
[FromQuery] int? maxAudioChannels,
[FromQuery] int? transcodingAudioChannels,
[FromQuery] long? maxStreamingBitrate,
[FromQuery] int? maxStreamingBitrate,
[FromQuery] int? audioBitRate,
[FromQuery] long? startTimeTicks,
[FromQuery] string? transcodingContainer,
[FromQuery] string? transcodingProtocol,
@ -212,7 +212,7 @@ namespace Jellyfin.Api.Controllers
AudioSampleRate = maxAudioSampleRate,
MaxAudioChannels = maxAudioChannels,
MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
StartTimeTicks = startTimeTicks,
SubtitleMethod = SubtitleDeliveryMethod.Hls,
RequireAvc = true,
@ -244,7 +244,7 @@ namespace Jellyfin.Api.Controllers
BreakOnNonKeyFrames = breakOnNonKeyFrames,
AudioSampleRate = maxAudioSampleRate,
MaxAudioChannels = maxAudioChannels,
AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
AudioBitRate = isStatic ? (int?)null : (audioBitRate ?? maxStreamingBitrate),
MaxAudioBitDepth = maxAudioBitDepth,
AudioChannels = maxAudioChannels,
CopyTimestamps = true,
@ -270,20 +270,24 @@ namespace Jellyfin.Api.Controllers
{
var deviceProfile = new DeviceProfile();
var directPlayProfiles = new List<DirectPlayProfile>();
var containers = RequestHelpers.Split(container, ',', true);
foreach (var cont in containers)
int len = containers.Length;
var directPlayProfiles = new DirectPlayProfile[len];
for (int i = 0; i < len; i++)
{
var parts = RequestHelpers.Split(cont, ',', true);
var parts = RequestHelpers.Split(containers[i], '|', true);
var audioCodecs = parts.Length == 1 ? null : string.Join(",", parts.Skip(1).ToArray());
var audioCodecs = parts.Length == 1 ? null : string.Join(',', parts.Skip(1));
directPlayProfiles.Add(new DirectPlayProfile { Type = DlnaProfileType.Audio, Container = parts[0], AudioCodec = audioCodecs });
directPlayProfiles[i] = new DirectPlayProfile
{
Type = DlnaProfileType.Audio,
Container = parts[0],
AudioCodec = audioCodecs
};
}
deviceProfile.DirectPlayProfiles = directPlayProfiles.ToArray();
deviceProfile.DirectPlayProfiles = directPlayProfiles;
deviceProfile.TranscodingProfiles = new[]
{

@ -381,17 +381,13 @@ namespace Jellyfin.Api.Controllers
var user = _userManager.GetUserById(userId);
if (string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal))
{
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
_userManager.UpdateConfiguration(user.Id, updateUser.Configuration);
}
else
if (!string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal))
{
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();
}
@ -409,7 +405,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult UpdateUserPolicy(
public async Task<ActionResult> UpdateUserPolicy(
[FromRoute, Required] Guid userId,
[FromBody] UserPolicy newPolicy)
{
@ -447,7 +443,7 @@ namespace Jellyfin.Api.Controllers
_sessionManager.RevokeUserTokens(user.Id, currentToken);
}
_userManager.UpdatePolicy(userId, newPolicy);
await _userManager.UpdatePolicyAsync(userId, newPolicy).ConfigureAwait(false);
return NoContent();
}
@ -464,7 +460,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult UpdateUserConfiguration(
public async Task<ActionResult> UpdateUserConfiguration(
[FromRoute, Required] Guid userId,
[FromBody] UserConfiguration userConfig)
{
@ -473,7 +469,7 @@ namespace Jellyfin.Api.Controllers
return Forbid("User configuration update not allowed");
}
_userManager.UpdateConfiguration(userId, userConfig);
await _userManager.UpdateConfigurationAsync(userId, userConfig).ConfigureAwait(false);
return NoContent();
}
@ -534,6 +530,33 @@ namespace Jellyfin.Api.Controllers
return result;
}
/// <summary>
/// Gets the user based on auth token.
/// </summary>
/// <response code="200">User returned.</response>
/// <response code="400">Token is not owned by a user.</response>
/// <returns>A <see cref="UserDto"/> for the authenticated user.</returns>
[HttpGet("Me")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult<UserDto> GetCurrentUser()
{
var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
if (userId == null)
{
return BadRequest();
}
var user = _userManager.GetUserById(userId.Value);
if (user == null)
{
return BadRequest();
}
return _userManager.GetUserDto(user);
}
private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
{
var users = _userManager.Users;

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@ -251,7 +252,7 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <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="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="isPlayed">Filter by items that are played, or not.</param>
/// <param name="enableImages">Optional. include image information in output.</param>
@ -267,12 +268,12 @@ namespace Jellyfin.Api.Controllers
public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia(
[FromRoute, Required] Guid userId,
[FromQuery] Guid? parentId,
[FromQuery] string? fields,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? includeItemTypes,
[FromQuery] bool? isPlayed,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] int limit = 20,
[FromQuery] bool groupItems = true)
@ -287,8 +288,7 @@ namespace Jellyfin.Api.Controllers
}
}
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);

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

Loading…
Cancel
Save