diff --git a/DvdLib/Ifo/Dvd.cs b/DvdLib/Ifo/Dvd.cs index 361319625..b4a11ed5d 100644 --- a/DvdLib/Ifo/Dvd.cs +++ b/DvdLib/Ifo/Dvd.cs @@ -31,7 +31,7 @@ namespace DvdLib.Ifo continue; } - var nums = ifo.Name.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries); + var nums = ifo.Name.Split('_', StringSplitOptions.RemoveEmptyEntries); if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber)) { ReadVTS(ifoNumber, ifo.FullName); diff --git a/Emby.Dlna/Didl/Filter.cs b/Emby.Dlna/Didl/Filter.cs index b58fdff2c..d703f043e 100644 --- a/Emby.Dlna/Didl/Filter.cs +++ b/Emby.Dlna/Didl/Filter.cs @@ -18,7 +18,7 @@ namespace Emby.Dlna.Didl { _all = string.Equals(filter, "*", StringComparison.OrdinalIgnoreCase); - _fields = (filter ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + _fields = (filter ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries); } public bool Contains(string field) diff --git a/Emby.Dlna/Eventing/DlnaEventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs index 770d56c30..b6e45c50e 100644 --- a/Emby.Dlna/Eventing/DlnaEventManager.cs +++ b/Emby.Dlna/Eventing/DlnaEventManager.cs @@ -83,7 +83,7 @@ namespace Emby.Dlna.Eventing if (!string.IsNullOrEmpty(header)) { // Starts with SECOND- - header = header.Split('-').Last(); + header = header.Split('-')[^1]; if (int.TryParse(header, NumberStyles.Integer, _usCulture, out var val)) { diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index 4ab0a2a3f..4f72c8ce1 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -133,6 +133,33 @@ namespace Emby.Server.Implementations.AppBase } } + /// + /// Manually pre-loads a factory so that it is available pre system initialisation. + /// + /// Class to register. + public virtual void RegisterConfiguration() + where T : IConfigurationFactory + { + IConfigurationFactory factory = Activator.CreateInstance(); + + if (_configurationFactories == null) + { + _configurationFactories = new[] { factory }; + } + else + { + var oldLen = _configurationFactories.Length; + var arr = new IConfigurationFactory[oldLen + 1]; + _configurationFactories.CopyTo(arr, 0); + arr[oldLen] = factory; + _configurationFactories = arr; + } + + _configurationStores = _configurationFactories + .SelectMany(i => i.GetConfigurations()) + .ToArray(); + } + /// /// Adds parts. /// diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 9d5b651d9..ad3c19618 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -126,7 +126,6 @@ namespace Emby.Server.Implementations private IMediaEncoder _mediaEncoder; private ISessionManager _sessionManager; private IHttpClientFactory _httpClientFactory; - private string[] _urlPrefixes; /// @@ -497,24 +496,11 @@ namespace Emby.Server.Implementations HttpsPort = ServerConfiguration.DefaultHttpsPort; } - if (Plugins != null) - { - var pluginBuilder = new StringBuilder(); - - foreach (var plugin in Plugins) - { - pluginBuilder.Append(plugin.Name) - .Append(' ') - .Append(plugin.Version) - .AppendLine(); - } - - Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString()); - } - DiscoverTypes(); RegisterServices(); + + RegisterPluginServices(); } /// @@ -779,10 +765,24 @@ namespace Emby.Server.Implementations ConfigurationManager.AddParts(GetExports()); _plugins = GetExports() - .Select(LoadPlugin) .Where(i => i != null) .ToArray(); + if (Plugins != null) + { + var pluginBuilder = new StringBuilder(); + + foreach (var plugin in Plugins) + { + pluginBuilder.Append(plugin.Name) + .Append(' ') + .Append(plugin.Version) + .AppendLine(); + } + + Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString()); + } + _urlPrefixes = GetUrlPrefixes().ToArray(); Resolve().AddParts( @@ -812,21 +812,6 @@ namespace Emby.Server.Implementations Resolve().AddParts(GetExports()); } - private IPlugin LoadPlugin(IPlugin plugin) - { - try - { - plugin.RegisterServices(ServiceCollection); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error loading plugin {PluginName}", plugin.GetType().FullName); - return null; - } - - return plugin; - } - /// /// Discovers the types. /// @@ -837,6 +822,22 @@ namespace Emby.Server.Implementations _allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray(); } + private void RegisterPluginServices() + { + foreach (var pluginServiceRegistrator in GetExportTypes()) + { + try + { + var instance = (IPluginServiceRegistrator)Activator.CreateInstance(pluginServiceRegistrator); + instance.RegisterServices(ServiceCollection); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error registering plugin services from {Assembly}.", pluginServiceRegistrator.Assembly); + } + } + } + private IEnumerable GetTypes(IEnumerable assemblies) { foreach (var ass in assemblies) @@ -996,6 +997,12 @@ namespace Emby.Server.Implementations { var minimumVersion = new Version(0, 0, 0, 1); var versions = new List(); + if (!Directory.Exists(path)) + { + // Plugin path doesn't exist, don't try to enumerate subfolders. + return Enumerable.Empty(); + } + var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly); foreach (var dir in directories) @@ -1026,7 +1033,7 @@ namespace Emby.Server.Implementations else { // No metafile, so lets see if the folder is versioned. - metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1]; + metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1]; int versionIndex = dir.LastIndexOf('_'); if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version parsedVersion)) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index acb75e9b8..638c7a9b4 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -1007,7 +1007,7 @@ namespace Emby.Server.Implementations.Data return; } - var parts = value.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + var parts = value.Split('|', StringSplitOptions.RemoveEmptyEntries); foreach (var part in parts) { @@ -1057,7 +1057,7 @@ namespace Emby.Server.Implementations.Data return; } - var parts = value.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + var parts = value.Split('|' , StringSplitOptions.RemoveEmptyEntries); var list = new List(); foreach (var part in parts) { @@ -1096,7 +1096,7 @@ namespace Emby.Server.Implementations.Data public ItemImageInfo ItemImageInfoFromValueString(string value) { - var parts = value.Split(new[] { '*' }, StringSplitOptions.None); + var parts = value.Split('*', StringSplitOptions.None); if (parts.Length < 3) { @@ -1532,7 +1532,7 @@ namespace Emby.Server.Implementations.Data { if (!reader.IsDBNull(index)) { - item.Genres = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + item.Genres = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries); } index++; @@ -1593,7 +1593,7 @@ namespace Emby.Server.Implementations.Data { IEnumerable GetLockedFields(string s) { - foreach (var i in s.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)) + foreach (var i in s.Split('|', StringSplitOptions.RemoveEmptyEntries)) { if (Enum.TryParse(i, true, out MetadataField parsedValue)) { @@ -1612,7 +1612,7 @@ namespace Emby.Server.Implementations.Data { if (!reader.IsDBNull(index)) { - item.Studios = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + item.Studios = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries); } index++; @@ -1622,7 +1622,7 @@ namespace Emby.Server.Implementations.Data { if (!reader.IsDBNull(index)) { - item.Tags = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + item.Tags = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries); } index++; @@ -1636,7 +1636,7 @@ namespace Emby.Server.Implementations.Data { IEnumerable GetTrailerTypes(string s) { - foreach (var i in s.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)) + foreach (var i in s.Split('|', StringSplitOptions.RemoveEmptyEntries)) { if (Enum.TryParse(i, true, out TrailerType parsedValue)) { @@ -1811,7 +1811,7 @@ namespace Emby.Server.Implementations.Data { if (!reader.IsDBNull(index)) { - item.ProductionLocations = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).ToArray(); + item.ProductionLocations = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries).ToArray(); } index++; @@ -1848,14 +1848,14 @@ namespace Emby.Server.Implementations.Data { if (item is IHasArtist hasArtists && !reader.IsDBNull(index)) { - hasArtists.Artists = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + hasArtists.Artists = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries); } index++; if (item is IHasAlbumArtist hasAlbumArtists && !reader.IsDBNull(index)) { - hasAlbumArtists.AlbumArtists = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + hasAlbumArtists.AlbumArtists = reader.GetString(index).Split('|', StringSplitOptions.RemoveEmptyEntries); } index++; @@ -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"); @@ -5052,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); @@ -5593,7 +5611,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type return counts; } - var allTypes = typeString.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) + var allTypes = typeString.Split('|', StringSplitOptions.RemoveEmptyEntries) .ToLookup(x => x); foreach (var type in allTypes) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 73502c2c9..f3e3a6397 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -275,7 +275,7 @@ namespace Emby.Server.Implementations.Dto continue; } - var containers = container.Split(new[] { ',' }); + var containers = container.Split(','); if (containers.Length < 2) { continue; diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs index e733c9092..fdf2e3908 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -245,7 +245,7 @@ namespace Emby.Server.Implementations.HttpServer.Security return null; } - var parts = authorizationHeader.Split(new[] { ' ' }, 2); + var parts = authorizationHeader.Split(' ', 2); // There should be at least to parts if (parts.Length != 2) @@ -269,11 +269,11 @@ namespace Emby.Server.Implementations.HttpServer.Security foreach (var item in parts) { - var param = item.Trim().Split(new[] { '=' }, 2); + var param = item.Trim().Split('=', 2); if (param.Length == 2) { - var value = NormalizeValue(param[1].Trim(new[] { '"' })); + var value = NormalizeValue(param[1].Trim('"')); result[param[0]] = value; } } diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index f16eda1ec..7074382b6 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2705,7 +2705,7 @@ namespace Emby.Server.Implementations.Library var videos = videoListResolver.Resolve(fileSystemChildren); - var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files.First().Path, StringComparison.OrdinalIgnoreCase)); + var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase)); if (currentVideo != null) { diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 376a15570..928f5f88e 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -849,7 +849,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentException("Key can't be empty.", nameof(key)); } - var keys = key.Split(new[] { LiveStreamIdDelimeter }, 2); + var keys = key.Split(LiveStreamIdDelimeter, 2); var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase)); diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index 70be52411..2c4497c69 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -201,7 +201,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio continue; } - var firstMedia = resolvedItem.Files.First(); + var firstMedia = resolvedItem.Files[0]; var libraryItem = new T { diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index 9c7d624ee..8c9bb6ba0 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -1429,7 +1429,7 @@ namespace Emby.Server.Implementations.LiveTv return result; } - public Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> tuples, ItemFields[] fields, User user = null) + public Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> tuples, IReadOnlyList fields, User user = null) { var programTuples = new List>(); var hasChannelImage = fields.Contains(ItemFields.ChannelImage); @@ -2208,7 +2208,7 @@ namespace Emby.Server.Implementations.LiveTv /// Task. public Task ResetTuner(string id, CancellationToken cancellationToken) { - var parts = id.Split(new[] { '_' }, 2); + var parts = id.Split('_', 2); var service = _services.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), parts[0], StringComparison.OrdinalIgnoreCase)); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs index 0333e723b..78e62ff0a 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs @@ -182,7 +182,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts if (string.IsNullOrEmpty(currentFile)) { - return (files.Last(), true); + return (files[^1], true); } var nextIndex = files.FindIndex(i => string.Equals(i, currentFile, StringComparison.OrdinalIgnoreCase)) + 1; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index c064e2fe6..7c13d45e9 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -163,7 +163,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private string GetChannelNumber(string extInf, Dictionary attributes, string mediaUrl) { - var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + var nameParts = extInf.Split(',', StringSplitOptions.RemoveEmptyEntries); var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].AsSpan().Trim() : ReadOnlySpan.Empty; string numberString = null; @@ -273,8 +273,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private static string GetChannelName(string extInf, Dictionary attributes) { - var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - var nameInExtInf = nameParts.Length > 1 ? nameParts.Last().Trim() : null; + var nameParts = extInf.Split(',', StringSplitOptions.RemoveEmptyEntries); + var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].Trim() : null; // Check for channel number with the format from SatIp // #EXTINF:0,84. VOX Schweiz diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json index 57ff13219..cd64cdde4 100644 --- a/Emby.Server.Implementations/Localization/Core/en-GB.json +++ b/Emby.Server.Implementations/Localization/Core/en-GB.json @@ -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" } diff --git a/Emby.Server.Implementations/Localization/Core/fil.json b/Emby.Server.Implementations/Localization/Core/fil.json index 1a3e18832..e5ca676a4 100644 --- a/Emby.Server.Implementations/Localization/Core/fil.json +++ b/Emby.Server.Implementations/Localization/Core/fil.json @@ -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." } diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index 343d213d4..804dabe57 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -113,5 +113,7 @@ "TaskDownloadMissingSubtitles": "Hiányzó feliratok letöltése", "TaskRefreshChannelsDescription": "Frissíti az internetes csatornák adatait.", "TaskRefreshChannels": "Csatornák frissítése", - "TaskCleanTranscodeDescription": "Törli az egy napnál régebbi átkódolási fájlokat." + "TaskCleanTranscodeDescription": "Törli az egy napnál régebbi átkódolási fájlokat.", + "TaskCleanActivityLogDescription": "A beállítottnál korábbi bejegyzések törlése a tevékenységnaplóból.", + "TaskCleanActivityLog": "Tevékenységnapló törlése" } diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index e102b92b9..e1e88cc9b 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -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" } diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json index bc008df3b..5e4022292 100644 --- a/Emby.Server.Implementations/Localization/Core/ro.json +++ b/Emby.Server.Implementations/Localization/Core/ro.json @@ -112,5 +112,7 @@ "TasksChannelsCategory": "Canale de pe Internet", "TasksApplicationCategory": "Aplicație", "TasksLibraryCategory": "Librărie", - "TasksMaintenanceCategory": "Mentenanță" + "TasksMaintenanceCategory": "Mentenanță", + "TaskCleanActivityLogDescription": "Șterge intrările din jurnalul de activitate mai vechi de data configurată.", + "TaskCleanActivityLog": "Curăță Jurnalul de Activitate" } diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json index 2b1eccfaf..8da92f309 100644 --- a/Emby.Server.Implementations/Localization/Core/sr.json +++ b/Emby.Server.Implementations/Localization/Core/sr.json @@ -112,5 +112,7 @@ "TasksChannelsCategory": "Интернет канали", "TasksApplicationCategory": "Апликација", "TasksLibraryCategory": "Библиотека", - "TasksMaintenanceCategory": "Одржавање" + "TasksMaintenanceCategory": "Одржавање", + "TaskCleanActivityLogDescription": "Брише историју активности старију од конфигурисаног броја година.", + "TaskCleanActivityLog": "Очисти историју активности" } diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json index 8089fc304..e8cd23d5d 100644 --- a/Emby.Server.Implementations/Localization/Core/ta.json +++ b/Emby.Server.Implementations/Localization/Core/ta.json @@ -112,5 +112,7 @@ "UserOnlineFromDevice": "{1} இருந்து {0} ஆன்லைன்", "HomeVideos": "முகப்பு வீடியோக்கள்", "UserStoppedPlayingItemWithValues": "{0} {2} இல் {1} முடித்துவிட்டது", - "UserStartedPlayingItemWithValues": "{0} {2}இல் {1} ஐ இயக்குகிறது" + "UserStartedPlayingItemWithValues": "{0} {2}இல் {1} ஐ இயக்குகிறது", + "TaskCleanActivityLogDescription": "உள்ளமைக்கப்பட்ட வயதை விட பழைய செயல்பாட்டு பதிவு உள்ளீடுகளை நீக்குகிறது.", + "TaskCleanActivityLog": "செயல்பாட்டு பதிவை அழி" } diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index 56f4133a0..3a9e28458 100644 --- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -653,7 +653,7 @@ namespace Emby.Server.Implementations.ScheduledTasks try { _logger.LogInformation(Name + ": Waiting on Task"); - var exited = Task.WaitAll(new[] { task }, 2000); + var exited = task.Wait(2000); if (exited) { diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index 8439f8a99..171e44258 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -106,7 +106,7 @@ namespace Emby.Server.Implementations.ScheduledTasks try { previouslyFailedImages = File.ReadAllText(failHistoryPath) - .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) + .Split('|', StringSplitOptions.RemoveEmptyEntries) .ToList(); } catch (IOException) diff --git a/Jellyfin.Api/Controllers/AlbumsController.cs b/Jellyfin.Api/Controllers/AlbumsController.cs deleted file mode 100644 index 357f646a2..000000000 --- a/Jellyfin.Api/Controllers/AlbumsController.cs +++ /dev/null @@ -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 -{ - /// - /// The albums controller. - /// - [Route("")] - public class AlbumsController : BaseJellyfinApiController - { - private readonly IUserManager _userManager; - private readonly ILibraryManager _libraryManager; - private readonly IDtoService _dtoService; - - /// - /// Initializes a new instance of the class. - /// - /// Instance of the interface. - /// Instance of the interface. - /// Instance of the interface. - public AlbumsController( - IUserManager userManager, - ILibraryManager libraryManager, - IDtoService dtoService) - { - _userManager = userManager; - _libraryManager = libraryManager; - _dtoService = dtoService; - } - - /// - /// Finds albums similar to a given album. - /// - /// The album id. - /// Optional. Filter by user id, and attach user data. - /// Optional. Ids of artists to exclude. - /// Optional. The maximum number of records to return. - /// Similar albums returned. - /// A with similar albums. - [HttpGet("Albums/{albumId}/Similar")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> 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); - } - - /// - /// Finds artists similar to a given artist. - /// - /// The artist id. - /// Optional. Filter by user id, and attach user data. - /// Optional. Ids of artists to exclude. - /// Optional. The maximum number of records to return. - /// Similar artists returned. - /// A with similar artists. - [HttpGet("Artists/{artistId}/Similar")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> 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); - } - - /// - /// Gets a similairty score of two albums. - /// - /// The first item. - /// The item1 people. - /// All people. - /// The second item. - /// System.Int32. - private int GetAlbumSimilarityScore(BaseItem item1, List item1People, List 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( - album2.GetAllArtists().DistinctNames(), - StringComparer.OrdinalIgnoreCase); - - return points + artists1.Where(artists2.Contains).Sum(i => 5); - } - } -} diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index a35d7a556..9bad206e0 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -53,7 +53,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The maximum number of records to return. /// Optional. Search term. /// Specify this to localize the search to a specific item or folder. Omit to use the root. - /// 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. + /// Optional. Specify additional fields of information to return in the output. /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited. /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. /// Optional. Specify additional filters to apply. @@ -88,7 +88,7 @@ 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, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, @@ -114,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); @@ -262,7 +261,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The maximum number of records to return. /// Optional. Search term. /// Specify this to localize the search to a specific item or folder. Omit to use the root. - /// 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. + /// Optional. Specify additional fields of information to return in the output. /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited. /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. /// Optional. Specify additional filters to apply. @@ -297,7 +296,7 @@ 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, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, @@ -323,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); diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index 20fc96ba8..ec9d7cdce 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -108,7 +108,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Sort Order - Ascending,Descending. /// Optional. Specify additional filters to apply. /// 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. - /// 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. + /// Optional. Specify additional fields of information to return in the output. /// Channel items returned. /// /// A representing the request to get the channel items. @@ -124,7 +124,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? sortOrder, [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) @@ -137,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) @@ -185,7 +184,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The record index to start at. All items with a lower index will be dropped from the results. /// Optional. The maximum number of records to return. /// Optional. Specify additional filters to apply. - /// 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. + /// Optional. Specify additional fields of information to return in the output. /// Optional. Specify one or more channel id's, comma delimited. /// Latest channel items returned. /// @@ -198,7 +197,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, - [FromQuery] string? fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] string? channelIds) { var user = userId.HasValue && !userId.Equals(Guid.Empty) @@ -214,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) diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index 2a567c846..008bb58d1 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -78,8 +78,8 @@ namespace Jellyfin.Api.Controllers var query = new InternalItemsQuery { User = user, - MediaTypes = (mediaTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries), - IncludeItemTypes = (includeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries), + MediaTypes = (mediaTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries), + IncludeItemTypes = (includeItemTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries), Recursive = true, EnableTotalRecordCount = false, DtoOptions = new DtoOptions @@ -168,7 +168,7 @@ namespace Jellyfin.Api.Controllers var genreQuery = new InternalItemsQuery(user) { IncludeItemTypes = - (includeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries), + (includeItemTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries), DtoOptions = new DtoOptions { Fields = Array.Empty(), diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index 9c009d784..9c222135d 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -52,7 +52,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The maximum number of records to return. /// The search term. /// Specify this to localize the search to a specific item or folder. Omit to use the root. - /// 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. + /// Optional. Specify additional fields of information to return in the output. /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited. /// Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited. /// Optional filter by items that are marked as favorite, or not. @@ -73,7 +73,7 @@ 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] bool? isFavorite, @@ -86,8 +86,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, false, imageTypeLimit, enableImageTypes); diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index 2f342d0a8..d17a26db4 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -56,7 +56,7 @@ namespace Jellyfin.Api.Controllers /// The item id. /// Optional. Filter by user id, and attach user data. /// Optional. The maximum number of records to return. - /// 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. + /// Optional. Specify additional fields of information to return in the output. /// Optional. Include image information in output. /// Optional. Include user data. /// Optional. The max number of images to return, per image type. @@ -69,7 +69,7 @@ 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, @@ -79,8 +79,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!); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); @@ -93,7 +92,7 @@ namespace Jellyfin.Api.Controllers /// The item id. /// Optional. Filter by user id, and attach user data. /// Optional. The maximum number of records to return. - /// 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. + /// Optional. Specify additional fields of information to return in the output. /// Optional. Include image information in output. /// Optional. Include user data. /// Optional. The max number of images to return, per image type. @@ -106,7 +105,7 @@ 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, @@ -116,8 +115,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!); var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions); @@ -130,7 +128,7 @@ namespace Jellyfin.Api.Controllers /// The item id. /// Optional. Filter by user id, and attach user data. /// Optional. The maximum number of records to return. - /// 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. + /// Optional. Specify additional fields of information to return in the output. /// Optional. Include image information in output. /// Optional. Include user data. /// Optional. The max number of images to return, per image type. @@ -143,7 +141,7 @@ 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, @@ -153,8 +151,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!); var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions); @@ -167,7 +164,7 @@ namespace Jellyfin.Api.Controllers /// The genre name. /// Optional. Filter by user id, and attach user data. /// Optional. The maximum number of records to return. - /// 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. + /// Optional. Specify additional fields of information to return in the output. /// Optional. Include image information in output. /// Optional. Include user data. /// Optional. The max number of images to return, per image type. @@ -180,7 +177,7 @@ 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, @@ -189,8 +186,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!); var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions); @@ -203,7 +199,7 @@ namespace Jellyfin.Api.Controllers /// The item id. /// Optional. Filter by user id, and attach user data. /// Optional. The maximum number of records to return. - /// 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. + /// Optional. Specify additional fields of information to return in the output. /// Optional. Include image information in output. /// Optional. Include user data. /// Optional. The max number of images to return, per image type. @@ -216,7 +212,7 @@ 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, @@ -226,8 +222,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!); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); @@ -240,7 +235,7 @@ namespace Jellyfin.Api.Controllers /// The item id. /// Optional. Filter by user id, and attach user data. /// Optional. The maximum number of records to return. - /// 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. + /// Optional. Specify additional fields of information to return in the output. /// Optional. Include image information in output. /// Optional. Include user data. /// Optional. The max number of images to return, per image type. @@ -253,7 +248,7 @@ 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, @@ -263,8 +258,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!); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); @@ -277,7 +271,7 @@ namespace Jellyfin.Api.Controllers /// The item id. /// Optional. Filter by user id, and attach user data. /// Optional. The maximum number of records to return. - /// 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. + /// Optional. Specify additional fields of information to return in the output. /// Optional. Include image information in output. /// Optional. Include user data. /// Optional. The max number of images to return, per image type. @@ -290,7 +284,7 @@ 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, @@ -300,8 +294,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!); var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions); diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index adc95f757..d8d371ebc 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -180,7 +180,7 @@ 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, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, @@ -234,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); @@ -533,7 +532,7 @@ 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, @@ -545,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); diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 60bbc5022..1b115d800 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -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 /// 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. /// Similar items returned. /// A containing the similar items. - [HttpGet("Artists/{itemId}/Similar", Name = "GetSimilarArtists2")] + [HttpGet("Artists/{itemId}/Similar", Name = "GetSimilarArtists")] [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", Name = "GetSimilarAlbums")] + [HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows")] + [HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies")] + [HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> 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); - 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) - { - return GetSimilarItemsResult( - item, - excludeArtistIds, - userId, - limit, - fields, - new[] { nameof(Series) }, - false); - } - - if (item is MediaBrowser.Controller.Entities.TV.Episode || (item is IItemByName && !(item is MusicArtist))) + if (item is Episode || (item is IItemByName && !(item is MusicArtist))) { return new QueryResult(); } - return GetSimilarItemsResult( - item, - excludeArtistIds, - userId, - limit, - fields, - new[] { item.GetType().Name }, - isMovie); + 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; + 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(); + if (isMovie.Value) + { + includeItemTypes.Add(nameof(Movie)); + if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) + { + includeItemTypes.Add(nameof(Trailer)); + includeItemTypes.Add(nameof(LiveTvProgram)); + } + } + else if (isSeries.Value) + { + includeItemTypes.Add(nameof(Series)); + } + else + { + // For non series and movie types these columns are typically null + isSeries = null; + isMovie = null; + includeItemTypes.Add(item.GetType().Name); + } + + var query = new InternalItemsQuery(user) + { + Limit = limit, + IncludeItemTypes = includeItemTypes.ToArray(), + IsMovie = isMovie, + IsSeries = isSeries, + SimilarTo = item, + DtoOptions = dtoOptions, + EnableTotalRecordCount = !isMovie ?? true, + EnableGroupByMetadataKey = isMovie ?? false, + MinSimilarityScore = 2 // A remnant from album/artist scoring + }; + + // ExcludeArtistIds + if (!string.IsNullOrEmpty(excludeArtistIds)) + { + query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds); + } + + List itemsResult = _libraryManager.GetItemList(query); + + var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user); + + return new QueryResult + { + Items = returnList, + TotalRecordCount = itemsResult.Count + }; } /// @@ -880,75 +919,6 @@ namespace Jellyfin.Api.Controllers } } - private QueryResult 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 itemsResult; - - if (isMovie) - { - var itemTypes = new List { 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(); - - 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 - { - Items = returnList, - TotalRecordCount = itemsResult.Count - }; - - return result; - } - private static string[] GetRepresentativeItemTypes(string? contentType) { return contentType switch diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 384a48705..29c0f1df4 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -119,7 +119,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Include image information in output. /// Optional. The max number of images to return, per image type. /// "Optional. The image types to include in the output. - /// 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. + /// Optional. Specify additional fields of information to return in the output. /// Optional. Include user data. /// Optional. Key to sort by. /// Optional. Sort order. @@ -148,15 +148,14 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery] string? fields, + [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); @@ -240,7 +239,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Include image information in output. /// Optional. The max number of images to return, per image type. /// Optional. The image types to include in the output. - /// 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. + /// Optional. Specify additional fields of information to return in the output. /// Optional. Include user data. /// Optional. Filter for movies. /// Optional. Filter for series. @@ -265,7 +264,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery] string? fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] bool? enableUserData, [FromQuery] bool? isMovie, [FromQuery] bool? isSeries, @@ -275,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); @@ -297,7 +295,7 @@ namespace Jellyfin.Api.Controllers IsKids = isKids, IsSports = isSports, IsLibraryItem = isLibraryItem, - Fields = RequestHelpers.GetItemFields(fields), + Fields = fields, ImageTypeLimit = imageTypeLimit, EnableImages = enableImages }, dtoOptions); @@ -317,7 +315,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Include image information in output. /// Optional. The max number of images to return, per image type. /// Optional. The image types to include in the output. - /// 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. + /// Optional. Specify additional fields of information to return in the output. /// Optional. Include user data. /// Optional. Return total record count. /// Live tv recordings returned. @@ -352,7 +350,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool? enableImages, [FromQuery] int? imageTypeLimit, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, - [FromQuery] string? fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] bool? enableUserData, [FromQuery] bool enableTotalRecordCount = true) { @@ -531,7 +529,7 @@ namespace Jellyfin.Api.Controllers /// Optional. Include user data. /// Optional. Filter by series timer id. /// Optional. Filter by library series id. - /// 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. + /// Optional. Specify additional fields of information to return in the output. /// Retrieve total record count. /// Live tv epgs returned. /// @@ -566,7 +564,7 @@ namespace Jellyfin.Api.Controllers [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) @@ -607,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); @@ -663,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); @@ -686,7 +682,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The max number of images to return, per image type. /// Optional. The image types to include in the output. /// The genres to return guide information for. - /// 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. + /// Optional. Specify additional fields of information to return in the output. /// Optional. include user data. /// Retrieve total record count. /// Recommended epgs returned. @@ -708,7 +704,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? imageTypeLimit, [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) { @@ -730,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); diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index afb536e74..ebc148fe5 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -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> 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(); diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 989c383fd..229d9ff02 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -52,7 +52,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The maximum number of records to return. /// The search term. /// Specify this to localize the search to a specific item or folder. Omit to use the root. - /// 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. + /// Optional. Specify additional fields of information to return in the output. /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited. /// Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited. /// Optional filter by items that are marked as favorite, or not. @@ -73,7 +73,7 @@ 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] bool? isFavorite, @@ -86,8 +86,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, false, imageTypeLimit, enableImageTypes); diff --git a/Jellyfin.Api/Controllers/PackageController.cs b/Jellyfin.Api/Controllers/PackageController.cs index a104af4e6..1f797d6bc 100644 --- a/Jellyfin.Api/Controllers/PackageController.cs +++ b/Jellyfin.Api/Controllers/PackageController.cs @@ -154,12 +154,13 @@ namespace Jellyfin.Api.Controllers /// The list of package repositories. /// Package repositories saved. /// A . - [HttpOptions("Repositories")] + [HttpPost("Repositories")] [Authorize(Policy = Policies.DefaultAuthorization)] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult SetRepositories([FromBody] List repositoryInfos) { _serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos; + _serverConfigurationManager.SaveConfiguration(); return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index 80395950c..6ac3e6417 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -53,8 +53,8 @@ namespace Jellyfin.Api.Controllers /// /// Optional. The maximum number of records to return. /// The search term. - /// 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. - /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes. + /// Optional. Specify additional fields of information to return in the output. + /// Optional. Specify additional filters to apply. /// Optional filter by items that are marked as favorite, or not. userId is required. /// Optional, include user data. /// Optional, the max number of images to return, per image type. @@ -71,7 +71,7 @@ namespace Jellyfin.Api.Controllers public ActionResult> GetPersons( [FromQuery] int? limit, [FromQuery] string? searchTerm, - [FromQuery] string? fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, [FromQuery] bool? enableUserData, @@ -83,8 +83,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] Guid? userId, [FromQuery] bool? enableImages = true) { - var dtoOptions = new DtoOptions() - .AddItemFields(fields) + var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index f2213f20d..4b3d8d3d3 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -135,7 +135,7 @@ namespace Jellyfin.Api.Controllers /// User id. /// Optional. The record index to start at. All items with a lower index will be dropped from the results. /// Optional. The maximum number of records to return. - /// 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. + /// Optional. Specify additional fields of information to return in the output. /// Optional. Include image information in output. /// Optional. Include user data. /// Optional. The max number of images to return, per image type. @@ -149,7 +149,7 @@ 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, @@ -177,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); diff --git a/Jellyfin.Api/Controllers/RemoteImageController.cs b/Jellyfin.Api/Controllers/RemoteImageController.cs index c2bfab175..5284888d8 100644 --- a/Jellyfin.Api/Controllers/RemoteImageController.cs +++ b/Jellyfin.Api/Controllers/RemoteImageController.cs @@ -254,7 +254,7 @@ namespace Jellyfin.Api.Controllers throw new ResourceNotFoundException(nameof(response.Content.Headers.ContentType)); } - var ext = response.Content.Headers.ContentType.MediaType.Split('/').Last(); + var ext = response.Content.Headers.ContentType.MediaType.Split('/')[^1]; var fullCachePath = GetFullCachePath(urlHash + "." + ext); var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid."); diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs index c60d14479..27dcd51bc 100644 --- a/Jellyfin.Api/Controllers/StudiosController.cs +++ b/Jellyfin.Api/Controllers/StudiosController.cs @@ -50,7 +50,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The maximum number of records to return. /// Optional. Search term. /// Specify this to localize the search to a specific item or folder. Omit to use the root. - /// 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. + /// Optional. Specify additional fields of information to return in the output. /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited. /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited. /// Optional filter by items that are marked as favorite, or not. @@ -72,7 +72,7 @@ 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] bool? isFavorite, @@ -86,8 +86,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); diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs index 6b4eee718..d78adcbcd 100644 --- a/Jellyfin.Api/Controllers/TrailersController.cs +++ b/Jellyfin.Api/Controllers/TrailersController.cs @@ -146,7 +146,7 @@ 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, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery] bool? isFavorite, diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index 55cdae39d..6fd154836 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -59,7 +59,7 @@ namespace Jellyfin.Api.Controllers /// The user id of the user to get the next up episodes for. /// Optional. The record index to start at. All items with a lower index will be dropped from the results. /// Optional. The maximum number of records to return. - /// 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. + /// Optional. Specify additional fields of information to return in the output. /// Optional. Filter by series id. /// Optional. Specify this to localize the search to a specific item or folder. Omit to use the root. /// Optional. Include image information in output. @@ -74,7 +74,7 @@ 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, @@ -83,8 +83,7 @@ namespace Jellyfin.Api.Controllers [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!); @@ -119,7 +118,7 @@ namespace Jellyfin.Api.Controllers /// The user id of the user to get the upcoming episodes for. /// Optional. The record index to start at. All items with a lower index will be dropped from the results. /// Optional. The maximum number of records to return. - /// 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. + /// Optional. Specify additional fields of information to return in the output. /// Optional. Specify this to localize the search to a specific item or folder. Omit to use the root. /// Optional. Include image information in output. /// Optional. The max number of images to return, per image type. @@ -132,7 +131,7 @@ 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, @@ -147,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!); @@ -198,7 +196,7 @@ namespace Jellyfin.Api.Controllers public ActionResult> 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, @@ -218,8 +216,7 @@ namespace Jellyfin.Api.Controllers List episodes; - var dtoOptions = new DtoOptions() - .AddItemFields(fields!) + var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!); @@ -321,7 +318,7 @@ namespace Jellyfin.Api.Controllers public ActionResult> 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, @@ -346,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!); diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index faa8a0634..cfd851129 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -252,7 +252,7 @@ namespace Jellyfin.Api.Controllers /// /// User id. /// Specify this to localize the search to a specific item or folder. Omit to use the root. - /// 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. + /// Optional. Specify additional fields of information to return in the output. /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted. /// Filter by items that are played, or not. /// Optional. include image information in output. @@ -268,7 +268,7 @@ namespace Jellyfin.Api.Controllers public ActionResult> 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, @@ -288,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); diff --git a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs index 09a1c93e6..418c0c123 100644 --- a/Jellyfin.Api/Controllers/VideoAttachmentsController.cs +++ b/Jellyfin.Api/Controllers/VideoAttachmentsController.cs @@ -46,7 +46,7 @@ namespace Jellyfin.Api.Controllers [Produces(MediaTypeNames.Application.Octet)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> GetAttachment( + public async Task GetAttachment( [FromRoute, Required] Guid videoId, [FromRoute, Required] string mediaSourceId, [FromRoute, Required] int index) diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index 1712a6a54..1b38e399d 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -52,7 +52,7 @@ namespace Jellyfin.Api.Controllers /// Optional. The maximum number of records to return. /// Sort Order - Ascending,Descending. /// Specify this to localize the search to a specific item or folder. Omit to use the root. - /// 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. + /// Optional. Specify additional fields of information to return in the output. /// Optional. If specified, results will be excluded based on item type. This allows multiple, comma delimited. /// Optional. If specified, results will be included based on item type. This allows multiple, comma delimited. /// Optional. Filter by MediaType. Allows multiple, comma delimited. @@ -72,7 +72,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] int? limit, [FromQuery] string? sortOrder, [FromQuery] string? parentId, - [FromQuery] string? fields, + [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery] string? excludeItemTypes, [FromQuery] string? includeItemTypes, [FromQuery] string? mediaTypes, @@ -84,8 +84,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] bool recursive = true, [FromQuery] bool? enableImages = true) { - var dtoOptions = new DtoOptions() - .AddItemFields(fields) + var dtoOptions = new DtoOptions { Fields = fields } .AddClientFields(Request) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs index 85f8d789e..f2abd515d 100644 --- a/Jellyfin.Api/Extensions/DtoExtensions.cs +++ b/Jellyfin.Api/Extensions/DtoExtensions.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Linq; using Jellyfin.Api.Helpers; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; @@ -13,42 +15,6 @@ namespace Jellyfin.Api.Extensions /// public static class DtoExtensions { - /// - /// Add Dto Item fields. - /// - /// - /// Converted from IHasItemFields. - /// Legacy order: 1. - /// - /// DtoOptions object. - /// Comma delimited string of fields. - /// Modified DtoOptions object. - internal static DtoOptions AddItemFields(this DtoOptions dtoOptions, string? fields) - { - if (string.IsNullOrEmpty(fields)) - { - dtoOptions.Fields = Array.Empty(); - } - else - { - dtoOptions.Fields = fields.Split(',') - .Select(v => - { - if (Enum.TryParse(v, true, out ItemFields value)) - { - return (ItemFields?)value; - } - - return null; - }) - .Where(i => i.HasValue) - .Select(i => i!.Value) - .ToArray(); - } - - return dtoOptions; - } - /// /// Add additional fields depending on client. /// @@ -79,7 +45,7 @@ namespace Jellyfin.Api.Extensions client.IndexOf("media center", StringComparison.OrdinalIgnoreCase) != -1 || client.IndexOf("classic", StringComparison.OrdinalIgnoreCase) != -1) { - int oldLen = dtoOptions.Fields.Length; + int oldLen = dtoOptions.Fields.Count; var arr = new ItemFields[oldLen + 1]; dtoOptions.Fields.CopyTo(arr, 0); arr[oldLen] = ItemFields.RecursiveItemCount; @@ -97,7 +63,7 @@ namespace Jellyfin.Api.Extensions client.IndexOf("samsung", StringComparison.OrdinalIgnoreCase) != -1 || client.IndexOf("androidtv", StringComparison.OrdinalIgnoreCase) != -1) { - int oldLen = dtoOptions.Fields.Length; + int oldLen = dtoOptions.Fields.Count; var arr = new ItemFields[oldLen + 1]; dtoOptions.Fields.CopyTo(arr, 0); arr[oldLen] = ItemFields.ChildCount; @@ -126,7 +92,7 @@ namespace Jellyfin.Api.Extensions bool? enableImages, bool? enableUserData, int? imageTypeLimit, - ImageType[] enableImageTypes) + IReadOnlyList enableImageTypes) { dtoOptions.EnableImages = enableImages ?? true; @@ -140,7 +106,7 @@ namespace Jellyfin.Api.Extensions dtoOptions.EnableUserData = enableUserData.Value; } - if (enableImageTypes.Length != 0) + if (enableImageTypes.Count != 0) { dtoOptions.ImageTypes = enableImageTypes; } diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index 13d6da317..f06f038ab 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -74,7 +74,7 @@ namespace Jellyfin.Api.Helpers } return removeEmpty - ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries) + ? value.Split(separator, StringSplitOptions.RemoveEmptyEntries) : value.Split(separator); } diff --git a/Jellyfin.Api/Helpers/SimilarItemsHelper.cs b/Jellyfin.Api/Helpers/SimilarItemsHelper.cs deleted file mode 100644 index 6b06f87cd..000000000 --- a/Jellyfin.Api/Helpers/SimilarItemsHelper.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using MediaBrowser.Controller.Dto; -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Querying; - -namespace Jellyfin.Api.Helpers -{ - /// - /// The similar items helper class. - /// - public static class SimilarItemsHelper - { - internal static QueryResult GetSimilarItemsResult( - DtoOptions dtoOptions, - IUserManager userManager, - ILibraryManager libraryManager, - IDtoService dtoService, - Guid? userId, - string id, - string? excludeArtistIds, - int? limit, - Type[] includeTypes, - Func, List, BaseItem, int> getSimilarityScore) - { - var user = userId.HasValue && !userId.Equals(Guid.Empty) - ? userManager.GetUserById(userId.Value) - : null; - - var item = string.IsNullOrEmpty(id) ? - (!userId.Equals(Guid.Empty) ? libraryManager.GetUserRootFolder() : - libraryManager.RootFolder) : libraryManager.GetItemById(id); - - var query = new InternalItemsQuery(user) - { - IncludeItemTypes = includeTypes.Select(i => i.Name).ToArray(), - Recursive = true, - DtoOptions = dtoOptions, - ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds) - }; - - var inputItems = libraryManager.GetItemList(query); - - var items = GetSimilaritems(item, libraryManager, inputItems, getSimilarityScore) - .ToList(); - - var returnItems = items; - - if (limit.HasValue && limit < returnItems.Count) - { - returnItems = returnItems.GetRange(0, limit.Value); - } - - var dtos = dtoService.GetBaseItemDtos(returnItems, dtoOptions, user); - - return new QueryResult - { - Items = dtos, - TotalRecordCount = items.Count - }; - } - - /// - /// Gets the similaritems. - /// - /// The item. - /// The library manager. - /// The input items. - /// The get similarity score. - /// IEnumerable{BaseItem}. - private static IEnumerable GetSimilaritems( - BaseItem item, - ILibraryManager libraryManager, - IEnumerable inputItems, - Func, List, BaseItem, int> getSimilarityScore) - { - var itemId = item.Id; - inputItems = inputItems.Where(i => i.Id != itemId); - var itemPeople = libraryManager.GetPeople(item); - var allPeople = libraryManager.GetPeople(new InternalPeopleQuery - { - AppearsInItemId = item.Id - }); - - return inputItems.Select(i => new Tuple(i, getSimilarityScore(item, itemPeople, allPeople, i))) - .Where(i => i.Item2 > 2) - .OrderByDescending(i => i.Item2) - .Select(i => i.Item1); - } - - private static IEnumerable GetTags(BaseItem item) - { - return item.Tags; - } - - /// - /// Gets the similiarity score. - /// - /// The item1. - /// The item1 people. - /// All people. - /// The item2. - /// System.Int32. - internal static int GetSimiliarityScore(BaseItem item1, List item1People, List allPeople, BaseItem item2) - { - var points = 0; - - if (!string.IsNullOrEmpty(item1.OfficialRating) && string.Equals(item1.OfficialRating, item2.OfficialRating, StringComparison.OrdinalIgnoreCase)) - { - points += 10; - } - - // Find common genres - points += item1.Genres.Where(i => item2.Genres.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 10); - - // Find common tags - points += GetTags(item1).Where(i => GetTags(item2).Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 10); - - // Find common studios - points += item1.Studios.Where(i => item2.Studios.Contains(i, StringComparer.OrdinalIgnoreCase)).Sum(i => 3); - - var item2PeopleNames = allPeople.Where(i => i.ItemId == item2.Id) - .Select(i => i.Name) - .Where(i => !string.IsNullOrWhiteSpace(i)) - .DistinctNames() - .ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); - - points += item1People.Where(i => item2PeopleNames.ContainsKey(i.Name)).Sum(i => - { - if (string.Equals(i.Type, PersonType.Director, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Director, StringComparison.OrdinalIgnoreCase)) - { - return 5; - } - - if (string.Equals(i.Type, PersonType.Actor, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Actor, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - - if (string.Equals(i.Type, PersonType.Composer, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Composer, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - - if (string.Equals(i.Type, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase)) - { - return 3; - } - - if (string.Equals(i.Type, PersonType.Writer, StringComparison.OrdinalIgnoreCase) || string.Equals(i.Role, PersonType.Writer, StringComparison.OrdinalIgnoreCase)) - { - return 2; - } - - return 1; - }); - - if (item1.ProductionYear.HasValue && item2.ProductionYear.HasValue) - { - var diff = Math.Abs(item1.ProductionYear.Value - item2.ProductionYear.Value); - - // Add if they came out within the same decade - if (diff < 10) - { - points += 2; - } - - // And more if within five years - if (diff < 5) - { - points += 2; - } - } - - return points; - } - } -} diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 04b107727..0566f4c4d 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -88,7 +88,7 @@ namespace Jellyfin.Api.Helpers throw new ResourceNotFoundException(nameof(httpRequest.Path)); } - var url = httpRequest.Path.Value.Split('.').Last(); + var url = httpRequest.Path.Value.Split('.')[^1]; if (string.IsNullOrEmpty(streamingRequest.AudioCodec)) { diff --git a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs index 4f012cab2..e90f48d2f 100644 --- a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs +++ b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Logging; namespace Jellyfin.Api.ModelBinders { @@ -11,6 +13,17 @@ namespace Jellyfin.Api.ModelBinders /// public class CommaDelimitedArrayModelBinder : IModelBinder { + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + public CommaDelimitedArrayModelBinder(ILogger logger) + { + _logger = logger; + } + /// public Task BindModelAsync(ModelBindingContext bindingContext) { @@ -20,16 +33,8 @@ namespace Jellyfin.Api.ModelBinders if (valueProviderResult.Length > 1) { - var result = Array.CreateInstance(elementType, valueProviderResult.Length); - - for (int i = 0; i < valueProviderResult.Length; i++) - { - var value = converter.ConvertFromString(valueProviderResult.Values[i].Trim()); - - result.SetValue(value, i); - } - - bindingContext.Result = ModelBindingResult.Success(result); + var typedValues = GetParsedResult(valueProviderResult.Values, elementType, converter); + bindingContext.Result = ModelBindingResult.Success(typedValues); } else { @@ -37,13 +42,8 @@ namespace Jellyfin.Api.ModelBinders if (value != null) { - var values = Array.ConvertAll( - value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries), - x => converter.ConvertFromString(x?.Trim())); - - var typedValues = Array.CreateInstance(elementType, values.Length); - values.CopyTo(typedValues, 0); - + var splitValues = value.Split(',', StringSplitOptions.RemoveEmptyEntries); + var typedValues = GetParsedResult(splitValues, elementType, converter); bindingContext.Result = ModelBindingResult.Success(typedValues); } else @@ -55,5 +55,36 @@ namespace Jellyfin.Api.ModelBinders return Task.CompletedTask; } + + private Array GetParsedResult(IReadOnlyList values, Type elementType, TypeConverter converter) + { + var parsedValues = new object?[values.Count]; + var convertedCount = 0; + for (var i = 0; i < values.Count; i++) + { + try + { + parsedValues[i] = converter.ConvertFromString(values[i].Trim()); + convertedCount++; + } + catch (FormatException e) + { + _logger.LogWarning(e, "Error converting value."); + } + } + + var typedValues = Array.CreateInstance(elementType, convertedCount); + var typedValueIndex = 0; + for (var i = 0; i < parsedValues.Length; i++) + { + if (parsedValues[i] != null) + { + typedValues.SetValue(parsedValues[i], typedValueIndex); + typedValueIndex++; + } + } + + return typedValues; + } } } diff --git a/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs index 33eda33cb..7de44aa65 100644 --- a/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs +++ b/Jellyfin.Api/Models/LibraryDtos/LibraryOptionsResultDto.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Collections.Generic; namespace Jellyfin.Api.Models.LibraryDtos { @@ -10,25 +11,21 @@ namespace Jellyfin.Api.Models.LibraryDtos /// /// Gets or sets the metadata savers. /// - [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "MetadataSavers", Justification = "Imported from ServiceStack")] - public LibraryOptionInfoDto[] MetadataSavers { get; set; } = null!; + public IReadOnlyList MetadataSavers { get; set; } = Array.Empty(); /// /// Gets or sets the metadata readers. /// - [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "MetadataReaders", Justification = "Imported from ServiceStack")] - public LibraryOptionInfoDto[] MetadataReaders { get; set; } = null!; + public IReadOnlyList MetadataReaders { get; set; } = Array.Empty(); /// /// Gets or sets the subtitle fetchers. /// - [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "SubtitleFetchers", Justification = "Imported from ServiceStack")] - public LibraryOptionInfoDto[] SubtitleFetchers { get; set; } = null!; + public IReadOnlyList SubtitleFetchers { get; set; } = Array.Empty(); /// /// Gets or sets the type options. /// - [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "TypeOptions", Justification = "Imported from ServiceStack")] - public LibraryTypeOptionsDto[] TypeOptions { get; set; } = null!; + public IReadOnlyList TypeOptions { get; set; } = Array.Empty(); } } diff --git a/Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs b/Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs index ad031e95e..20f45196d 100644 --- a/Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs +++ b/Jellyfin.Api/Models/LibraryDtos/LibraryTypeOptionsDto.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Collections.Generic; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; @@ -17,25 +18,21 @@ namespace Jellyfin.Api.Models.LibraryDtos /// /// Gets or sets the metadata fetchers. /// - [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "MetadataFetchers", Justification = "Imported from ServiceStack")] - public LibraryOptionInfoDto[] MetadataFetchers { get; set; } = null!; + public IReadOnlyList MetadataFetchers { get; set; } = Array.Empty(); /// /// Gets or sets the image fetchers. /// - [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "ImageFetchers", Justification = "Imported from ServiceStack")] - public LibraryOptionInfoDto[] ImageFetchers { get; set; } = null!; + public IReadOnlyList ImageFetchers { get; set; } = Array.Empty(); /// /// Gets or sets the supported image types. /// - [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "SupportedImageTypes", Justification = "Imported from ServiceStack")] - public ImageType[] SupportedImageTypes { get; set; } = null!; + public IReadOnlyList SupportedImageTypes { get; set; } = Array.Empty(); /// /// Gets or sets the default image options. /// - [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "DefaultImageOptions", Justification = "Imported from ServiceStack")] - public ImageOption[] DefaultImageOptions { get; set; } = null!; + public IReadOnlyList DefaultImageOptions { get; set; } = Array.Empty(); } } diff --git a/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs index 970d8acdb..f43822da7 100644 --- a/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs +++ b/Jellyfin.Api/Models/LiveTvDtos/ChannelMappingOptionsDto.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Dto; @@ -25,8 +26,7 @@ namespace Jellyfin.Api.Models.LiveTvDtos /// /// Gets or sets list of mappings. /// - [SuppressMessage("Microsoft.Performance", "CA1819:DontReturnArrays", MessageId = "Mappings", Justification = "Imported from ServiceStack")] - public NameValuePair[] Mappings { get; set; } = null!; + public IReadOnlyList Mappings { get; set; } = Array.Empty(); /// /// Gets or sets provider name. diff --git a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs index 581153393..5ca4408d1 100644 --- a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs +++ b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using MediaBrowser.Common.Json.Converters; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; namespace Jellyfin.Api.Models.LiveTvDtos { @@ -142,8 +144,7 @@ namespace Jellyfin.Api.Models.LiveTvDtos /// Optional. /// [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] - [SuppressMessage("Microsoft.Performance", "CA1819:ReturnArrays", MessageId = "EnableImageTypes", Justification = "Imported from ServiceStack")] - public ImageType[] EnableImageTypes { get; set; } = Array.Empty(); + public IReadOnlyList EnableImageTypes { get; set; } = Array.Empty(); /// /// Gets or sets include user data. @@ -167,6 +168,7 @@ namespace Jellyfin.Api.Models.LiveTvDtos /// Gets or sets 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. /// Optional. /// - public string? Fields { get; set; } + [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))] + public IReadOnlyList Fields { get; set; } = Array.Empty(); } } diff --git a/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs b/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs index f797a3807..b0b3de855 100644 --- a/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs +++ b/Jellyfin.Api/Models/MediaInfoDtos/OpenLiveStreamDto.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.CodeAnalysis; +using System; +using System.Collections.Generic; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.MediaInfo; @@ -17,8 +18,6 @@ namespace Jellyfin.Api.Models.MediaInfoDtos /// /// Gets or sets the device play protocols. /// - [SuppressMessage("Microsoft.Performance", "CA1819:DontReturnArrays", MessageId = "DevicePlayProtocols", Justification = "Imported from ServiceStack")] - [SuppressMessage("Microsoft.Performance", "SA1011:ClosingBracketsSpace", MessageId = "DevicePlayProtocols", Justification = "Imported from ServiceStack")] - public MediaProtocol[]? DirectPlayProtocols { get; set; } + public IReadOnlyList DirectPlayProtocols { get; set; } = Array.Empty(); } } diff --git a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs index 7f57358ec..8992c281d 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateDisplayPreferencesDb.cs @@ -9,6 +9,7 @@ using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Server.Implementations; using MediaBrowser.Controller; +using MediaBrowser.Controller.Library; using MediaBrowser.Model.Entities; using Microsoft.Extensions.Logging; using SQLitePCL.pretty; @@ -26,6 +27,7 @@ namespace Jellyfin.Server.Migrations.Routines private readonly IServerApplicationPaths _paths; private readonly JellyfinDbProvider _provider; private readonly JsonSerializerOptions _jsonOptions; + private readonly IUserManager _userManager; /// /// Initializes a new instance of the class. @@ -33,11 +35,17 @@ namespace Jellyfin.Server.Migrations.Routines /// The logger. /// The server application paths. /// The database provider. - public MigrateDisplayPreferencesDb(ILogger logger, IServerApplicationPaths paths, JellyfinDbProvider provider) + /// The user manager. + public MigrateDisplayPreferencesDb( + ILogger logger, + IServerApplicationPaths paths, + JellyfinDbProvider provider, + IUserManager userManager) { _logger = logger; _paths = paths; _provider = provider; + _userManager = userManager; _jsonOptions = new JsonSerializerOptions(); _jsonOptions.Converters.Add(new JsonStringEnumConverter()); } @@ -86,11 +94,19 @@ namespace Jellyfin.Server.Migrations.Routines continue; } + var dtoUserId = new Guid(result[1].ToBlob()); + var existingUser = _userManager.GetUserById(dtoUserId); + if (existingUser == null) + { + _logger.LogWarning("User with ID {UserId} does not exist in the database, skipping migration.", dtoUserId); + continue; + } + var chromecastVersion = dto.CustomPrefs.TryGetValue("chromecastVersion", out var version) ? chromecastDict[version] : ChromecastVersion.Stable; - var displayPreferences = new DisplayPreferences(new Guid(result[1].ToBlob()), result[2].ToString()) + var displayPreferences = new DisplayPreferences(dtoUserId, result[2].ToString()) { IndexBy = Enum.TryParse(dto.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null, ShowBackdrop = dto.ShowBackdrop, diff --git a/MediaBrowser.Common/Configuration/IConfigurationManager.cs b/MediaBrowser.Common/Configuration/IConfigurationManager.cs index fe726090d..fc63d9350 100644 --- a/MediaBrowser.Common/Configuration/IConfigurationManager.cs +++ b/MediaBrowser.Common/Configuration/IConfigurationManager.cs @@ -46,6 +46,13 @@ namespace MediaBrowser.Common.Configuration /// The new configuration. void ReplaceConfiguration(BaseApplicationConfiguration newConfiguration); + /// + /// Manually pre-loads a factory so that it is available pre system initialisation. + /// + /// Class to register. + void RegisterConfiguration() + where T : IConfigurationFactory; + /// /// Gets the configuration. /// diff --git a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs index bf7048c37..06a29a0db 100644 --- a/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs +++ b/MediaBrowser.Common/Json/Converters/JsonCommaDelimitedArrayConverter.cs @@ -26,19 +26,40 @@ namespace MediaBrowser.Common.Json.Converters { if (reader.TokenType == JsonTokenType.String) { - var stringEntries = reader.GetString()?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + var stringEntries = reader.GetString()?.Split(',', StringSplitOptions.RemoveEmptyEntries); if (stringEntries == null || stringEntries.Length == 0) { return Array.Empty(); } - var entries = new T[stringEntries.Length]; + var parsedValues = new object[stringEntries.Length]; + var convertedCount = 0; for (var i = 0; i < stringEntries.Length; i++) { - entries[i] = (T)_typeConverter.ConvertFrom(stringEntries[i].Trim()); + try + { + parsedValues[i] = _typeConverter.ConvertFrom(stringEntries[i].Trim()); + convertedCount++; + } + catch (FormatException) + { + // TODO log when upgraded to .Net5 + // _logger.LogWarning(e, "Error converting value."); + } } - return entries; + var typedValues = new T[convertedCount]; + var typedValueIndex = 0; + for (var i = 0; i < stringEntries.Length; i++) + { + if (parsedValues[i] != null) + { + typedValues.SetValue(parsedValues[i], typedValueIndex); + typedValueIndex++; + } + } + + return typedValues; } return JsonSerializer.Deserialize(ref reader, options); @@ -50,4 +71,4 @@ namespace MediaBrowser.Common.Json.Converters JsonSerializer.Serialize(writer, value, options); } } -} \ No newline at end of file +} diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs index 8545fd5dc..e21d8c7d1 100644 --- a/MediaBrowser.Common/Plugins/BasePlugin.cs +++ b/MediaBrowser.Common/Plugins/BasePlugin.cs @@ -83,16 +83,6 @@ namespace MediaBrowser.Common.Plugins { } - /// - public virtual void RegisterServices(IServiceCollection serviceCollection) - { - } - - /// - public virtual void UnregisterServices(IServiceCollection serviceCollection) - { - } - /// public void SetAttributes(string assemblyFilePath, string dataFolderPath, Version assemblyVersion) { @@ -185,6 +175,11 @@ namespace MediaBrowser.Common.Plugins /// The type of the configuration. public Type ConfigurationType => typeof(TConfigurationType); + /// + /// Gets or sets the event handler that is triggered when this configuration changes. + /// + public EventHandler ConfigurationChanged { get; set; } + /// /// Gets the name the assembly file. /// @@ -280,6 +275,8 @@ namespace MediaBrowser.Common.Plugins Configuration = (TConfigurationType)configuration; SaveConfiguration(); + + ConfigurationChanged.Invoke(this, configuration); } /// diff --git a/MediaBrowser.Common/Plugins/IPlugin.cs b/MediaBrowser.Common/Plugins/IPlugin.cs index 1844eb124..d583a5887 100644 --- a/MediaBrowser.Common/Plugins/IPlugin.cs +++ b/MediaBrowser.Common/Plugins/IPlugin.cs @@ -62,18 +62,6 @@ namespace MediaBrowser.Common.Plugins /// Called when just before the plugin is uninstalled from the server. /// void OnUninstalling(); - - /// - /// Registers the plugin's services to the service collection. - /// - /// The service collection. - void RegisterServices(IServiceCollection serviceCollection); - - /// - /// Unregisters the plugin's services from the service collection. - /// - /// The service collection. - void UnregisterServices(IServiceCollection serviceCollection); } public interface IHasPluginConfiguration diff --git a/MediaBrowser.Common/Plugins/IPluginServiceRegistrator.cs b/MediaBrowser.Common/Plugins/IPluginServiceRegistrator.cs new file mode 100644 index 000000000..3afe874c5 --- /dev/null +++ b/MediaBrowser.Common/Plugins/IPluginServiceRegistrator.cs @@ -0,0 +1,19 @@ +namespace MediaBrowser.Common.Plugins +{ + using Microsoft.Extensions.DependencyInjection; + + /// + /// Defines the . + /// + public interface IPluginServiceRegistrator + { + /// + /// Registers the plugin's services with the service collection. + /// + /// + /// This interface is only used for service registration and requires a parameterless constructor. + /// + /// The service collection. + void RegisterServices(IServiceCollection serviceCollection); + } +} diff --git a/MediaBrowser.Controller/Dto/DtoOptions.cs b/MediaBrowser.Controller/Dto/DtoOptions.cs index 76f20ace2..356783750 100644 --- a/MediaBrowser.Controller/Dto/DtoOptions.cs +++ b/MediaBrowser.Controller/Dto/DtoOptions.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using System.Collections.Generic; using System.Linq; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; @@ -15,9 +16,9 @@ namespace MediaBrowser.Controller.Dto ItemFields.RefreshState }; - public ItemFields[] Fields { get; set; } + public IReadOnlyList Fields { get; set; } - public ImageType[] ImageTypes { get; set; } + public IReadOnlyList ImageTypes { get; set; } public int ImageTypeLimit { get; set; } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 2fc7d45c9..1d44a5511 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -87,6 +87,8 @@ namespace MediaBrowser.Controller.Entities public const string InterviewFolderName = "interviews"; public const string SceneFolderName = "scenes"; public const string SampleFolderName = "samples"; + public const string ShortsFolderName = "shorts"; + public const string FeaturettesFolderName = "featurettes"; public static readonly string[] AllExtrasTypesFolderNames = { ExtrasFolderName, @@ -94,7 +96,9 @@ namespace MediaBrowser.Controller.Entities DeletedScenesFolderName, InterviewFolderName, SceneFolderName, - SampleFolderName + SampleFolderName, + ShortsFolderName, + FeaturettesFolderName }; [JsonIgnore] diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index 6c365caa4..54495c1c4 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -225,7 +225,7 @@ namespace MediaBrowser.Controller.LiveTv /// The fields. /// The user. /// Task. - Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> programs, ItemFields[] fields, User user = null); + Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> programs, IReadOnlyList fields, User user = null); /// /// Saves the tuner host. diff --git a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs index ac520c5c4..cc8820f39 100644 --- a/MediaBrowser.Controller/MediaEncoding/JobLogger.cs +++ b/MediaBrowser.Controller/MediaEncoding/JobLogger.cs @@ -93,7 +93,7 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (part.StartsWith("fps=", StringComparison.OrdinalIgnoreCase)) { - var rate = part.Split(new[] { '=' }, 2)[^1]; + var rate = part.Split('=', 2)[^1]; if (float.TryParse(rate, NumberStyles.Any, _usCulture, out var val)) { @@ -103,7 +103,7 @@ namespace MediaBrowser.Controller.MediaEncoding else if (state.RunTimeTicks.HasValue && part.StartsWith("time=", StringComparison.OrdinalIgnoreCase)) { - var time = part.Split(new[] { '=' }, 2).Last(); + var time = part.Split('=', 2)[^1]; if (TimeSpan.TryParse(time, _usCulture, out var val)) { @@ -116,7 +116,7 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (part.StartsWith("size=", StringComparison.OrdinalIgnoreCase)) { - var size = part.Split(new[] { '=' }, 2).Last(); + var size = part.Split('=', 2)[^1]; int? scale = null; if (size.IndexOf("kb", StringComparison.OrdinalIgnoreCase) != -1) @@ -135,7 +135,7 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (part.StartsWith("bitrate=", StringComparison.OrdinalIgnoreCase)) { - var rate = part.Split(new[] { '=' }, 2).Last(); + var rate = part.Split('=', 2)[^1]; int? scale = null; if (rate.IndexOf("kbits/s", StringComparison.OrdinalIgnoreCase) != -1) diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index cdeefbbbd..15a70e2e7 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -149,7 +149,7 @@ namespace MediaBrowser.MediaEncoding.Probing var iTunEXTC = FFProbeHelpers.GetDictionaryValue(tags, "iTunEXTC"); if (!string.IsNullOrWhiteSpace(iTunEXTC)) { - var parts = iTunEXTC.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + var parts = iTunEXTC.Split('|', StringSplitOptions.RemoveEmptyEntries); // Example // mpaa|G|100|For crude humor if (parts.Length > 1) @@ -1139,7 +1139,7 @@ namespace MediaBrowser.MediaEncoding.Probing return null; } - return value.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries) + return value.Split('/', StringSplitOptions.RemoveEmptyEntries) .Select(i => i.Trim()) .FirstOrDefault(i => !string.IsNullOrWhiteSpace(i)); } diff --git a/MediaBrowser.Model/Dlna/ContainerProfile.cs b/MediaBrowser.Model/Dlna/ContainerProfile.cs index f77d9b267..09afa64bb 100644 --- a/MediaBrowser.Model/Dlna/ContainerProfile.cs +++ b/MediaBrowser.Model/Dlna/ContainerProfile.cs @@ -34,7 +34,7 @@ namespace MediaBrowser.Model.Dlna return Array.Empty(); } - return value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + return value.Split(',', StringSplitOptions.RemoveEmptyEntries); } public bool ContainsContainer(string container) diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs index 8b73ecbd4..50e3374f7 100644 --- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs +++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs @@ -186,7 +186,7 @@ namespace MediaBrowser.Model.Dlna if (mediaProfile != null && !string.IsNullOrEmpty(mediaProfile.OrgPn)) { - orgPnValues.AddRange(mediaProfile.OrgPn.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)); + orgPnValues.AddRange(mediaProfile.OrgPn.Split(',', StringSplitOptions.RemoveEmptyEntries)); } else { diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 4959a9b92..13234c381 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -1647,7 +1647,7 @@ namespace MediaBrowser.Model.Dlna // strip spaces to avoid having to encode var values = value - .Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); + .Split('|', StringSplitOptions.RemoveEmptyEntries); if (condition.Condition == ProfileConditionType.Equals || condition.Condition == ProfileConditionType.EqualsAny) { diff --git a/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs b/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs index a8ea405e2..36a240706 100644 --- a/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs +++ b/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs @@ -2,6 +2,7 @@ #pragma warning disable CS1591 using System; +using System.Collections.Generic; using MediaBrowser.Model.Dlna; namespace MediaBrowser.Model.MediaInfo @@ -55,6 +56,6 @@ namespace MediaBrowser.Model.MediaInfo public bool EnableDirectStream { get; set; } - public MediaProtocol[] DirectPlayProtocols { get; set; } + public IReadOnlyList DirectPlayProtocols { get; set; } } } diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index 32dab60a6..9eed6172d 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -391,7 +391,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb item.Genres = Array.Empty(); foreach (var genre in result.Genre - .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Split(',', StringSplitOptions.RemoveEmptyEntries) .Select(i => i.Trim()) .Where(i => !string.IsNullOrWhiteSpace(i))) { diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs index b34e52235..e5a3e9a6a 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs @@ -170,7 +170,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb _logger.LogError(e, "Failed to retrieve series with remote id {RemoteId}", id); } - return result?.Data.First().Id.ToString(); + return result?.Data[0].Id.ToString(CultureInfo.InvariantCulture); } /// diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index f3fbe2d12..47e9d5ee8 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -147,7 +147,7 @@ namespace MediaBrowser.Providers.Subtitles string subtitleId, CancellationToken cancellationToken) { - var parts = subtitleId.Split(new[] { '_' }, 2); + var parts = subtitleId.Split('_', 2); var provider = GetProvider(parts[0]); try @@ -329,7 +329,7 @@ namespace MediaBrowser.Providers.Subtitles Index = index, ItemId = item.Id, Type = MediaStreamType.Subtitle - }).First(); + })[0]; var path = stream.Path; _monitor.ReportFileSystemChangeBeginning(path); @@ -349,10 +349,10 @@ namespace MediaBrowser.Providers.Subtitles /// public Task GetRemoteSubtitles(string id, CancellationToken cancellationToken) { - var parts = id.Split(new[] { '_' }, 2); + var parts = id.Split('_', 2); var provider = GetProvider(parts[0]); - id = parts.Last(); + id = parts[^1]; return provider.GetSubtitles(id, cancellationToken); } diff --git a/RSSDP/HttpParserBase.cs b/RSSDP/HttpParserBase.cs index a40612bc2..11202940e 100644 --- a/RSSDP/HttpParserBase.cs +++ b/RSSDP/HttpParserBase.cs @@ -119,7 +119,7 @@ namespace Rssdp.Infrastructure } else { - headersToAddTo.TryAddWithoutValidation(headerName, values.First()); + headersToAddTo.TryAddWithoutValidation(headerName, values[0]); } } @@ -151,7 +151,7 @@ namespace Rssdp.Infrastructure return lineIndex; } - private IList ParseValues(string headerValue) + private List ParseValues(string headerValue) { // This really should be better and match the HTTP 1.1 spec, // but this should actually be good enough for SSDP implementations @@ -160,7 +160,7 @@ namespace Rssdp.Infrastructure if (headerValue == "\"\"") { - values.Add(String.Empty); + values.Add(string.Empty); return values; } @@ -172,7 +172,7 @@ namespace Rssdp.Infrastructure else { var segments = headerValue.Split(SeparatorCharacters); - if (headerValue.Contains("\"")) + if (headerValue.Contains('"')) { for (int segmentIndex = 0; segmentIndex < segments.Length; segmentIndex++) { diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs index 6f3bd9132..94f2800d4 100644 --- a/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs +++ b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Jellyfin.Api.ModelBinders; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Primitives; using Moq; using Xunit; @@ -21,7 +22,7 @@ namespace Jellyfin.Api.Tests.ModelBinders var queryParamString = "lol,xd"; var queryParamType = typeof(string[]); - var modelBinder = new CommaDelimitedArrayModelBinder(); + var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger()); var valueProvider = new QueryStringValueProvider( new BindingSource(string.Empty, string.Empty, false, false), new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), @@ -46,7 +47,7 @@ namespace Jellyfin.Api.Tests.ModelBinders var queryParamString = "42,0"; var queryParamType = typeof(int[]); - var modelBinder = new CommaDelimitedArrayModelBinder(); + var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger()); var valueProvider = new QueryStringValueProvider( new BindingSource(string.Empty, string.Empty, false, false), new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), @@ -71,7 +72,7 @@ namespace Jellyfin.Api.Tests.ModelBinders var queryParamString = "How,Much"; var queryParamType = typeof(TestType[]); - var modelBinder = new CommaDelimitedArrayModelBinder(); + var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger()); var valueProvider = new QueryStringValueProvider( new BindingSource(string.Empty, string.Empty, false, false), new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), @@ -96,7 +97,7 @@ namespace Jellyfin.Api.Tests.ModelBinders var queryParamString = "How,,Much"; var queryParamType = typeof(TestType[]); - var modelBinder = new CommaDelimitedArrayModelBinder(); + var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger()); var valueProvider = new QueryStringValueProvider( new BindingSource(string.Empty, string.Empty, false, false), new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), @@ -122,7 +123,7 @@ namespace Jellyfin.Api.Tests.ModelBinders var queryParamString2 = "Much"; var queryParamType = typeof(TestType[]); - var modelBinder = new CommaDelimitedArrayModelBinder(); + var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger()); var valueProvider = new QueryStringValueProvider( new BindingSource(string.Empty, string.Empty, false, false), @@ -150,7 +151,7 @@ namespace Jellyfin.Api.Tests.ModelBinders IReadOnlyList queryParamValues = Array.Empty(); var queryParamType = typeof(TestType[]); - var modelBinder = new CommaDelimitedArrayModelBinder(); + var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger()); var valueProvider = new QueryStringValueProvider( new BindingSource(string.Empty, string.Empty, false, false), @@ -172,13 +173,13 @@ namespace Jellyfin.Api.Tests.ModelBinders } [Fact] - public async Task BindModelAsync_ThrowsIfCommaDelimitedEnumArrayQueryIsInvalid() + public async Task BindModelAsync_EnumArrayQuery_BindValidOnly() { var queryParamName = "test"; var queryParamString = "🔥,😢"; var queryParamType = typeof(TestType[]); - var modelBinder = new CommaDelimitedArrayModelBinder(); + var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger()); var valueProvider = new QueryStringValueProvider( new BindingSource(string.Empty, string.Empty, false, false), new QueryCollection(new Dictionary { { queryParamName, new StringValues(queryParamString) } }), @@ -189,20 +190,20 @@ namespace Jellyfin.Api.Tests.ModelBinders bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType); bindingContextMock.SetupProperty(b => b.Result); - Func act = async () => await modelBinder.BindModelAsync(bindingContextMock.Object); - - await Assert.ThrowsAsync(act); + await modelBinder.BindModelAsync(bindingContextMock.Object); + Assert.True(bindingContextMock.Object.Result.IsModelSet); + Assert.Empty((TestType[])bindingContextMock.Object.Result.Model); } [Fact] - public async Task BindModelAsync_ThrowsIfCommaDelimitedEnumArrayQueryIsInvalid2() + public async Task BindModelAsync_EnumArrayQuery_BindValidOnly_2() { var queryParamName = "test"; var queryParamString1 = "How"; var queryParamString2 = "😱"; var queryParamType = typeof(TestType[]); - var modelBinder = new CommaDelimitedArrayModelBinder(); + var modelBinder = new CommaDelimitedArrayModelBinder(new NullLogger()); var valueProvider = new QueryStringValueProvider( new BindingSource(string.Empty, string.Empty, false, false), @@ -217,9 +218,9 @@ namespace Jellyfin.Api.Tests.ModelBinders bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType); bindingContextMock.SetupProperty(b => b.Result); - Func act = async () => await modelBinder.BindModelAsync(bindingContextMock.Object); - - await Assert.ThrowsAsync(act); + await modelBinder.BindModelAsync(bindingContextMock.Object); + Assert.True(bindingContextMock.Object.Result.IsModelSet); + Assert.Single((TestType[])bindingContextMock.Object.Result.Model); } } }