Pārlūkot izejas kodu

Merge branch 'master' into unharden-for-lxc

Cody Robibero 3 gadi atpakaļ
vecāks
revīzija
cecfdeeec3
100 mainītis faili ar 1072 papildinājumiem un 1175 dzēšanām
  1. 1 0
      .copr
  2. 0 1
      .copr/Makefile
  3. 23 31
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  4. 0 1
      Emby.Dlna/DlnaManager.cs
  5. 5 5
      Emby.Naming/AudioBook/AudioBookListResolver.cs
  6. 2 2
      Emby.Naming/AudioBook/AudioBookResolver.cs
  7. 40 13
      Emby.Naming/Common/NamingOptions.cs
  8. 6 5
      Emby.Naming/Subtitles/SubtitleParser.cs
  9. 2 2
      Emby.Naming/TV/EpisodeResolver.cs
  10. 0 1
      Emby.Naming/TV/SeriesPathParser.cs
  11. 0 1
      Emby.Naming/Video/CleanStringParser.cs
  12. 74 29
      Emby.Naming/Video/ExtraResolver.cs
  13. 17 12
      Emby.Naming/Video/FileStack.cs
  14. 48 0
      Emby.Naming/Video/FileStackRule.cs
  15. 68 151
      Emby.Naming/Video/StackResolver.cs
  16. 2 2
      Emby.Naming/Video/StubResolver.cs
  17. 6 7
      Emby.Naming/Video/VideoInfo.cs
  18. 30 144
      Emby.Naming/Video/VideoListResolver.cs
  19. 4 3
      Emby.Naming/Video/VideoResolver.cs
  20. 2 1
      Emby.Notifications/NotificationEntryPoint.cs
  21. 2 1
      Emby.Photos/PhotoProvider.cs
  22. 1 1
      Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
  23. 0 1
      Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
  24. 0 116
      Emby.Server.Implementations/Archiving/ZipClient.cs
  25. 5 4
      Emby.Server.Implementations/Channels/ChannelManager.cs
  26. 2 1
      Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
  27. 2 2
      Emby.Server.Implementations/Data/BaseSqliteRepository.cs
  28. 160 90
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  29. 3 3
      Emby.Server.Implementations/Dto/DtoService.cs
  30. 1 1
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  31. 0 1
      Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
  32. 0 1
      Emby.Server.Implementations/HttpServer/Security/AuthService.cs
  33. 8 8
      Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
  34. 4 2
      Emby.Server.Implementations/Images/DynamicImageProvider.cs
  35. 1 3
      Emby.Server.Implementations/Images/GenreImageProvider.cs
  36. 3 3
      Emby.Server.Implementations/Images/MusicGenreImageProvider.cs
  37. 4 14
      Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
  38. 86 80
      Emby.Server.Implementations/Library/LibraryManager.cs
  39. 8 7
      Emby.Server.Implementations/Library/MediaStreamSelector.cs
  40. 2 2
      Emby.Server.Implementations/Library/MusicManager.cs
  41. 22 8
      Emby.Server.Implementations/Library/PathExtensions.cs
  42. 0 2
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
  43. 36 85
      Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
  44. 2 1
      Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
  45. 1 1
      Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
  46. 69 44
      Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
  47. 2 1
      Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
  48. 3 2
      Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
  49. 23 22
      Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
  50. 33 7
      Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
  51. 23 26
      Emby.Server.Implementations/Library/SearchEngine.cs
  52. 11 13
      Emby.Server.Implementations/Library/UserViewManager.cs
  53. 2 1
      Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
  54. 2 2
      Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
  55. 2 1
      Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
  56. 2 1
      Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
  57. 12 11
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  58. 9 10
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  59. 4 4
      Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
  60. 6 6
      Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
  61. 14 16
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  62. 0 1
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
  63. 2 1
      Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
  64. 45 1
      Emby.Server.Implementations/Localization/Core/cy.json
  65. 3 1
      Emby.Server.Implementations/Localization/Core/fil.json
  66. 3 3
      Emby.Server.Implementations/Localization/Core/ms.json
  67. 1 1
      Emby.Server.Implementations/Localization/Core/ta.json
  68. 1 1
      Emby.Server.Implementations/Localization/LocalizationManager.cs
  69. 3 2
      Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
  70. 2 1
      Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
  71. 2 1
      Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
  72. 1 1
      Emby.Server.Implementations/Serialization/MyXmlSerializer.cs
  73. 1 1
      Emby.Server.Implementations/Session/SessionManager.cs
  74. 4 4
      Emby.Server.Implementations/TV/TVSeriesManager.cs
  75. 3 6
      Emby.Server.Implementations/Updates/InstallationManager.cs
  76. 4 4
      Jellyfin.Api/Controllers/ArtistsController.cs
  77. 2 3
      Jellyfin.Api/Controllers/FilterController.cs
  78. 7 7
      Jellyfin.Api/Controllers/GenresController.cs
  79. 6 7
      Jellyfin.Api/Controllers/ItemsController.cs
  80. 26 25
      Jellyfin.Api/Controllers/LibraryController.cs
  81. 13 15
      Jellyfin.Api/Controllers/MoviesController.cs
  82. 7 7
      Jellyfin.Api/Controllers/MusicGenresController.cs
  83. 0 1
      Jellyfin.Api/Controllers/PluginsController.cs
  84. 0 5
      Jellyfin.Api/Controllers/RemoteImageController.cs
  85. 2 3
      Jellyfin.Api/Controllers/SearchController.cs
  86. 1 1
      Jellyfin.Api/Controllers/SessionController.cs
  87. 2 2
      Jellyfin.Api/Controllers/StudiosController.cs
  88. 1 1
      Jellyfin.Api/Controllers/SuggestionsController.cs
  89. 0 1
      Jellyfin.Api/Controllers/SystemController.cs
  90. 7 7
      Jellyfin.Api/Controllers/TvShowsController.cs
  91. 2 3
      Jellyfin.Api/Controllers/UserLibraryController.cs
  92. 4 3
      Jellyfin.Api/Controllers/YearsController.cs
  93. 0 16
      Jellyfin.Api/Helpers/RequestHelpers.cs
  94. 1 1
      Jellyfin.Api/Jellyfin.Api.csproj
  95. 2 1
      Jellyfin.Server.Implementations/Devices/DeviceManager.cs
  96. 5 5
      Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
  97. 2 2
      Jellyfin.Server/Jellyfin.Server.csproj
  98. 7 2
      Jellyfin.Server/Migrations/MigrationRunner.cs
  99. 0 10
      MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs
  100. 0 2
      MediaBrowser.Controller/Entities/Audio/Audio.cs

+ 1 - 0
.copr

@@ -0,0 +1 @@
+fedora

+ 0 - 1
.copr/Makefile

@@ -1 +0,0 @@
-../fedora/Makefile

+ 23 - 31
Emby.Dlna/ContentDirectory/ControlHandler.cs

@@ -18,11 +18,8 @@ using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.TV;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dto;
@@ -30,12 +27,7 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Querying;
 using Microsoft.Extensions.Logging;
-using Book = MediaBrowser.Controller.Entities.Book;
-using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 using Genre = MediaBrowser.Controller.Entities.Genre;
-using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
-using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
-using Series = MediaBrowser.Controller.Entities.TV.Series;
 
 namespace Emby.Dlna.ContentDirectory
 {
@@ -539,7 +531,7 @@ namespace Emby.Dlna.ContentDirectory
                 User = user,
                 Recursive = true,
                 IsMissing = false,
-                ExcludeItemTypes = new[] { nameof(Book) },
+                ExcludeItemTypes = new[] { BaseItemKind.Book },
                 IsFolder = isFolder,
                 MediaTypes = mediaTypes,
                 DtoOptions = GetDtoOptions()
@@ -619,7 +611,7 @@ namespace Emby.Dlna.ContentDirectory
                 Limit = limit,
                 StartIndex = startIndex,
                 IsVirtualItem = false,
-                ExcludeItemTypes = new[] { nameof(Book) },
+                ExcludeItemTypes = new[] { BaseItemKind.Book },
                 IsPlaceHolder = false,
                 DtoOptions = GetDtoOptions(),
                 OrderBy = GetOrderBy(sort, folder.IsPreSorted)
@@ -644,7 +636,7 @@ namespace Emby.Dlna.ContentDirectory
             {
                 StartIndex = startIndex,
                 Limit = limit,
-                IncludeItemTypes = new[] { nameof(LiveTvChannel) },
+                IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel },
                 OrderBy = GetOrderBy(sort, false)
             };
 
@@ -675,23 +667,23 @@ namespace Emby.Dlna.ContentDirectory
             switch (stubType)
             {
                 case StubType.Latest:
-                    return GetLatest(item, query, nameof(Audio));
+                    return GetLatest(item, query, BaseItemKind.Audio);
                 case StubType.Playlists:
                     return GetMusicPlaylists(query);
                 case StubType.Albums:
-                    return GetChildrenOfItem(item, query, nameof(MusicAlbum));
+                    return GetChildrenOfItem(item, query, BaseItemKind.MusicAlbum);
                 case StubType.Artists:
                     return GetMusicArtists(item, query);
                 case StubType.AlbumArtists:
                     return GetMusicAlbumArtists(item, query);
                 case StubType.FavoriteAlbums:
-                    return GetChildrenOfItem(item, query, nameof(MusicAlbum), true);
+                    return GetChildrenOfItem(item, query, BaseItemKind.MusicAlbum, true);
                 case StubType.FavoriteArtists:
                     return GetFavoriteArtists(item, query);
                 case StubType.FavoriteSongs:
-                    return GetChildrenOfItem(item, query, nameof(Audio), true);
+                    return GetChildrenOfItem(item, query, BaseItemKind.Audio, true);
                 case StubType.Songs:
-                    return GetChildrenOfItem(item, query, nameof(Audio));
+                    return GetChildrenOfItem(item, query, BaseItemKind.Audio);
                 case StubType.Genres:
                     return GetMusicGenres(item, query);
             }
@@ -746,13 +738,13 @@ namespace Emby.Dlna.ContentDirectory
                 case StubType.ContinueWatching:
                     return GetMovieContinueWatching(item, query);
                 case StubType.Latest:
-                    return GetLatest(item, query, nameof(Movie));
+                    return GetLatest(item, query, BaseItemKind.Movie);
                 case StubType.Movies:
-                    return GetChildrenOfItem(item, query, nameof(Movie));
+                    return GetChildrenOfItem(item, query, BaseItemKind.Movie);
                 case StubType.Collections:
                     return GetMovieCollections(query);
                 case StubType.Favorites:
-                    return GetChildrenOfItem(item, query, nameof(Movie), true);
+                    return GetChildrenOfItem(item, query, BaseItemKind.Movie, true);
                 case StubType.Genres:
                     return GetGenres(item, query);
             }
@@ -831,13 +823,13 @@ namespace Emby.Dlna.ContentDirectory
                 case StubType.NextUp:
                     return GetNextUp(item, query);
                 case StubType.Latest:
-                    return GetLatest(item, query, nameof(Episode));
+                    return GetLatest(item, query, BaseItemKind.Episode);
                 case StubType.Series:
-                    return GetChildrenOfItem(item, query, nameof(Series));
+                    return GetChildrenOfItem(item, query, BaseItemKind.Series);
                 case StubType.FavoriteSeries:
-                    return GetChildrenOfItem(item, query, nameof(Series), true);
+                    return GetChildrenOfItem(item, query, BaseItemKind.Series, true);
                 case StubType.FavoriteEpisodes:
-                    return GetChildrenOfItem(item, query, nameof(Episode), true);
+                    return GetChildrenOfItem(item, query, BaseItemKind.Episode, true);
                 case StubType.Genres:
                     return GetGenres(item, query);
             }
@@ -898,7 +890,7 @@ namespace Emby.Dlna.ContentDirectory
         private QueryResult<ServerItem> GetMovieCollections(InternalItemsQuery query)
         {
             query.Recursive = true;
-            query.IncludeItemTypes = new[] { nameof(BoxSet) };
+            query.IncludeItemTypes = new[] { BaseItemKind.BoxSet };
 
             var result = _libraryManager.GetItemsResult(query);
 
@@ -913,7 +905,7 @@ namespace Emby.Dlna.ContentDirectory
         /// <param name="itemType">The item type.</param>
         /// <param name="isFavorite">A value indicating whether to only fetch favorite items.</param>
         /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
-        private QueryResult<ServerItem> GetChildrenOfItem(BaseItem parent, InternalItemsQuery query, string itemType, bool isFavorite = false)
+        private QueryResult<ServerItem> GetChildrenOfItem(BaseItem parent, InternalItemsQuery query, BaseItemKind itemType, bool isFavorite = false)
         {
             query.Recursive = true;
             query.Parent = parent;
@@ -1013,7 +1005,7 @@ namespace Emby.Dlna.ContentDirectory
         private QueryResult<ServerItem> GetMusicPlaylists(InternalItemsQuery query)
         {
             query.Parent = null;
-            query.IncludeItemTypes = new[] { nameof(Playlist) };
+            query.IncludeItemTypes = new[] { BaseItemKind.Playlist };
             query.Recursive = true;
 
             var result = _libraryManager.GetItemsResult(query);
@@ -1052,7 +1044,7 @@ namespace Emby.Dlna.ContentDirectory
         /// <param name="query">The <see cref="InternalItemsQuery"/>.</param>
         /// <param name="itemType">The item type.</param>
         /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
-        private QueryResult<ServerItem> GetLatest(BaseItem parent, InternalItemsQuery query, string itemType)
+        private QueryResult<ServerItem> GetLatest(BaseItem parent, InternalItemsQuery query, BaseItemKind itemType)
         {
             query.OrderBy = Array.Empty<(string, SortOrder)>();
 
@@ -1086,7 +1078,7 @@ namespace Emby.Dlna.ContentDirectory
             {
                 Recursive = true,
                 ArtistIds = new[] { item.Id },
-                IncludeItemTypes = new[] { nameof(MusicAlbum) },
+                IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
                 Limit = limit,
                 StartIndex = startIndex,
                 DtoOptions = GetDtoOptions(),
@@ -1115,8 +1107,8 @@ namespace Emby.Dlna.ContentDirectory
                 GenreIds = new[] { item.Id },
                 IncludeItemTypes = new[]
                 {
-                    nameof(Movie),
-                    nameof(Series)
+                    BaseItemKind.Movie,
+                    BaseItemKind.Series
                 },
                 Limit = limit,
                 StartIndex = startIndex,
@@ -1144,7 +1136,7 @@ namespace Emby.Dlna.ContentDirectory
             {
                 Recursive = true,
                 GenreIds = new[] { item.Id },
-                IncludeItemTypes = new[] { nameof(MusicAlbum) },
+                IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
                 Limit = limit,
                 StartIndex = startIndex,
                 DtoOptions = GetDtoOptions(),

+ 0 - 1
Emby.Dlna/DlnaManager.cs

@@ -5,7 +5,6 @@ using System.Globalization;
 using System.IO;
 using System.Linq;
 using System.Reflection;
-using System.Text;
 using System.Text.Json;
 using System.Text.RegularExpressions;
 using System.Threading.Tasks;

+ 5 - 5
Emby.Naming/AudioBook/AudioBookListResolver.cs

@@ -14,6 +14,7 @@ namespace Emby.Naming.AudioBook
     public class AudioBookListResolver
     {
         private readonly NamingOptions _options;
+        private readonly AudioBookResolver _audioBookResolver;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="AudioBookListResolver"/> class.
@@ -22,6 +23,7 @@ namespace Emby.Naming.AudioBook
         public AudioBookListResolver(NamingOptions options)
         {
             _options = options;
+            _audioBookResolver = new AudioBookResolver(_options);
         }
 
         /// <summary>
@@ -31,21 +33,19 @@ namespace Emby.Naming.AudioBook
         /// <returns>Returns IEnumerable of <see cref="AudioBookInfo"/>.</returns>
         public IEnumerable<AudioBookInfo> Resolve(IEnumerable<FileSystemMetadata> files)
         {
-            var audioBookResolver = new AudioBookResolver(_options);
 
             // File with empty fullname will be sorted out here.
             var audiobookFileInfos = files
-                .Select(i => audioBookResolver.Resolve(i.FullName))
+                .Select(i => _audioBookResolver.Resolve(i.FullName))
                 .OfType<AudioBookFileInfo>()
                 .ToList();
 
-            var stackResult = new StackResolver(_options)
-                .ResolveAudioBooks(audiobookFileInfos);
+            var stackResult = StackResolver.ResolveAudioBooks(audiobookFileInfos);
 
             foreach (var stack in stackResult)
             {
                 var stackFiles = stack.Files
-                    .Select(i => audioBookResolver.Resolve(i))
+                    .Select(i => _audioBookResolver.Resolve(i))
                     .OfType<AudioBookFileInfo>()
                     .ToList();
 

+ 2 - 2
Emby.Naming/AudioBook/AudioBookResolver.cs

@@ -1,7 +1,7 @@
 using System;
 using System.IO;
-using System.Linq;
 using Emby.Naming.Common;
+using Jellyfin.Extensions;
 
 namespace Emby.Naming.AudioBook
 {
@@ -37,7 +37,7 @@ namespace Emby.Naming.AudioBook
             var extension = Path.GetExtension(path);
 
             // Check supported extensions
-            if (!_options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+            if (!_options.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
             {
                 return null;
             }

+ 40 - 13
Emby.Naming/Common/NamingOptions.cs

@@ -1,6 +1,7 @@
 #pragma warning disable CA1819
 
 using System;
+using System.Collections.Generic;
 using System.Linq;
 using System.Text.RegularExpressions;
 using Emby.Naming.Video;
@@ -124,11 +125,11 @@ namespace Emby.Naming.Common
                     token: "DSR")
             };
 
-            VideoFileStackingExpressions = new[]
+            VideoFileStackingRules = new[]
             {
-                "(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[0-9]+)(?<ignore>.*?)(?<extension>\\.[^.]+)$",
-                "(?<title>.*?)(?<volume>[ _.-]*(?:cd|dvd|p(?:ar)?t|dis[ck])[ _.-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$",
-                "(?<title>.*?)(?<volume>[ ._-]*[a-d])(?<ignore>.*?)(?<extension>\\.[^.]+)$"
+                new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
+                new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false),
+                new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?<number>[a-d])(?:\.[^.]+)?$", false)
             };
 
             CleanDateTimes = new[]
@@ -403,6 +404,12 @@ namespace Emby.Naming.Common
 
             VideoExtraRules = new[]
             {
+                new ExtraRule(
+                    ExtraType.Trailer,
+                    ExtraRuleType.DirectoryName,
+                    "trailers",
+                    MediaType.Video),
+
                 new ExtraRule(
                     ExtraType.Trailer,
                     ExtraRuleType.Filename,
@@ -469,6 +476,12 @@ namespace Emby.Naming.Common
                     "theme",
                     MediaType.Audio),
 
+                new ExtraRule(
+                    ExtraType.ThemeSong,
+                    ExtraRuleType.DirectoryName,
+                    "theme-music",
+                    MediaType.Audio),
+
                 new ExtraRule(
                     ExtraType.Scene,
                     ExtraRuleType.Suffix,
@@ -563,7 +576,7 @@ namespace Emby.Naming.Common
                     ExtraType.Unknown,
                     ExtraRuleType.DirectoryName,
                     "extras",
-                    MediaType.Video),
+                    MediaType.Video)
             };
 
             Format3DRules = new[]
@@ -675,9 +688,29 @@ namespace Emby.Naming.Common
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .ToArray();
 
+            AllExtrasTypesFolderNames = new Dictionary<string, ExtraType>(StringComparer.OrdinalIgnoreCase)
+            {
+                ["trailers"] = ExtraType.Trailer,
+                ["theme-music"] = ExtraType.ThemeSong,
+                ["backdrops"] = ExtraType.ThemeVideo,
+                ["extras"] = ExtraType.Unknown,
+                ["behind the scenes"] = ExtraType.BehindTheScenes,
+                ["deleted scenes"] = ExtraType.DeletedScene,
+                ["interviews"] = ExtraType.Interview,
+                ["scenes"] = ExtraType.Scene,
+                ["samples"] = ExtraType.Sample,
+                ["shorts"] = ExtraType.Clip,
+                ["featurettes"] = ExtraType.Clip
+            };
+
             Compile();
         }
 
+        /// <summary>
+        /// Gets or sets the folder name to extra types mapping.
+        /// </summary>
+        public Dictionary<string, ExtraType> AllExtrasTypesFolderNames { get; set; }
+
         /// <summary>
         /// Gets or sets list of audio file extensions.
         /// </summary>
@@ -759,9 +792,9 @@ namespace Emby.Naming.Common
         public Format3DRule[] Format3DRules { get; set; }
 
         /// <summary>
-        /// Gets or sets list of raw video file-stacking expressions strings.
+        /// Gets the file stacking rules.
         /// </summary>
-        public string[] VideoFileStackingExpressions { get; set; }
+        public FileStackRule[] VideoFileStackingRules { get; }
 
         /// <summary>
         /// Gets or sets list of raw clean DateTimes regular expressions strings.
@@ -783,11 +816,6 @@ namespace Emby.Naming.Common
         /// </summary>
         public ExtraRule[] VideoExtraRules { get; set; }
 
-        /// <summary>
-        /// Gets list of video file-stack regular expressions.
-        /// </summary>
-        public Regex[] VideoFileStackingRegexes { get; private set; } = Array.Empty<Regex>();
-
         /// <summary>
         /// Gets list of clean datetime regular expressions.
         /// </summary>
@@ -813,7 +841,6 @@ namespace Emby.Naming.Common
         /// </summary>
         public void Compile()
         {
-            VideoFileStackingRegexes = VideoFileStackingExpressions.Select(Compile).ToArray();
             CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
             CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
             EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();

+ 6 - 5
Emby.Naming/Subtitles/SubtitleParser.cs

@@ -2,6 +2,7 @@ using System;
 using System.IO;
 using System.Linq;
 using Emby.Naming.Common;
+using Jellyfin.Extensions;
 
 namespace Emby.Naming.Subtitles
 {
@@ -34,7 +35,7 @@ namespace Emby.Naming.Subtitles
             }
 
             var extension = Path.GetExtension(path);
-            if (!_options.SubtitleFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+            if (!_options.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
             {
                 return null;
             }
@@ -42,11 +43,11 @@ namespace Emby.Naming.Subtitles
             var flags = GetFlags(path);
             var info = new SubtitleInfo(
                 path,
-                _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)),
-                _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)));
+                _options.SubtitleDefaultFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)),
+                _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparison.OrdinalIgnoreCase)));
 
-            var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparer.OrdinalIgnoreCase)
-                && !_options.SubtitleForcedFlags.Contains(i, StringComparer.OrdinalIgnoreCase))
+            var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparison.OrdinalIgnoreCase)
+                && !_options.SubtitleForcedFlags.Contains(i, StringComparison.OrdinalIgnoreCase))
                 .ToList();
 
             // Should have a name, language and file extension

+ 2 - 2
Emby.Naming/TV/EpisodeResolver.cs

@@ -1,8 +1,8 @@
 using System;
 using System.IO;
-using System.Linq;
 using Emby.Naming.Common;
 using Emby.Naming.Video;
+using Jellyfin.Extensions;
 
 namespace Emby.Naming.TV
 {
@@ -48,7 +48,7 @@ namespace Emby.Naming.TV
             {
                 var extension = Path.GetExtension(path);
                 // Check supported extensions
-                if (!_options.VideoFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+                if (!_options.VideoFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
                 {
                     // It's not supported. Check stub extensions
                     if (!StubResolver.TryResolveFile(path, _options, out stubType))

+ 0 - 1
Emby.Naming/TV/SeriesPathParser.cs

@@ -1,4 +1,3 @@
-using System.Globalization;
 using Emby.Naming.Common;
 
 namespace Emby.Naming.TV

+ 0 - 1
Emby.Naming/Video/CleanStringParser.cs

@@ -1,4 +1,3 @@
-using System;
 using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.Text.RegularExpressions;

+ 74 - 29
Emby.Naming/Video/ExtraResolver.cs

@@ -1,5 +1,7 @@
 using System;
+using System.Collections.Generic;
 using System.IO;
+using System.Linq;
 using System.Text.RegularExpressions;
 using Emby.Naming.Audio;
 using Emby.Naming.Common;
@@ -9,45 +11,27 @@ namespace Emby.Naming.Video
     /// <summary>
     /// Resolve if file is extra for video.
     /// </summary>
-    public class ExtraResolver
+    public static class ExtraResolver
     {
-        private static readonly char[] _digits = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
-        private readonly NamingOptions _options;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ExtraResolver"/> class.
-        /// </summary>
-        /// <param name="options"><see cref="NamingOptions"/> object containing VideoExtraRules and passed to <see cref="AudioFileParser"/> and <see cref="VideoResolver"/>.</param>
-        public ExtraResolver(NamingOptions options)
-        {
-            _options = options;
-        }
+        private static readonly char[] _digits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
 
         /// <summary>
         /// Attempts to resolve if file is extra.
         /// </summary>
         /// <param name="path">Path to file.</param>
+        /// <param name="namingOptions">The naming options.</param>
         /// <returns>Returns <see cref="ExtraResult"/> object.</returns>
-        public ExtraResult GetExtraInfo(string path)
+        public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions)
         {
             var result = new ExtraResult();
 
-            for (var i = 0; i < _options.VideoExtraRules.Length; i++)
+            for (var i = 0; i < namingOptions.VideoExtraRules.Length; i++)
             {
-                var rule = _options.VideoExtraRules[i];
-                if (rule.MediaType == MediaType.Audio)
+                var rule = namingOptions.VideoExtraRules[i];
+                if ((rule.MediaType == MediaType.Audio && !AudioFileParser.IsAudioFile(path, namingOptions))
+                    || (rule.MediaType == MediaType.Video && !VideoResolver.IsVideoFile(path, namingOptions)))
                 {
-                    if (!AudioFileParser.IsAudioFile(path, _options))
-                    {
-                        continue;
-                    }
-                }
-                else if (rule.MediaType == MediaType.Video)
-                {
-                    if (!VideoResolver.IsVideoFile(path, _options))
-                    {
-                        continue;
-                    }
+                    continue;
                 }
 
                 var pathSpan = path.AsSpan();
@@ -76,9 +60,9 @@ namespace Emby.Naming.Video
                 {
                     var filename = Path.GetFileName(path);
 
-                    var regex = new Regex(rule.Token, RegexOptions.IgnoreCase);
+                    var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
 
-                    if (regex.IsMatch(filename))
+                    if (isMatch)
                     {
                         result.ExtraType = rule.ExtraType;
                         result.Rule = rule;
@@ -102,5 +86,66 @@ namespace Emby.Naming.Video
 
             return result;
         }
+
+        /// <summary>
+        /// Finds extras matching the video info.
+        /// </summary>
+        /// <param name="files">The list of file video infos.</param>
+        /// <param name="videoInfo">The video to compare against.</param>
+        /// <param name="videoFlagDelimiters">The video flag delimiters.</param>
+        /// <returns>A list of video extras for [videoInfo].</returns>
+        public static IReadOnlyList<VideoFileInfo> GetExtras(IReadOnlyList<VideoInfo> files, VideoFileInfo videoInfo, ReadOnlySpan<char> videoFlagDelimiters)
+        {
+            var parentDir = videoInfo.IsDirectory ? videoInfo.Path : Path.GetDirectoryName(videoInfo.Path.AsSpan());
+
+            var trimmedFileNameWithoutExtension = TrimFilenameDelimiters(videoInfo.FileNameWithoutExtension, videoFlagDelimiters);
+            var trimmedVideoInfoName = TrimFilenameDelimiters(videoInfo.Name, videoFlagDelimiters);
+
+            var result = new List<VideoFileInfo>();
+            for (var pos = files.Count - 1; pos >= 0; pos--)
+            {
+                var current = files[pos];
+                // ignore non-extras and multi-file (can this happen?)
+                if (current.ExtraType == null || current.Files.Count > 1)
+                {
+                    continue;
+                }
+
+                var currentFile = current.Files[0];
+                var trimmedCurrentFileName = TrimFilenameDelimiters(currentFile.Name, videoFlagDelimiters);
+
+                // first check filenames
+                bool isValid = StartsWith(trimmedCurrentFileName, trimmedFileNameWithoutExtension)
+                               || (StartsWith(trimmedCurrentFileName, trimmedVideoInfoName) && currentFile.Year == videoInfo.Year);
+
+                // then by directory
+                if (!isValid)
+                {
+                    // When the extra rule type is DirectoryName we must go one level higher to get the "real" dir name
+                    var currentParentDir = currentFile.ExtraRule?.RuleType == ExtraRuleType.DirectoryName
+                        ? Path.GetDirectoryName(Path.GetDirectoryName(currentFile.Path.AsSpan()))
+                        : Path.GetDirectoryName(currentFile.Path.AsSpan());
+
+                    isValid = !currentParentDir.IsEmpty && !parentDir.IsEmpty && currentParentDir.Equals(parentDir, StringComparison.OrdinalIgnoreCase);
+                }
+
+                if (isValid)
+                {
+                    result.Add(currentFile);
+                }
+            }
+
+            return result.OrderBy(r => r.Path).ToArray();
+        }
+
+        private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters)
+        {
+            return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
+        }
+
+        private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName)
+        {
+            return !baseName.IsEmpty && fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase);
+        }
     }
 }

+ 17 - 12
Emby.Naming/Video/FileStack.cs

@@ -1,6 +1,6 @@
 using System;
 using System.Collections.Generic;
-using System.Linq;
+using Jellyfin.Extensions;
 
 namespace Emby.Naming.Video
 {
@@ -12,25 +12,30 @@ namespace Emby.Naming.Video
         /// <summary>
         /// Initializes a new instance of the <see cref="FileStack"/> class.
         /// </summary>
-        public FileStack()
+        /// <param name="name">The stack name.</param>
+        /// <param name="isDirectory">Whether the stack files are directories.</param>
+        /// <param name="files">The stack files.</param>
+        public FileStack(string name, bool isDirectory, IReadOnlyList<string> files)
         {
-            Files = new List<string>();
+            Name = name;
+            IsDirectoryStack = isDirectory;
+            Files = files;
         }
 
         /// <summary>
-        /// Gets or sets name of file stack.
+        /// Gets the name of file stack.
         /// </summary>
-        public string Name { get; set; } = string.Empty;
+        public string Name { get; }
 
         /// <summary>
-        /// Gets or sets list of paths in stack.
+        /// Gets the list of paths in stack.
         /// </summary>
-        public List<string> Files { get; set; }
+        public IReadOnlyList<string> Files { get; }
 
         /// <summary>
-        /// Gets or sets a value indicating whether stack is directory stack.
+        /// Gets a value indicating whether stack is directory stack.
         /// </summary>
-        public bool IsDirectoryStack { get; set; }
+        public bool IsDirectoryStack { get; }
 
         /// <summary>
         /// Helper function to determine if path is in the stack.
@@ -40,12 +45,12 @@ namespace Emby.Naming.Video
         /// <returns>True if file is in the stack.</returns>
         public bool ContainsFile(string file, bool isDirectory)
         {
-            if (IsDirectoryStack == isDirectory)
+            if (string.IsNullOrEmpty(file))
             {
-                return Files.Contains(file, StringComparer.OrdinalIgnoreCase);
+                return false;
             }
 
-            return false;
+            return IsDirectoryStack == isDirectory && Files.Contains(file, StringComparison.OrdinalIgnoreCase);
         }
     }
 }

+ 48 - 0
Emby.Naming/Video/FileStackRule.cs

@@ -0,0 +1,48 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.RegularExpressions;
+
+namespace Emby.Naming.Video;
+
+/// <summary>
+/// Regex based rule for file stacking (eg. disc1, disc2).
+/// </summary>
+public class FileStackRule
+{
+    private readonly Regex _tokenRegex;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="FileStackRule"/> class.
+    /// </summary>
+    /// <param name="token">Token.</param>
+    /// <param name="isNumerical">Whether the file stack rule uses numerical or alphabetical numbering.</param>
+    public FileStackRule(string token, bool isNumerical)
+    {
+        _tokenRegex = new Regex(token, RegexOptions.IgnoreCase);
+        IsNumerical = isNumerical;
+    }
+
+    /// <summary>
+    /// Gets a value indicating whether the rule uses numerical or alphabetical numbering.
+    /// </summary>
+    public bool IsNumerical { get; }
+
+    /// <summary>
+    /// Match the input against the rule regex.
+    /// </summary>
+    /// <param name="input">The input.</param>
+    /// <param name="result">The part type and number or <c>null</c>.</param>
+    /// <returns>A value indicating whether the input matched the rule.</returns>
+    public bool Match(string input, [NotNullWhen(true)] out (string StackName, string PartType, string PartNumber)? result)
+    {
+        result = null;
+        var match = _tokenRegex.Match(input);
+        if (!match.Success)
+        {
+            return false;
+        }
+
+        var partType = match.Groups["parttype"].Success ? match.Groups["parttype"].Value : "unknown";
+        result = (match.Groups["filename"].Value, partType, match.Groups["number"].Value);
+        return true;
+    }
+}

+ 68 - 151
Emby.Naming/Video/StackResolver.cs

@@ -2,7 +2,6 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
-using System.Text.RegularExpressions;
 using Emby.Naming.AudioBook;
 using Emby.Naming.Common;
 using MediaBrowser.Model.IO;
@@ -12,37 +11,28 @@ namespace Emby.Naming.Video
     /// <summary>
     /// Resolve <see cref="FileStack"/> from list of paths.
     /// </summary>
-    public class StackResolver
+    public static class StackResolver
     {
-        private readonly NamingOptions _options;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="StackResolver"/> class.
-        /// </summary>
-        /// <param name="options"><see cref="NamingOptions"/> object containing VideoFileStackingRegexes and passes options to <see cref="VideoResolver"/>.</param>
-        public StackResolver(NamingOptions options)
-        {
-            _options = options;
-        }
-
         /// <summary>
         /// Resolves only directories from paths.
         /// </summary>
         /// <param name="files">List of paths.</param>
+        /// <param name="namingOptions">The naming options.</param>
         /// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
-        public IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files)
+        public static IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files, NamingOptions namingOptions)
         {
-            return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }));
+            return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true }), namingOptions);
         }
 
         /// <summary>
         /// Resolves only files from paths.
         /// </summary>
         /// <param name="files">List of paths.</param>
+        /// <param name="namingOptions">The naming options.</param>
         /// <returns>Enumerable <see cref="FileStack"/> of files.</returns>
-        public IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files)
+        public static IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files, NamingOptions namingOptions)
         {
-            return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }));
+            return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false }), namingOptions);
         }
 
         /// <summary>
@@ -50,7 +40,7 @@ namespace Emby.Naming.Video
         /// </summary>
         /// <param name="files">List of paths.</param>
         /// <returns>Enumerable <see cref="FileStack"/> of directories.</returns>
-        public IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files)
+        public static IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<AudioBookFileInfo> files)
         {
             var groupedDirectoryFiles = files.GroupBy(file => Path.GetDirectoryName(file.Path));
 
@@ -60,19 +50,13 @@ namespace Emby.Naming.Video
                 {
                     foreach (var file in directory)
                     {
-                        var stack = new FileStack { Name = Path.GetFileNameWithoutExtension(file.Path), IsDirectoryStack = false };
-                        stack.Files.Add(file.Path);
+                        var stack = new FileStack(Path.GetFileNameWithoutExtension(file.Path), false, new[] { file.Path });
                         yield return stack;
                     }
                 }
                 else
                 {
-                    var stack = new FileStack { Name = Path.GetFileName(directory.Key), IsDirectoryStack = false };
-                    foreach (var file in directory)
-                    {
-                        stack.Files.Add(file.Path);
-                    }
-
+                    var stack = new FileStack(Path.GetFileName(directory.Key), false, directory.Select(f => f.Path).ToArray());
                     yield return stack;
                 }
             }
@@ -82,158 +66,91 @@ namespace Emby.Naming.Video
         /// Resolves videos from paths.
         /// </summary>
         /// <param name="files">List of paths.</param>
+        /// <param name="namingOptions">The naming options.</param>
         /// <returns>Enumerable <see cref="FileStack"/> of videos.</returns>
-        public IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files)
+        public static IEnumerable<FileStack> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions)
         {
-            var list = files
-                .Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, _options) || VideoResolver.IsStubFile(i.FullName, _options))
-                .OrderBy(i => i.FullName)
-                .ToList();
-
-            var expressions = _options.VideoFileStackingRegexes;
+            var potentialFiles = files
+                .Where(i => i.IsDirectory || VideoResolver.IsVideoFile(i.FullName, namingOptions) || VideoResolver.IsStubFile(i.FullName, namingOptions))
+                .OrderBy(i => i.FullName);
 
-            for (var i = 0; i < list.Count; i++)
+            var potentialStacks = new Dictionary<string, StackMetadata>();
+            foreach (var file in potentialFiles)
             {
-                var offset = 0;
-
-                var file1 = list[i];
+                var name = file.Name;
+                if (string.IsNullOrEmpty(name))
+                {
+                    name = Path.GetFileName(file.FullName);
+                }
 
-                var expressionIndex = 0;
-                while (expressionIndex < expressions.Length)
+                for (var i = 0; i < namingOptions.VideoFileStackingRules.Length; i++)
                 {
-                    var exp = expressions[expressionIndex];
-                    var stack = new FileStack();
+                    var rule = namingOptions.VideoFileStackingRules[i];
+                    if (!rule.Match(name, out var stackParsingResult))
+                    {
+                        continue;
+                    }
 
-                    // (Title)(Volume)(Ignore)(Extension)
-                    var match1 = FindMatch(file1, exp, offset);
+                    var stackName = stackParsingResult.Value.StackName;
+                    var partNumber = stackParsingResult.Value.PartNumber;
+                    var partType = stackParsingResult.Value.PartType;
 
-                    if (match1.Success)
+                    if (!potentialStacks.TryGetValue(stackName, out var stackResult))
                     {
-                        var title1 = match1.Groups["title"].Value;
-                        var volume1 = match1.Groups["volume"].Value;
-                        var ignore1 = match1.Groups["ignore"].Value;
-                        var extension1 = match1.Groups["extension"].Value;
+                        stackResult = new StackMetadata(file.IsDirectory, rule.IsNumerical, partType);
+                        potentialStacks[stackName] = stackResult;
+                    }
 
-                        var j = i + 1;
-                        while (j < list.Count)
+                    if (stackResult.Parts.Count > 0)
+                    {
+                        if (stackResult.IsDirectory != file.IsDirectory
+                            || !string.Equals(partType, stackResult.PartType, StringComparison.OrdinalIgnoreCase)
+                            || stackResult.ContainsPart(partNumber))
                         {
-                            var file2 = list[j];
-
-                            if (file1.IsDirectory != file2.IsDirectory)
-                            {
-                                j++;
-                                continue;
-                            }
-
-                            // (Title)(Volume)(Ignore)(Extension)
-                            var match2 = FindMatch(file2, exp, offset);
-
-                            if (match2.Success)
-                            {
-                                var title2 = match2.Groups[1].Value;
-                                var volume2 = match2.Groups[2].Value;
-                                var ignore2 = match2.Groups[3].Value;
-                                var extension2 = match2.Groups[4].Value;
-
-                                if (string.Equals(title1, title2, StringComparison.OrdinalIgnoreCase))
-                                {
-                                    if (!string.Equals(volume1, volume2, StringComparison.OrdinalIgnoreCase))
-                                    {
-                                        if (string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase)
-                                            && string.Equals(extension1, extension2, StringComparison.OrdinalIgnoreCase))
-                                        {
-                                            if (stack.Files.Count == 0)
-                                            {
-                                                stack.Name = title1 + ignore1;
-                                                stack.IsDirectoryStack = file1.IsDirectory;
-                                                stack.Files.Add(file1.FullName);
-                                            }
-
-                                            stack.Files.Add(file2.FullName);
-                                        }
-                                        else
-                                        {
-                                            // Sequel
-                                            offset = 0;
-                                            expressionIndex++;
-                                            break;
-                                        }
-                                    }
-                                    else if (!string.Equals(ignore1, ignore2, StringComparison.OrdinalIgnoreCase))
-                                    {
-                                        // False positive, try again with offset
-                                        offset = match1.Groups[3].Index;
-                                        break;
-                                    }
-                                    else
-                                    {
-                                        // Extension mismatch
-                                        offset = 0;
-                                        expressionIndex++;
-                                        break;
-                                    }
-                                }
-                                else
-                                {
-                                    // Title mismatch
-                                    offset = 0;
-                                    expressionIndex++;
-                                    break;
-                                }
-                            }
-                            else
-                            {
-                                // No match 2, next expression
-                                offset = 0;
-                                expressionIndex++;
-                                break;
-                            }
-
-                            j++;
+                            continue;
                         }
 
-                        if (j == list.Count)
+                        if (rule.IsNumerical != stackResult.IsNumerical)
                         {
-                            expressionIndex = expressions.Length;
+                            break;
                         }
                     }
-                    else
-                    {
-                        // No match 1
-                        offset = 0;
-                        expressionIndex++;
-                    }
 
-                    if (stack.Files.Count > 1)
-                    {
-                        yield return stack;
-                        i += stack.Files.Count - 1;
-                        break;
-                    }
+                    stackResult.Parts.Add(partNumber, file);
+                    break;
                 }
             }
-        }
 
-        private static string GetRegexInput(FileSystemMetadata file)
-        {
-            // For directories, dummy up an extension otherwise the expressions will fail
-            var input = !file.IsDirectory
-                ? file.FullName
-                : file.FullName + ".mkv";
+            foreach (var (fileName, stack) in potentialStacks)
+            {
+                if (stack.Parts.Count < 2)
+                {
+                    continue;
+                }
 
-            return Path.GetFileName(input);
+                yield return new FileStack(fileName, stack.IsDirectory, stack.Parts.Select(kv => kv.Value.FullName).ToArray());
+            }
         }
 
-        private static Match FindMatch(FileSystemMetadata input, Regex regex, int offset)
+        private class StackMetadata
         {
-            var regexInput = GetRegexInput(input);
-
-            if (offset < 0 || offset >= regexInput.Length)
+            public StackMetadata(bool isDirectory, bool isNumerical, string partType)
             {
-                return Match.Empty;
+                Parts = new Dictionary<string, FileSystemMetadata>(StringComparer.OrdinalIgnoreCase);
+                IsDirectory = isDirectory;
+                IsNumerical = isNumerical;
+                PartType = partType;
             }
 
-            return regex.Match(regexInput, offset);
+            public Dictionary<string, FileSystemMetadata> Parts { get; }
+
+            public bool IsDirectory { get; }
+
+            public bool IsNumerical { get; }
+
+            public string PartType { get; }
+
+            public bool ContainsPart(string partNumber) => Parts.ContainsKey(partNumber);
         }
     }
 }

+ 2 - 2
Emby.Naming/Video/StubResolver.cs

@@ -1,7 +1,7 @@
 using System;
 using System.IO;
-using System.Linq;
 using Emby.Naming.Common;
+using Jellyfin.Extensions;
 
 namespace Emby.Naming.Video
 {
@@ -28,7 +28,7 @@ namespace Emby.Naming.Video
 
             var extension = Path.GetExtension(path);
 
-            if (!options.StubFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+            if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
             {
                 return false;
             }

+ 6 - 7
Emby.Naming/Video/VideoInfo.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
 
 namespace Emby.Naming.Video
 {
@@ -17,7 +18,6 @@ namespace Emby.Naming.Video
             Name = name;
 
             Files = Array.Empty<VideoFileInfo>();
-            Extras = Array.Empty<VideoFileInfo>();
             AlternateVersions = Array.Empty<VideoFileInfo>();
         }
 
@@ -39,16 +39,15 @@ namespace Emby.Naming.Video
         /// <value>The files.</value>
         public IReadOnlyList<VideoFileInfo> Files { get; set; }
 
-        /// <summary>
-        /// Gets or sets the extras.
-        /// </summary>
-        /// <value>The extras.</value>
-        public IReadOnlyList<VideoFileInfo> Extras { get; set; }
-
         /// <summary>
         /// Gets or sets the alternate versions.
         /// </summary>
         /// <value>The alternate versions.</value>
         public IReadOnlyList<VideoFileInfo> AlternateVersions { get; set; }
+
+        /// <summary>
+        /// Gets or sets the extra type.
+        /// </summary>
+        public ExtraType? ExtraType { get; set; }
     }
 }

+ 30 - 144
Emby.Naming/Video/VideoListResolver.cs

@@ -4,7 +4,6 @@ using System.IO;
 using System.Linq;
 using System.Text.RegularExpressions;
 using Emby.Naming.Common;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 
 namespace Emby.Naming.Video
@@ -17,29 +16,38 @@ namespace Emby.Naming.Video
         /// <summary>
         /// Resolves alternative versions and extras from list of video files.
         /// </summary>
-        /// <param name="files">List of related video files.</param>
+        /// <param name="videoInfos">List of related video files.</param>
         /// <param name="namingOptions">The naming options.</param>
         /// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
+        /// <param name="parseName">Whether to parse the name or use the filename.</param>
         /// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
-        public static IEnumerable<VideoInfo> Resolve(IEnumerable<FileSystemMetadata> files, NamingOptions namingOptions, bool supportMultiVersion = true)
+        public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true)
         {
-            var videoInfos = files
-                .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, namingOptions))
-                .OfType<VideoFileInfo>()
-                .ToList();
-
             // Filter out all extras, otherwise they could cause stacks to not be resolved
             // See the unit test TestStackedWithTrailer
             var nonExtras = videoInfos
                 .Where(i => i.ExtraType == null)
                 .Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory });
 
-            var stackResult = new StackResolver(namingOptions)
-                .Resolve(nonExtras).ToList();
+            var stackResult = StackResolver.Resolve(nonExtras, namingOptions).ToList();
 
-            var remainingFiles = videoInfos
-                .Where(i => !stackResult.Any(s => i.Path != null && s.ContainsFile(i.Path, i.IsDirectory)))
-                .ToList();
+            var remainingFiles = new List<VideoFileInfo>();
+            var standaloneMedia = new List<VideoFileInfo>();
+
+            for (var i = 0; i < videoInfos.Count; i++)
+            {
+                var current = videoInfos[i];
+                if (stackResult.Any(s => s.ContainsFile(current.Path, current.IsDirectory)))
+                {
+                    continue;
+                }
+
+                remainingFiles.Add(current);
+                if (current.ExtraType == null)
+                {
+                    standaloneMedia.Add(current);
+                }
+            }
 
             var list = new List<VideoInfo>();
 
@@ -47,27 +55,15 @@ namespace Emby.Naming.Video
             {
                 var info = new VideoInfo(stack.Name)
                 {
-                    Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions))
+                    Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName))
                         .OfType<VideoFileInfo>()
                         .ToList()
                 };
 
                 info.Year = info.Files[0].Year;
-
-                var extras = ExtractExtras(remainingFiles, stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0].AsSpan()), namingOptions.VideoFlagDelimiters);
-
-                if (extras.Count > 0)
-                {
-                    info.Extras = extras;
-                }
-
                 list.Add(info);
             }
 
-            var standaloneMedia = remainingFiles
-                .Where(i => i.ExtraType == null)
-                .ToList();
-
             foreach (var media in standaloneMedia)
             {
                 var info = new VideoInfo(media.Name) { Files = new[] { media } };
@@ -75,10 +71,6 @@ namespace Emby.Naming.Video
                 info.Year = info.Files[0].Year;
 
                 remainingFiles.Remove(media);
-                var extras = ExtractExtras(remainingFiles, media.FileNameWithoutExtension, namingOptions.VideoFlagDelimiters);
-
-                info.Extras = extras;
-
                 list.Add(info);
             }
 
@@ -87,58 +79,12 @@ namespace Emby.Naming.Video
                 list = GetVideosGroupedByVersion(list, namingOptions);
             }
 
-            // If there's only one resolved video, use the folder name as well to find extras
-            if (list.Count == 1)
-            {
-                var info = list[0];
-                var videoPath = list[0].Files[0].Path;
-                var parentPath = Path.GetDirectoryName(videoPath.AsSpan());
-
-                if (!parentPath.IsEmpty)
-                {
-                    var folderName = Path.GetFileName(parentPath);
-                    if (!folderName.IsEmpty)
-                    {
-                        var extras = ExtractExtras(remainingFiles, folderName, namingOptions.VideoFlagDelimiters);
-                        extras.AddRange(info.Extras);
-                        info.Extras = extras;
-                    }
-                }
-
-                // Add the extras that are just based on file name as well
-                var extrasByFileName = remainingFiles
-                    .Where(i => i.ExtraRule != null && i.ExtraRule.RuleType == ExtraRuleType.Filename)
-                    .ToList();
-
-                remainingFiles = remainingFiles
-                    .Except(extrasByFileName)
-                    .ToList();
-
-                extrasByFileName.AddRange(info.Extras);
-                info.Extras = extrasByFileName;
-            }
-
-            // If there's only one video, accept all trailers
-            // Be lenient because people use all kinds of mishmash conventions with trailers.
-            if (list.Count == 1)
-            {
-                var trailers = remainingFiles
-                    .Where(i => i.ExtraType == ExtraType.Trailer)
-                    .ToList();
-
-                trailers.AddRange(list[0].Extras);
-                list[0].Extras = trailers;
-
-                remainingFiles = remainingFiles
-                    .Except(trailers)
-                    .ToList();
-            }
-
             // Whatever files are left, just add them
             list.AddRange(remainingFiles.Select(i => new VideoInfo(i.Name)
             {
                 Files = new[] { i },
-                Year = i.Year
+                Year = i.Year,
+                ExtraType = i.ExtraType
             }));
 
             return list;
@@ -162,6 +108,11 @@ namespace Emby.Naming.Video
             for (var i = 0; i < videos.Count; i++)
             {
                 var video = videos[i];
+                if (video.ExtraType != null)
+                {
+                    continue;
+                }
+
                 if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
                 {
                     return videos;
@@ -178,17 +129,14 @@ namespace Emby.Naming.Video
 
             var alternateVersionsLen = videos.Count - 1;
             var alternateVersions = new VideoFileInfo[alternateVersionsLen];
-            var extras = new List<VideoFileInfo>(list[0].Extras);
             for (int i = 0; i < alternateVersionsLen; i++)
             {
                 var video = videos[i + 1];
                 alternateVersions[i] = video.Files[0];
-                extras.AddRange(video.Extras);
             }
 
             list[0].AlternateVersions = alternateVersions;
             list[0].Name = folderName.ToString();
-            list[0].Extras = extras;
 
             return list;
         }
@@ -230,7 +178,7 @@ namespace Emby.Naming.Video
             var tmpTestFilename = testFilename.ToString();
             if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
             {
-                tmpTestFilename = cleanName.Trim().ToString();
+                tmpTestFilename = cleanName.Trim();
             }
 
             // The CleanStringParser should have removed common keywords etc.
@@ -238,67 +186,5 @@ namespace Emby.Naming.Video
                    || testFilename[0] == '-'
                    || Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
         }
-
-        private static ReadOnlySpan<char> TrimFilenameDelimiters(ReadOnlySpan<char> name, ReadOnlySpan<char> videoFlagDelimiters)
-        {
-            return name.IsEmpty ? name : name.TrimEnd().TrimEnd(videoFlagDelimiters).TrimEnd();
-        }
-
-        private static bool StartsWith(ReadOnlySpan<char> fileName, ReadOnlySpan<char> baseName, ReadOnlySpan<char> trimmedBaseName)
-        {
-            if (baseName.IsEmpty)
-            {
-                return false;
-            }
-
-            return fileName.StartsWith(baseName, StringComparison.OrdinalIgnoreCase)
-                   || (!trimmedBaseName.IsEmpty && fileName.StartsWith(trimmedBaseName, StringComparison.OrdinalIgnoreCase));
-        }
-
-        /// <summary>
-        /// Finds similar filenames to that of [baseName] and removes any matches from [remainingFiles].
-        /// </summary>
-        /// <param name="remainingFiles">The list of remaining filenames.</param>
-        /// <param name="baseName">The base name to use for the comparison.</param>
-        /// <param name="videoFlagDelimiters">The video flag delimiters.</param>
-        /// <returns>A list of video extras for [baseName].</returns>
-        private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> baseName, ReadOnlySpan<char> videoFlagDelimiters)
-        {
-            return ExtractExtras(remainingFiles, baseName, ReadOnlySpan<char>.Empty, videoFlagDelimiters);
-        }
-
-        /// <summary>
-        /// Finds similar filenames to that of [firstBaseName] and [secondBaseName] and removes any matches from [remainingFiles].
-        /// </summary>
-        /// <param name="remainingFiles">The list of remaining filenames.</param>
-        /// <param name="firstBaseName">The first base name to use for the comparison.</param>
-        /// <param name="secondBaseName">The second base name to use for the comparison.</param>
-        /// <param name="videoFlagDelimiters">The video flag delimiters.</param>
-        /// <returns>A list of video extras for [firstBaseName] and [secondBaseName].</returns>
-        private static List<VideoFileInfo> ExtractExtras(IList<VideoFileInfo> remainingFiles, ReadOnlySpan<char> firstBaseName, ReadOnlySpan<char> secondBaseName, ReadOnlySpan<char> videoFlagDelimiters)
-        {
-            var trimmedFirstBaseName = TrimFilenameDelimiters(firstBaseName, videoFlagDelimiters);
-            var trimmedSecondBaseName = TrimFilenameDelimiters(secondBaseName, videoFlagDelimiters);
-
-            var result = new List<VideoFileInfo>();
-            for (var pos = remainingFiles.Count - 1; pos >= 0; pos--)
-            {
-                var file = remainingFiles[pos];
-                if (file.ExtraType == null)
-                {
-                    continue;
-                }
-
-                var filename = file.FileNameWithoutExtension;
-                if (StartsWith(filename, firstBaseName, trimmedFirstBaseName)
-                    || StartsWith(filename, secondBaseName, trimmedSecondBaseName))
-                {
-                    result.Add(file);
-                    remainingFiles.RemoveAt(pos);
-                }
-            }
-
-            return result;
-        }
     }
 }

+ 4 - 3
Emby.Naming/Video/VideoResolver.cs

@@ -16,10 +16,11 @@ namespace Emby.Naming.Video
         /// </summary>
         /// <param name="path">The path.</param>
         /// <param name="namingOptions">The naming options.</param>
+        /// <param name="parseName">Whether to parse the name or use the filename.</param>
         /// <returns>VideoFileInfo.</returns>
-        public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions)
+        public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true)
         {
-            return Resolve(path, true, namingOptions);
+            return Resolve(path, true, namingOptions, parseName);
         }
 
         /// <summary>
@@ -74,7 +75,7 @@ namespace Emby.Naming.Video
 
             var format3DResult = Format3DParser.Parse(path, namingOptions);
 
-            var extraResult = new ExtraResolver(namingOptions).GetExtraInfo(path);
+            var extraResult = ExtraResolver.GetExtraInfo(path, namingOptions);
 
             var name = Path.GetFileNameWithoutExtension(path);
 

+ 2 - 1
Emby.Notifications/NotificationEntryPoint.cs

@@ -5,6 +5,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Events;
+using Jellyfin.Extensions;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Entities;
@@ -104,7 +105,7 @@ namespace Emby.Notifications
 
             var type = entry.Type;
 
-            if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparer.OrdinalIgnoreCase))
+            if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparison.OrdinalIgnoreCase))
             {
                 return;
             }

+ 2 - 1
Emby.Photos/PhotoProvider.cs

@@ -3,6 +3,7 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -60,7 +61,7 @@ namespace Emby.Photos
             item.SetImagePath(ImageType.Primary, item.Path);
 
             // Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
-            if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparer.OrdinalIgnoreCase))
+            if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase))
             {
                 try
                 {

+ 1 - 1
Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs

@@ -301,7 +301,7 @@ namespace Emby.Server.Implementations.AppBase
         {
             return _configurations.GetOrAdd(
                 key,
-                (k, configurationManager) =>
+                static (k, configurationManager) =>
                 {
                     var file = configurationManager.GetConfigurationFile(k);
 

+ 0 - 1
Emby.Server.Implementations/AppBase/ConfigurationHelper.cs

@@ -1,6 +1,5 @@
 using System;
 using System.IO;
-using System.Linq;
 using MediaBrowser.Model.Serialization;
 
 namespace Emby.Server.Implementations.AppBase

+ 0 - 116
Emby.Server.Implementations/Archiving/ZipClient.cs

@@ -1,11 +1,8 @@
 using System.IO;
 using MediaBrowser.Model.IO;
-using SharpCompress.Archives.SevenZip;
-using SharpCompress.Archives.Tar;
 using SharpCompress.Common;
 using SharpCompress.Readers;
 using SharpCompress.Readers.GZip;
-using SharpCompress.Readers.Zip;
 
 namespace Emby.Server.Implementations.Archiving
 {
@@ -14,55 +11,6 @@ namespace Emby.Server.Implementations.Archiving
     /// </summary>
     public class ZipClient : IZipClient
     {
-        /// <summary>
-        /// Extracts all.
-        /// </summary>
-        /// <param name="sourceFile">The source file.</param>
-        /// <param name="targetPath">The target path.</param>
-        /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
-        public void ExtractAll(string sourceFile, string targetPath, bool overwriteExistingFiles)
-        {
-            using var fileStream = File.OpenRead(sourceFile);
-            ExtractAll(fileStream, targetPath, overwriteExistingFiles);
-        }
-
-        /// <summary>
-        /// Extracts all.
-        /// </summary>
-        /// <param name="source">The source.</param>
-        /// <param name="targetPath">The target path.</param>
-        /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
-        public void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles)
-        {
-            using var reader = ReaderFactory.Open(source);
-            var options = new ExtractionOptions
-            {
-                ExtractFullPath = true
-            };
-
-            if (overwriteExistingFiles)
-            {
-                options.Overwrite = true;
-            }
-
-            Directory.CreateDirectory(targetPath);
-            reader.WriteAllToDirectory(targetPath, options);
-        }
-
-        /// <inheritdoc />
-        public void ExtractAllFromZip(Stream source, string targetPath, bool overwriteExistingFiles)
-        {
-            using var reader = ZipReader.Open(source);
-            var options = new ExtractionOptions
-            {
-                ExtractFullPath = true,
-                Overwrite = overwriteExistingFiles
-            };
-
-            Directory.CreateDirectory(targetPath);
-            reader.WriteAllToDirectory(targetPath, options);
-        }
-
         /// <inheritdoc />
         public void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles)
         {
@@ -94,69 +42,5 @@ namespace Emby.Server.Implementations.Archiving
                 reader.WriteEntryToFile(Path.Combine(targetPath, filename));
             }
         }
-
-        /// <summary>
-        /// Extracts all from7z.
-        /// </summary>
-        /// <param name="sourceFile">The source file.</param>
-        /// <param name="targetPath">The target path.</param>
-        /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
-        public void ExtractAllFrom7z(string sourceFile, string targetPath, bool overwriteExistingFiles)
-        {
-            using var fileStream = File.OpenRead(sourceFile);
-            ExtractAllFrom7z(fileStream, targetPath, overwriteExistingFiles);
-        }
-
-        /// <summary>
-        /// Extracts all from7z.
-        /// </summary>
-        /// <param name="source">The source.</param>
-        /// <param name="targetPath">The target path.</param>
-        /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
-        public void ExtractAllFrom7z(Stream source, string targetPath, bool overwriteExistingFiles)
-        {
-            using var archive = SevenZipArchive.Open(source);
-            using var reader = archive.ExtractAllEntries();
-            var options = new ExtractionOptions
-            {
-                ExtractFullPath = true,
-                Overwrite = overwriteExistingFiles
-            };
-
-            Directory.CreateDirectory(targetPath);
-            reader.WriteAllToDirectory(targetPath, options);
-        }
-
-        /// <summary>
-        /// Extracts all from tar.
-        /// </summary>
-        /// <param name="sourceFile">The source file.</param>
-        /// <param name="targetPath">The target path.</param>
-        /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
-        public void ExtractAllFromTar(string sourceFile, string targetPath, bool overwriteExistingFiles)
-        {
-            using var fileStream = File.OpenRead(sourceFile);
-            ExtractAllFromTar(fileStream, targetPath, overwriteExistingFiles);
-        }
-
-        /// <summary>
-        /// Extracts all from tar.
-        /// </summary>
-        /// <param name="source">The source.</param>
-        /// <param name="targetPath">The target path.</param>
-        /// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
-        public void ExtractAllFromTar(Stream source, string targetPath, bool overwriteExistingFiles)
-        {
-            using var archive = TarArchive.Open(source);
-            using var reader = archive.ExtractAllEntries();
-            var options = new ExtractionOptions
-            {
-                ExtractFullPath = true,
-                Overwrite = overwriteExistingFiles
-            };
-
-            Directory.CreateDirectory(targetPath);
-            reader.WriteAllToDirectory(targetPath, options);
-        }
     }
 }

+ 5 - 4
Emby.Server.Implementations/Channels/ChannelManager.cs

@@ -10,6 +10,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using Jellyfin.Extensions.Json;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Progress;
@@ -179,7 +180,7 @@ namespace Emby.Server.Implementations.Channels
                     try
                     {
                         return (GetChannelProvider(i) is IHasFolderAttributes hasAttributes
-                            && hasAttributes.Attributes.Contains("Recordings", StringComparer.OrdinalIgnoreCase)) == val;
+                            && hasAttributes.Attributes.Contains("Recordings", StringComparison.OrdinalIgnoreCase)) == val;
                     }
                     catch
                     {
@@ -541,7 +542,7 @@ namespace Emby.Server.Implementations.Channels
             return _libraryManager.GetItemIds(
                 new InternalItemsQuery
                 {
-                    IncludeItemTypes = new[] { nameof(Channel) },
+                    IncludeItemTypes = new[] { BaseItemKind.Channel },
                     OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
                 }).Select(i => GetChannelFeatures(i)).ToArray();
         }
@@ -1135,7 +1136,7 @@ namespace Emby.Server.Implementations.Channels
 
             if (!info.IsLiveStream)
             {
-                if (item.Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase))
+                if (item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
                 {
                     item.Tags = item.Tags.Except(new[] { "livestream" }, StringComparer.OrdinalIgnoreCase).ToArray();
                     _logger.LogDebug("Forcing update due to Tags {0}", item.Name);
@@ -1144,7 +1145,7 @@ namespace Emby.Server.Implementations.Channels
             }
             else
             {
-                if (!item.Tags.Contains("livestream", StringComparer.OrdinalIgnoreCase))
+                if (!item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
                 {
                     item.Tags = item.Tags.Concat(new[] { "livestream" }).ToArray();
                     _logger.LogDebug("Forcing update due to Tags {0}", item.Name);

+ 2 - 1
Emby.Server.Implementations/Channels/ChannelPostScanTask.cs

@@ -2,6 +2,7 @@ using System;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -51,7 +52,7 @@ namespace Emby.Server.Implementations.Channels
 
             var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new[] { nameof(Channel) },
+                IncludeItemTypes = new[] { BaseItemKind.Channel },
                 ExcludeItemIds = installedChannelIds.ToArray()
             });
 

+ 2 - 2
Emby.Server.Implementations/Data/BaseSqliteRepository.cs

@@ -4,8 +4,8 @@
 
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using System.Threading;
+using Jellyfin.Extensions;
 using Microsoft.Extensions.Logging;
 using SQLitePCL.pretty;
 
@@ -194,7 +194,7 @@ namespace Emby.Server.Implementations.Data
 
         protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
         {
-            if (existingColumnNames.Contains(columnName, StringComparer.OrdinalIgnoreCase))
+            if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
             {
                 return;
             }

+ 160 - 90
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.Data
 
         private readonly ItemFields[] _allItemFields = Enum.GetValues<ItemFields>();
 
-        private static readonly string[] _retriveItemColumns =
+        private static readonly string[] _retrieveItemColumns =
         {
             "type",
             "data",
@@ -133,7 +133,7 @@ namespace Emby.Server.Implementations.Data
             "OwnerId"
         };
 
-        private static readonly string _retriveItemColumnsSelectQuery = $"select {string.Join(',', _retriveItemColumns)} from TypedBaseItems where guid = @guid";
+        private static readonly string _retrieveItemColumnsSelectQuery = $"select {string.Join(',', _retrieveItemColumns)} from TypedBaseItems where guid = @guid";
 
         private static readonly string[] _mediaStreamSaveColumns =
         {
@@ -196,57 +196,56 @@ namespace Emby.Server.Implementations.Data
 
         private static readonly string _mediaAttachmentInsertPrefix;
 
-        private static readonly HashSet<string> _programTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+        private static readonly BaseItemKind[] _programTypes = new[]
         {
-            "Program",
-            "TvChannel",
-            "LiveTvProgram",
-            "LiveTvTvChannel"
+            BaseItemKind.Program,
+            BaseItemKind.TvChannel,
+            BaseItemKind.LiveTvProgram,
+            BaseItemKind.LiveTvChannel
         };
 
-        private static readonly HashSet<string> _programExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+        private static readonly BaseItemKind[] _programExcludeParentTypes = new[]
         {
-            "Series",
-            "Season",
-            "MusicAlbum",
-            "MusicArtist",
-            "PhotoAlbum"
+            BaseItemKind.Series,
+            BaseItemKind.Season,
+            BaseItemKind.MusicAlbum,
+            BaseItemKind.MusicArtist,
+            BaseItemKind.PhotoAlbum
         };
 
-        private static readonly HashSet<string> _serviceTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+        private static readonly BaseItemKind[] _serviceTypes = new[]
         {
-            "TvChannel",
-            "LiveTvTvChannel"
+            BaseItemKind.TvChannel,
+            BaseItemKind.LiveTvChannel
         };
 
-        private static readonly HashSet<string> _startDateTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+        private static readonly BaseItemKind[] _startDateTypes = new[]
         {
-            "Program",
-            "LiveTvProgram"
+            BaseItemKind.Program,
+            BaseItemKind.LiveTvProgram
         };
 
-        private static readonly HashSet<string> _seriesTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+        private static readonly BaseItemKind[] _seriesTypes = new[]
         {
-            "Book",
-            "AudioBook",
-            "Episode",
-            "Season"
+            BaseItemKind.Book,
+            BaseItemKind.AudioBook,
+            BaseItemKind.Episode,
+            BaseItemKind.Season
         };
 
-        private static readonly HashSet<string> _artistExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+        private static readonly BaseItemKind[] _artistExcludeParentTypes = new[]
         {
-            "Series",
-            "Season",
-            "PhotoAlbum"
+            BaseItemKind.Series,
+            BaseItemKind.Season,
+            BaseItemKind.PhotoAlbum
         };
 
-        private static readonly HashSet<string> _artistsTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+        private static readonly BaseItemKind[] _artistsTypes = new[]
         {
-            "Audio",
-            "MusicAlbum",
-            "MusicVideo",
-            "AudioBook",
-            "AudioPodcast"
+            BaseItemKind.Audio,
+            BaseItemKind.MusicAlbum,
+            BaseItemKind.MusicVideo,
+            BaseItemKind.AudioBook
         };
 
         private static readonly Type[] _knownTypes =
@@ -285,6 +284,43 @@ namespace Emby.Server.Implementations.Data
 
         private readonly Dictionary<string, string> _types = GetTypeMapDictionary();
 
+        private static readonly Dictionary<BaseItemKind, string> _baseItemKindNames = new()
+        {
+            { BaseItemKind.AggregateFolder, typeof(AggregateFolder).FullName },
+            { BaseItemKind.Audio, typeof(Audio).FullName },
+            { BaseItemKind.AudioBook, typeof(AudioBook).FullName },
+            { BaseItemKind.BasePluginFolder, typeof(BasePluginFolder).FullName },
+            { BaseItemKind.Book, typeof(Book).FullName },
+            { BaseItemKind.BoxSet, typeof(BoxSet).FullName },
+            { BaseItemKind.Channel, typeof(Channel).FullName },
+            { BaseItemKind.CollectionFolder, typeof(CollectionFolder).FullName },
+            { BaseItemKind.Episode, typeof(Episode).FullName },
+            { BaseItemKind.Folder, typeof(Folder).FullName },
+            { BaseItemKind.Genre, typeof(Genre).FullName },
+            { BaseItemKind.Movie, typeof(Movie).FullName },
+            { BaseItemKind.LiveTvChannel, typeof(LiveTvChannel).FullName },
+            { BaseItemKind.LiveTvProgram, typeof(LiveTvProgram).FullName },
+            { BaseItemKind.MusicAlbum, typeof(MusicAlbum).FullName },
+            { BaseItemKind.MusicArtist, typeof(MusicArtist).FullName },
+            { BaseItemKind.MusicGenre, typeof(MusicGenre).FullName },
+            { BaseItemKind.MusicVideo, typeof(MusicVideo).FullName },
+            { BaseItemKind.Person, typeof(Person).FullName },
+            { BaseItemKind.Photo, typeof(Photo).FullName },
+            { BaseItemKind.PhotoAlbum, typeof(PhotoAlbum).FullName },
+            { BaseItemKind.Playlist, typeof(Playlist).FullName },
+            { BaseItemKind.PlaylistsFolder, typeof(PlaylistsFolder).FullName },
+            { BaseItemKind.Season, typeof(Season).FullName },
+            { BaseItemKind.Series, typeof(Series).FullName },
+            { BaseItemKind.Studio, typeof(Studio).FullName },
+            { BaseItemKind.Trailer, typeof(Trailer).FullName },
+            { BaseItemKind.TvChannel, typeof(LiveTvChannel).FullName },
+            { BaseItemKind.TvProgram, typeof(LiveTvProgram).FullName },
+            { BaseItemKind.UserRootFolder, typeof(UserRootFolder).FullName },
+            { BaseItemKind.UserView, typeof(UserView).FullName },
+            { BaseItemKind.Video, typeof(Video).FullName },
+            { BaseItemKind.Year, typeof(Year).FullName }
+        };
+
         static SqliteItemRepository()
         {
             var queryPrefixText = new StringBuilder();
@@ -1320,7 +1356,7 @@ namespace Emby.Server.Implementations.Data
 
             using (var connection = GetConnection(true))
             {
-                using (var statement = PrepareStatement(connection, _retriveItemColumnsSelectQuery))
+                using (var statement = PrepareStatement(connection, _retrieveItemColumnsSelectQuery))
                 {
                     statement.TryBind("@guid", id);
 
@@ -2212,7 +2248,7 @@ namespace Emby.Server.Implementations.Data
 
         private bool HasProgramAttributes(InternalItemsQuery query)
         {
-            if (_programExcludeParentTypes.Contains(query.ParentType))
+            if (query.ParentType != null && _programExcludeParentTypes.Contains(query.ParentType.Value))
             {
                 return false;
             }
@@ -2227,7 +2263,7 @@ namespace Emby.Server.Implementations.Data
 
         private bool HasServiceName(InternalItemsQuery query)
         {
-            if (_programExcludeParentTypes.Contains(query.ParentType))
+            if (query.ParentType != null && _programExcludeParentTypes.Contains(query.ParentType.Value))
             {
                 return false;
             }
@@ -2242,7 +2278,7 @@ namespace Emby.Server.Implementations.Data
 
         private bool HasStartDate(InternalItemsQuery query)
         {
-            if (_programExcludeParentTypes.Contains(query.ParentType))
+            if (query.ParentType != null && _programExcludeParentTypes.Contains(query.ParentType.Value))
             {
                 return false;
             }
@@ -2262,7 +2298,7 @@ namespace Emby.Server.Implementations.Data
                 return true;
             }
 
-            return query.IncludeItemTypes.Contains("Episode", StringComparer.OrdinalIgnoreCase);
+            return query.IncludeItemTypes.Contains(BaseItemKind.Episode);
         }
 
         private bool HasTrailerTypes(InternalItemsQuery query)
@@ -2272,12 +2308,12 @@ namespace Emby.Server.Implementations.Data
                 return true;
             }
 
-            return query.IncludeItemTypes.Contains("Trailer", StringComparer.OrdinalIgnoreCase);
+            return query.IncludeItemTypes.Contains(BaseItemKind.Trailer);
         }
 
         private bool HasArtistFields(InternalItemsQuery query)
         {
-            if (_artistExcludeParentTypes.Contains(query.ParentType))
+            if (query.ParentType != null && _artistExcludeParentTypes.Contains(query.ParentType.Value))
             {
                 return false;
             }
@@ -2292,7 +2328,7 @@ namespace Emby.Server.Implementations.Data
 
         private bool HasSeriesFields(InternalItemsQuery query)
         {
-            if (string.Equals(query.ParentType, "PhotoAlbum", StringComparison.OrdinalIgnoreCase))
+            if (query.ParentType == BaseItemKind.PhotoAlbum)
             {
                 return false;
             }
@@ -2630,7 +2666,7 @@ namespace Emby.Server.Implementations.Data
                 query.Limit = query.Limit.Value + 4;
             }
 
-            var columns = _retriveItemColumns.ToList();
+            var columns = _retrieveItemColumns.ToList();
             SetFinalColumnsToSelect(query, columns);
             var commandTextBuilder = new StringBuilder("select ", 1024)
                 .AppendJoin(',', columns)
@@ -2821,7 +2857,7 @@ namespace Emby.Server.Implementations.Data
                 query.Limit = query.Limit.Value + 4;
             }
 
-            var columns = _retriveItemColumns.ToList();
+            var columns = _retrieveItemColumns.ToList();
             SetFinalColumnsToSelect(query, columns);
             var commandTextBuilder = new StringBuilder("select ", 512)
                 .AppendJoin(',', columns)
@@ -3487,8 +3523,8 @@ namespace Emby.Server.Implementations.Data
             if (query.IsMovie == true)
             {
                 if (query.IncludeItemTypes.Length == 0
-                    || query.IncludeItemTypes.Contains(nameof(Movie))
-                    || query.IncludeItemTypes.Contains(nameof(Trailer)))
+                    || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
+                    || query.IncludeItemTypes.Contains(BaseItemKind.Trailer))
                 {
                     whereClauses.Add("(IsMovie is null OR IsMovie=@IsMovie)");
                 }
@@ -3563,31 +3599,81 @@ namespace Emby.Server.Implementations.Data
                 statement?.TryBind("@IsFolder", query.IsFolder);
             }
 
-            var includeTypes = query.IncludeItemTypes.Select(MapIncludeItemTypes).Where(x => x != null).ToArray();
+            var includeTypes = query.IncludeItemTypes;
             // Only specify excluded types if no included types are specified
-            if (includeTypes.Length == 0)
+            if (query.IncludeItemTypes.Length == 0)
             {
-                var excludeTypes = query.ExcludeItemTypes.Select(MapIncludeItemTypes).Where(x => x != null).ToArray();
+                var excludeTypes = query.ExcludeItemTypes;
                 if (excludeTypes.Length == 1)
                 {
-                    whereClauses.Add("type<>@type");
-                    statement?.TryBind("@type", excludeTypes[0]);
+                    if (_baseItemKindNames.TryGetValue(excludeTypes[0], out var excludeTypeName))
+                    {
+                        whereClauses.Add("type<>@type");
+                        statement?.TryBind("@type", excludeTypeName);
+                    }
+                    else
+                    {
+                        Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", excludeTypes[0]);
+                    }
                 }
                 else if (excludeTypes.Length > 1)
                 {
-                    var inClause = string.Join(',', excludeTypes.Select(i => "'" + i + "'"));
-                    whereClauses.Add($"type not in ({inClause})");
+                    var whereBuilder = new StringBuilder("type not in (");
+                    foreach (var excludeType in excludeTypes)
+                    {
+                        if (_baseItemKindNames.TryGetValue(excludeType, out var baseItemKindName))
+                        {
+                            whereBuilder
+                                .Append('\'')
+                                .Append(baseItemKindName)
+                                .Append("',");
+                        }
+                        else
+                        {
+                            Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", excludeType);
+                        }
+                    }
+
+                    // Remove trailing comma.
+                    whereBuilder.Length--;
+                    whereBuilder.Append(')');
+                    whereClauses.Add(whereBuilder.ToString());
                 }
             }
             else if (includeTypes.Length == 1)
             {
-                whereClauses.Add("type=@type");
-                statement?.TryBind("@type", includeTypes[0]);
+                if (_baseItemKindNames.TryGetValue(includeTypes[0], out var includeTypeName))
+                {
+                    whereClauses.Add("type=@type");
+                    statement?.TryBind("@type", includeTypeName);
+                }
+                else
+                {
+                    Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", includeTypes[0]);
+                }
             }
             else if (includeTypes.Length > 1)
             {
-                var inClause = string.Join(',', includeTypes.Select(i => "'" + i + "'"));
-                whereClauses.Add($"type in ({inClause})");
+                var whereBuilder = new StringBuilder("type in (");
+                foreach (var includeType in includeTypes)
+                {
+                    if (_baseItemKindNames.TryGetValue(includeType, out var baseItemKindName))
+                    {
+                        whereBuilder
+                            .Append('\'')
+                            .Append(baseItemKindName)
+                            .Append("',");
+                    }
+                    else
+                    {
+                        Logger.LogWarning("Undefined BaseItemKind to Type mapping: {BaseItemKind}", includeType);
+                    }
+                }
+
+                // Remove trailing comma.
+                whereBuilder.Length--;
+                whereBuilder.Append(')');
+                whereClauses.Add(whereBuilder.ToString());
             }
 
             if (query.ChannelIds.Count == 1)
@@ -3911,7 +3997,7 @@ namespace Emby.Server.Implementations.Data
                 if (query.IsPlayed.HasValue)
                 {
                     // We should probably figure this out for all folders, but for right now, this is the only place where we need it
-                    if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], nameof(Series), StringComparison.OrdinalIgnoreCase))
+                    if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.Series)
                     {
                         if (query.IsPlayed.Value)
                         {
@@ -4761,27 +4847,27 @@ namespace Emby.Server.Implementations.Data
         {
             var list = new List<string>();
 
-            if (IsTypeInQuery(nameof(Person), query))
+            if (IsTypeInQuery(BaseItemKind.Person, query))
             {
                 list.Add(typeof(Person).FullName);
             }
 
-            if (IsTypeInQuery(nameof(Genre), query))
+            if (IsTypeInQuery(BaseItemKind.Genre, query))
             {
                 list.Add(typeof(Genre).FullName);
             }
 
-            if (IsTypeInQuery(nameof(MusicGenre), query))
+            if (IsTypeInQuery(BaseItemKind.MusicGenre, query))
             {
                 list.Add(typeof(MusicGenre).FullName);
             }
 
-            if (IsTypeInQuery(nameof(MusicArtist), query))
+            if (IsTypeInQuery(BaseItemKind.MusicArtist, query))
             {
                 list.Add(typeof(MusicArtist).FullName);
             }
 
-            if (IsTypeInQuery(nameof(Studio), query))
+            if (IsTypeInQuery(BaseItemKind.Studio, query))
             {
                 list.Add(typeof(Studio).FullName);
             }
@@ -4789,14 +4875,14 @@ namespace Emby.Server.Implementations.Data
             return list;
         }
 
-        private bool IsTypeInQuery(string type, InternalItemsQuery query)
+        private bool IsTypeInQuery(BaseItemKind type, InternalItemsQuery query)
         {
-            if (query.ExcludeItemTypes.Contains(type, StringComparer.OrdinalIgnoreCase))
+            if (query.ExcludeItemTypes.Contains(type))
             {
                 return false;
             }
 
-            return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type, StringComparer.OrdinalIgnoreCase);
+            return query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(type);
         }
 
         private string GetCleanValue(string value)
@@ -4836,12 +4922,12 @@ namespace Emby.Server.Implementations.Data
                 return true;
             }
 
-            if (query.IncludeItemTypes.Contains(nameof(Episode), StringComparer.OrdinalIgnoreCase)
-                || query.IncludeItemTypes.Contains(nameof(Video), StringComparer.OrdinalIgnoreCase)
-                || query.IncludeItemTypes.Contains(nameof(Movie), StringComparer.OrdinalIgnoreCase)
-                || query.IncludeItemTypes.Contains(nameof(MusicVideo), StringComparer.OrdinalIgnoreCase)
-                || query.IncludeItemTypes.Contains(nameof(Series), StringComparer.OrdinalIgnoreCase)
-                || query.IncludeItemTypes.Contains(nameof(Season), StringComparer.OrdinalIgnoreCase))
+            if (query.IncludeItemTypes.Contains(BaseItemKind.Episode)
+                || query.IncludeItemTypes.Contains(BaseItemKind.Video)
+                || query.IncludeItemTypes.Contains(BaseItemKind.Movie)
+                || query.IncludeItemTypes.Contains(BaseItemKind.MusicVideo)
+                || query.IncludeItemTypes.Contains(BaseItemKind.Series)
+                || query.IncludeItemTypes.Contains(BaseItemKind.Season))
             {
                 return true;
             }
@@ -4890,22 +4976,6 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
             return dict;
         }
 
-        private string MapIncludeItemTypes(string value)
-        {
-            if (_types.TryGetValue(value, out string result))
-            {
-                return result;
-            }
-
-            if (IsValidType(value))
-            {
-                return value;
-            }
-
-            Logger.LogWarning("Unknown item type: {ItemType}", value);
-            return null;
-        }
-
         public void DeleteItem(Guid id)
         {
             if (id == Guid.Empty)
@@ -5351,7 +5421,7 @@ AND Type = @InternalPersonType)");
                 stringBuilder.Clear();
             }
 
-            List<string> columns = _retriveItemColumns.ToList();
+            List<string> columns = _retrieveItemColumns.ToList();
             // Unfortunately we need to add it to columns to ensure the order of the columns in the select
             if (!string.IsNullOrEmpty(itemCountColumns))
             {
@@ -5569,7 +5639,7 @@ AND Type = @InternalPersonType)");
             return result;
         }
 
-        private static ItemCounts GetItemCounts(IReadOnlyList<ResultSetValue> reader, int countStartColumn, string[] typesToCount)
+        private static ItemCounts GetItemCounts(IReadOnlyList<ResultSetValue> reader, int countStartColumn, BaseItemKind[] typesToCount)
         {
             var counts = new ItemCounts();
 

+ 3 - 3
Emby.Server.Implementations/Dto/DtoService.cs

@@ -7,9 +7,9 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Linq;
-using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Common;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Drawing;
@@ -294,7 +294,7 @@ namespace Emby.Server.Implementations.Dto
                             path = path.TrimStart('.');
                         }
 
-                        if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparer.OrdinalIgnoreCase))
+                        if (!string.IsNullOrEmpty(path) && containers.Contains(path, StringComparison.OrdinalIgnoreCase))
                         {
                             fileExtensionContainer = path;
                         }
@@ -470,7 +470,7 @@ namespace Emby.Server.Implementations.Dto
             {
                 var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery
                 {
-                    IncludeItemTypes = new[] { nameof(MusicAlbum) },
+                    IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
                     Name = item.Album,
                     Limit = 1
                 });

+ 1 - 1
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -29,7 +29,7 @@
     <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.0" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
     <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.1" />
     <PackageReference Include="Mono.Nat" Version="3.0.2" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.2" />
     <PackageReference Include="sharpcompress" Version="0.30.1" />

+ 0 - 1
Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs

@@ -13,7 +13,6 @@ using Jellyfin.Networking.Configuration;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Plugins;
-using MediaBrowser.Model.Dlna;
 using Microsoft.Extensions.Logging;
 using Mono.Nat;
 

+ 0 - 1
Emby.Server.Implementations/HttpServer/Security/AuthService.cs

@@ -2,7 +2,6 @@
 
 using System.Threading.Tasks;
 using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Net;
 using Microsoft.AspNetCore.Http;
 

+ 8 - 8
Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs

@@ -28,35 +28,35 @@ namespace Emby.Server.Implementations.Images
             var view = (CollectionFolder)item;
             var viewType = view.CollectionType;
 
-            string[] includeItemTypes;
+            BaseItemKind[] includeItemTypes;
 
             if (string.Equals(viewType, CollectionType.Movies, StringComparison.Ordinal))
             {
-                includeItemTypes = new string[] { "Movie" };
+                includeItemTypes = new[] { BaseItemKind.Movie };
             }
             else if (string.Equals(viewType, CollectionType.TvShows, StringComparison.Ordinal))
             {
-                includeItemTypes = new string[] { "Series" };
+                includeItemTypes = new[] { BaseItemKind.Series };
             }
             else if (string.Equals(viewType, CollectionType.Music, StringComparison.Ordinal))
             {
-                includeItemTypes = new string[] { "MusicAlbum" };
+                includeItemTypes = new[] { BaseItemKind.MusicAlbum };
             }
             else if (string.Equals(viewType, CollectionType.Books, StringComparison.Ordinal))
             {
-                includeItemTypes = new string[] { "Book", "AudioBook" };
+                includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook };
             }
             else if (string.Equals(viewType, CollectionType.BoxSets, StringComparison.Ordinal))
             {
-                includeItemTypes = new string[] { "BoxSet" };
+                includeItemTypes = new[] { BaseItemKind.BoxSet };
             }
             else if (string.Equals(viewType, CollectionType.HomeVideos, StringComparison.Ordinal) || string.Equals(viewType, CollectionType.Photos, StringComparison.Ordinal))
             {
-                includeItemTypes = new string[] { "Video", "Photo" };
+                includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Photo };
             }
             else
             {
-                includeItemTypes = new string[] { "Video", "Audio", "Photo", "Movie", "Series" };
+                includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Audio, BaseItemKind.Photo, BaseItemKind.Movie, BaseItemKind.Series };
             }
 
             var recursive = !string.Equals(CollectionType.Playlists, viewType, StringComparison.OrdinalIgnoreCase);

+ 4 - 2
Emby.Server.Implementations/Images/DynamicImageProvider.cs

@@ -6,6 +6,8 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
@@ -34,14 +36,14 @@ namespace Emby.Server.Implementations.Images
             var view = (UserView)item;
 
             var isUsingCollectionStrip = IsUsingCollectionStrip(view);
-            var recursive = isUsingCollectionStrip && !new[] { CollectionType.BoxSets, CollectionType.Playlists }.Contains(view.ViewType ?? string.Empty, StringComparer.OrdinalIgnoreCase);
+            var recursive = isUsingCollectionStrip && !new[] { CollectionType.BoxSets, CollectionType.Playlists }.Contains(view.ViewType ?? string.Empty, StringComparison.OrdinalIgnoreCase);
 
             var result = view.GetItemList(new InternalItemsQuery
             {
                 User = view.UserId.HasValue ? _userManager.GetUserById(view.UserId.Value) : null,
                 CollapseBoxSetItems = false,
                 Recursive = recursive,
-                ExcludeItemTypes = new[] { "UserView", "CollectionFolder", "Person" },
+                ExcludeItemTypes = new[] { BaseItemKind.UserView, BaseItemKind.CollectionFolder, BaseItemKind.Person },
                 DtoOptions = new DtoOptions(false)
             });
 

+ 1 - 3
Emby.Server.Implementations/Images/GenreImageProvider.cs

@@ -8,8 +8,6 @@ using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
@@ -43,7 +41,7 @@ namespace Emby.Server.Implementations.Images
             return _libraryManager.GetItemList(new InternalItemsQuery
             {
                 Genres = new[] { item.Name },
-                IncludeItemTypes = new[] { nameof(Series), nameof(Movie) },
+                IncludeItemTypes = new[] { BaseItemKind.Series, BaseItemKind.Movie },
                 OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
                 Limit = 4,
                 Recursive = true,

+ 3 - 3
Emby.Server.Implementations/Images/MusicGenreImageProvider.cs

@@ -44,9 +44,9 @@ namespace Emby.Server.Implementations.Images
                 Genres = new[] { item.Name },
                 IncludeItemTypes = new[]
                 {
-                    nameof(MusicAlbum),
-                    nameof(MusicVideo),
-                    nameof(Audio)
+                    BaseItemKind.MusicAlbum,
+                    BaseItemKind.MusicVideo,
+                    BaseItemKind.Audio
                 },
                 OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) },
                 Limit = 4,

+ 4 - 14
Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs

@@ -54,20 +54,10 @@ namespace Emby.Server.Implementations.Library
             {
                 if (parent != null)
                 {
-                    // Ignore trailer folders but allow it at the collection level
-                    if (string.Equals(filename, BaseItem.TrailersFolderName, StringComparison.OrdinalIgnoreCase)
-                        && !(parent is AggregateFolder)
-                        && !(parent is UserRootFolder))
-                    {
-                        return true;
-                    }
-
-                    if (string.Equals(filename, BaseItem.ThemeVideosFolderName, StringComparison.OrdinalIgnoreCase))
-                    {
-                        return true;
-                    }
-
-                    if (string.Equals(filename, BaseItem.ThemeSongsFolderName, StringComparison.OrdinalIgnoreCase))
+                    // Ignore extras folders but allow it at the collection level
+                    if (_namingOptions.AllExtrasTypesFolderNames.ContainsKey(filename)
+                        && parent is not AggregateFolder
+                        && parent is not UserRootFolder)
                     {
                         return true;
                     }

+ 86 - 80
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -11,11 +11,9 @@ using System.Net;
 using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
-using Emby.Naming.Audio;
 using Emby.Naming.Common;
 using Emby.Naming.TV;
 using Emby.Naming.Video;
-using Emby.Server.Implementations.Library.Resolvers;
 using Emby.Server.Implementations.Library.Validators;
 using Emby.Server.Implementations.Playlists;
 using Emby.Server.Implementations.ScheduledTasks;
@@ -533,8 +531,8 @@ namespace Emby.Server.Implementations.Library
             return key.GetMD5();
         }
 
-        public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null)
-            => ResolvePath(fileInfo, new DirectoryService(_fileSystem), null, parent);
+        public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null, IDirectoryService directoryService = null)
+            => ResolvePath(fileInfo, directoryService ?? new DirectoryService(_fileSystem), null, parent);
 
         private BaseItem ResolvePath(
             FileSystemMetadata fileInfo,
@@ -654,7 +652,7 @@ namespace Emby.Server.Implementations.Library
             return !args.ContainsFileSystemEntryByName(".ignore");
         }
 
-        public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, string collectionType)
+        public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, string collectionType = null)
         {
             return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers);
         }
@@ -677,7 +675,7 @@ namespace Emby.Server.Implementations.Library
                 {
                     var result = resolver.ResolveMultiple(parent, fileList, collectionType, directoryService);
 
-                    if (result != null && result.Items.Count > 0)
+                    if (result?.Items.Count > 0)
                     {
                         var items = new List<BaseItem>();
                         items.AddRange(result.Items);
@@ -965,7 +963,7 @@ namespace Emby.Server.Implementations.Library
             {
                 var existing = GetItemList(new InternalItemsQuery
                 {
-                    IncludeItemTypes = new[] { nameof(MusicArtist) },
+                    IncludeItemTypes = new[] { BaseItemKind.MusicArtist },
                     Name = name,
                     DtoOptions = options
                 }).Cast<MusicArtist>()
@@ -2685,89 +2683,105 @@ namespace Emby.Server.Implementations.Library
             };
         }
 
-        public IEnumerable<Video> FindTrailers(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
+        public IEnumerable<BaseItem> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
         {
-            var namingOptions = _namingOptions;
-
-            var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
-                .Where(i => string.Equals(i.Name, BaseItem.TrailersFolderName, StringComparison.OrdinalIgnoreCase))
-                .SelectMany(i => _fileSystem.GetFiles(i.FullName, namingOptions.VideoFileExtensions, false, false))
-                .ToList();
-
-            var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
-
-            var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
-
-            if (currentVideo != null)
+            var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions);
+            if (ownerVideoInfo == null)
             {
-                files.AddRange(currentVideo.Extras.Where(i => i.ExtraType == ExtraType.Trailer).Select(i => _fileSystem.GetFileInfo(i.Path)));
+                yield break;
             }
 
-            var resolvers = new IItemResolver[]
+            var count = fileSystemChildren.Count;
+            var files = new List<VideoFileInfo>();
+            var nonVideoFiles = new List<FileSystemMetadata>();
+            for (var i = 0; i < count; i++)
             {
-                new GenericVideoResolver<Trailer>(_namingOptions)
-            };
+                var current = fileSystemChildren[i];
+                if (current.IsDirectory && _namingOptions.AllExtrasTypesFolderNames.ContainsKey(current.Name))
+                {
+                    var filesInSubFolder = _fileSystem.GetFiles(current.FullName, _namingOptions.VideoFileExtensions, false, false);
+                    foreach (var file in filesInSubFolder)
+                    {
+                        var videoInfo = VideoResolver.Resolve(file.FullName, file.IsDirectory, _namingOptions);
+                        if (videoInfo == null)
+                        {
+                            nonVideoFiles.Add(file);
+                            continue;
+                        }
 
-            return ResolvePaths(files, directoryService, null, new LibraryOptions(), null, resolvers)
-                .OfType<Trailer>()
-                .Select(video =>
+                        files.Add(videoInfo);
+                    }
+                }
+                else if (!current.IsDirectory)
                 {
-                    // Try to retrieve it from the db. If we don't find it, use the resolved version
-                    if (GetItemById(video.Id) is Trailer dbItem)
+                    var videoInfo = VideoResolver.Resolve(current.FullName, current.IsDirectory, _namingOptions);
+                    if (videoInfo == null)
                     {
-                        video = dbItem;
+                        nonVideoFiles.Add(current);
+                        continue;
                     }
 
-                    video.ParentId = Guid.Empty;
-                    video.OwnerId = owner.Id;
-                    video.ExtraType = ExtraType.Trailer;
-                    video.TrailerTypes = new[] { TrailerType.LocalTrailer };
-
-                    return video;
-
-                    // Sort them so that the list can be easily compared for changes
-                }).OrderBy(i => i.Path);
-        }
-
-        public IEnumerable<Video> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
-        {
-            var namingOptions = _namingOptions;
+                    files.Add(videoInfo);
+                }
+            }
 
-            var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
-                .Where(i => BaseItem.AllExtrasTypesFolderNames.ContainsKey(i.Name ?? string.Empty))
-                .SelectMany(i => _fileSystem.GetFiles(i.FullName, namingOptions.VideoFileExtensions, false, false))
-                .ToList();
+            if (files.Count == 0)
+            {
+                yield break;
+            }
 
-            var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
+            var videos = VideoListResolver.Resolve(files, _namingOptions);
+            // owner video info cannot be null as that implies it has no path
+            var extras = ExtraResolver.GetExtras(videos, ownerVideoInfo, _namingOptions.VideoFlagDelimiters);
+            for (var i = 0; i < extras.Count; i++)
+            {
+                var currentExtra = extras[i];
+                var resolved = ResolvePath(_fileSystem.GetFileInfo(currentExtra.Path), null, directoryService);
+                if (resolved is not Video video)
+                {
+                    continue;
+                }
 
-            var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
+                // Try to retrieve it from the db. If we don't find it, use the resolved version
+                if (GetItemById(resolved.Id) is Video dbItem)
+                {
+                    video = dbItem;
+                }
 
-            if (currentVideo != null)
-            {
-                files.AddRange(currentVideo.Extras.Where(i => i.ExtraType != ExtraType.Trailer).Select(i => _fileSystem.GetFileInfo(i.Path)));
+                video.ExtraType = currentExtra.ExtraType;
+                video.ParentId = Guid.Empty;
+                video.OwnerId = owner.Id;
+                yield return video;
             }
 
-            return ResolvePaths(files, directoryService, null, new LibraryOptions(), null)
-                .OfType<Video>()
-                .Select(video =>
+            // TODO: theme songs must be handled "manually" (but should we?) since they aren't video files
+            for (var i = 0; i < nonVideoFiles.Count; i++)
+            {
+                var current = nonVideoFiles[i];
+                var extraInfo = ExtraResolver.GetExtraInfo(current.FullName, _namingOptions);
+                if (extraInfo.ExtraType != ExtraType.ThemeSong)
                 {
-                    // Try to retrieve it from the db. If we don't find it, use the resolved version
-                    var dbItem = GetItemById(video.Id) as Video;
-
-                    if (dbItem != null)
-                    {
-                        video = dbItem;
-                    }
+                    continue;
+                }
 
-                    video.ParentId = Guid.Empty;
-                    video.OwnerId = owner.Id;
+                var resolved = ResolvePath(current, null, directoryService);
+                if (resolved is not Audio themeSong)
+                {
+                    continue;
+                }
 
-                    SetExtraTypeFromFilename(video);
+                // Try to retrieve it from the db. If we don't find it, use the resolved version
+                if (GetItemById(themeSong.Id) is Audio dbItem)
+                {
+                    themeSong = dbItem;
+                }
 
-                    return video;
+                themeSong.ExtraType = ExtraType.ThemeSong;
+                themeSong.OwnerId = owner.Id;
+                themeSong.ParentId = Guid.Empty;
 
-                    // Sort them so that the list can be easily compared for changes
-                }).OrderBy(i => i.Path);
+                yield return themeSong;
+            }
         }
 
         public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
@@ -2817,15 +2831,6 @@ namespace Emby.Server.Implementations.Library
             return path;
         }
 
-        private void SetExtraTypeFromFilename(Video item)
-        {
-            var resolver = new ExtraResolver(_namingOptions);
-
-            var result = resolver.GetExtraInfo(item.Path);
-
-            item.ExtraType = result.ExtraType;
-        }
-
         public List<PersonInfo> GetPeople(InternalPeopleQuery query)
         {
             return _itemRepository.GetPeople(query);
@@ -2932,11 +2937,12 @@ namespace Emby.Server.Implementations.Library
 
             var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 
+            var existingNameCount = 1; // first numbered name will be 2
             var virtualFolderPath = Path.Combine(rootFolderPath, name);
             while (Directory.Exists(virtualFolderPath))
             {
-                name += "1";
-                virtualFolderPath = Path.Combine(rootFolderPath, name);
+                existingNameCount++;
+                virtualFolderPath = Path.Combine(rootFolderPath, name + " " + existingNameCount);
             }
 
             var mediaPathInfos = options.PathInfos;

+ 8 - 7
Emby.Server.Implementations/Library/MediaStreamSelector.cs

@@ -6,6 +6,7 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Model.Entities;
 
 namespace Emby.Server.Implementations.Library
@@ -64,18 +65,18 @@ namespace Emby.Server.Implementations.Library
                 stream = sortedStreams.FirstOrDefault(s => s.IsExternal || s.IsForced || s.IsDefault);
 
                 // if the audio language is not understood by the user, load their preferred subs, if there are any
-                if (stream == null && !preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase))
+                if (stream == null && !preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
                 {
-                    stream = sortedStreams.FirstOrDefault(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase));
+                    stream = sortedStreams.FirstOrDefault(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase));
                 }
             }
             else if (mode == SubtitlePlaybackMode.Smart)
             {
                 // if the audio language is not understood by the user, load their preferred subs, if there are any
-                if (!preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase))
+                if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
                 {
-                    stream = streams.FirstOrDefault(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase)) ??
-                        streams.FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase));
+                    stream = streams.FirstOrDefault(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase)) ??
+                        streams.FirstOrDefault(s => preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase));
                 }
             }
             else if (mode == SubtitlePlaybackMode.Always)
@@ -136,9 +137,9 @@ namespace Emby.Server.Implementations.Library
             else if (mode == SubtitlePlaybackMode.Smart)
             {
                 // Prefer smart logic over embedded metadata
-                if (!preferredLanguages.Contains(audioTrackLanguage, StringComparer.OrdinalIgnoreCase))
+                if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
                 {
-                    filteredStreams = streams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparer.OrdinalIgnoreCase))
+                    filteredStreams = streams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase))
                         .ToList();
                 }
             }

+ 2 - 2
Emby.Server.Implementations/Library/MusicManager.cs

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

+ 22 - 8
Emby.Server.Implementations/Library/PathExtensions.cs

@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library
         /// <param name="attribute">The attrib.</param>
         /// <returns>System.String.</returns>
         /// <exception cref="ArgumentException"><paramref name="str" /> or <paramref name="attribute" /> is empty.</exception>
-        public static string? GetAttributeValue(this string str, string attribute)
+        public static string? GetAttributeValue(this ReadOnlySpan<char> str, ReadOnlySpan<char> attribute)
         {
             if (str.Length == 0)
             {
@@ -28,17 +28,31 @@ namespace Emby.Server.Implementations.Library
                 throw new ArgumentException("String can't be empty.", nameof(attribute));
             }
 
-            string srch = "[" + attribute + "=";
-            int start = str.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
-            if (start != -1)
+            var attributeIndex = str.IndexOf(attribute, StringComparison.OrdinalIgnoreCase);
+
+            // Must be at least 3 characters after the attribute =, ], any character.
+            var maxIndex = str.Length - attribute.Length - 3;
+            while (attributeIndex > -1 && attributeIndex < maxIndex)
             {
-                start += srch.Length;
-                int end = str.IndexOf(']', start);
-                return str.Substring(start, end - start);
+                var attributeEnd = attributeIndex + attribute.Length;
+                if (attributeIndex > 0
+                    && str[attributeIndex - 1] == '['
+                    && str[attributeEnd] == '=')
+                {
+                    var closingIndex = str[attributeEnd..].IndexOf(']');
+                    // Must be at least 1 character before the closing bracket.
+                    if (closingIndex > 1)
+                    {
+                        return str[(attributeEnd + 1)..(attributeEnd + closingIndex)].Trim().ToString();
+                    }
+                }
+
+                str = str[attributeEnd..];
+                attributeIndex = str.IndexOf(attribute, StringComparison.OrdinalIgnoreCase);
             }
 
             // for imdbid we also accept pattern matching
-            if (string.Equals(attribute, "imdbid", StringComparison.OrdinalIgnoreCase))
+            if (attribute.Equals("imdbid", StringComparison.OrdinalIgnoreCase))
             {
                 var match = ProviderIdParsers.TryFindImdbId(str, out var imdbId);
                 return match ? imdbId.ToString() : null;

+ 0 - 2
Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs

@@ -4,12 +4,10 @@ using System;
 using System.Linq;
 using System.Threading.Tasks;
 using Emby.Naming.Common;
-using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.Library.Resolvers.Audio

+ 36 - 85
Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs

@@ -49,120 +49,71 @@ namespace Emby.Server.Implementations.Library.Resolvers
         protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
               where TVideoType : Video, new()
         {
-            var namingOptions = NamingOptions;
+            VideoFileInfo videoInfo = null;
+            VideoType? videoType = null;
 
             // If the path is a file check for a matching extensions
             if (args.IsDirectory)
             {
-                TVideoType video = null;
-                VideoFileInfo videoInfo = null;
-
                 // Loop through each child file/folder and see if we find a video
                 foreach (var child in args.FileSystemChildren)
                 {
                     var filename = child.Name;
-
                     if (child.IsDirectory)
                     {
                         if (IsDvdDirectory(child.FullName, filename, args.DirectoryService))
                         {
-                            videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
-
-                            if (videoInfo == null)
-                            {
-                                return null;
-                            }
-
-                            video = new TVideoType
-                            {
-                                Path = args.Path,
-                                VideoType = VideoType.Dvd,
-                                ProductionYear = videoInfo.Year
-                            };
-                            break;
+                            videoType = VideoType.Dvd;
                         }
-
-                        if (IsBluRayDirectory(filename))
+                        else if (IsBluRayDirectory(filename))
                         {
-                            videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
-
-                            if (videoInfo == null)
-                            {
-                                return null;
-                            }
-
-                            video = new TVideoType
-                            {
-                                Path = args.Path,
-                                VideoType = VideoType.BluRay,
-                                ProductionYear = videoInfo.Year
-                            };
-                            break;
+                            videoType = VideoType.BluRay;
                         }
                     }
                     else if (IsDvdFile(filename))
                     {
-                        videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
-
-                        if (videoInfo == null)
-                        {
-                            return null;
-                        }
-
-                        video = new TVideoType
-                        {
-                            Path = args.Path,
-                            VideoType = VideoType.Dvd,
-                            ProductionYear = videoInfo.Year
-                        };
-                        break;
+                        videoType = VideoType.Dvd;
                     }
-                }
 
-                if (video != null)
-                {
-                    video.Name = parseName ?
-                        videoInfo.Name :
-                        Path.GetFileName(args.Path);
+                    if (videoType == null)
+                    {
+                        continue;
+                    }
 
-                    Set3DFormat(video, videoInfo);
+                    videoInfo = VideoResolver.ResolveDirectory(args.Path, NamingOptions, parseName);
+                    break;
                 }
-
-                return video;
             }
             else
             {
-                var videoInfo = VideoResolver.Resolve(args.Path, false, namingOptions, false);
-
-                if (videoInfo == null)
-                {
-                    return null;
-                }
-
-                if (VideoResolver.IsVideoFile(args.Path, NamingOptions) || videoInfo.IsStub)
-                {
-                    var path = args.Path;
-
-                    var video = new TVideoType
-                    {
-                        Path = path,
-                        IsInMixedFolder = true,
-                        ProductionYear = videoInfo.Year
-                    };
-
-                    SetVideoType(video, videoInfo);
+                videoInfo = VideoResolver.Resolve(args.Path, false, NamingOptions, parseName);
+            }
 
-                    video.Name = parseName ?
-                        videoInfo.Name :
-                        Path.GetFileNameWithoutExtension(args.Path);
+            if (videoInfo == null || (!videoInfo.IsStub && !VideoResolver.IsVideoFile(args.Path, NamingOptions)))
+            {
+                return null;
+            }
 
-                    Set3DFormat(video, videoInfo);
+            var video = new TVideoType
+            {
+                Name = videoInfo.Name,
+                Path = args.Path,
+                ProductionYear = videoInfo.Year,
+                ExtraType = videoInfo.ExtraType
+            };
 
-                    return video;
-                }
+            if (videoType.HasValue)
+            {
+                video.VideoType = videoType.Value;
             }
+            else
+            {
+                SetVideoType(video, videoInfo);
+            }
+
+            Set3DFormat(video, videoInfo);
 
-            return null;
+            return video;
         }
 
         protected void SetVideoType(Video video, VideoFileInfo videoInfo)
@@ -207,8 +158,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
                 {
                     // use disc-utils, both DVDs and BDs use UDF filesystem
                     using (var videoFileStream = File.Open(video.Path, FileMode.Open, FileAccess.Read))
+                    using (UdfReader udfReader = new UdfReader(videoFileStream))
                     {
-                        UdfReader udfReader = new UdfReader(videoFileStream);
                         if (udfReader.DirectoryExists("VIDEO_TS"))
                         {
                             video.IsoType = IsoType.Dvd;

+ 2 - 1
Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs

@@ -5,6 +5,7 @@
 using System;
 using System.IO;
 using System.Linq;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Entities;
@@ -32,7 +33,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
 
             var extension = Path.GetExtension(args.Path);
 
-            if (extension != null && _validExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+            if (extension != null && _validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
             {
                 // It's a book
                 return new Book

+ 1 - 1
Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs

@@ -65,7 +65,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
         private static void SetProviderIdFromPath(BaseItem item)
         {
             // we need to only look at the name of this actual item (not parents)
-            var justName = Path.GetFileName(item.Path);
+            var justName = Path.GetFileName(item.Path.AsSpan());
 
             var id = justName.GetAttributeValue("tmdbid");
 

+ 69 - 44
Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs

@@ -26,7 +26,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
     public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
     {
         private readonly IImageProcessor _imageProcessor;
-        private readonly StackResolver _stackResolver;
 
         private string[] _validCollectionTypes = new[]
         {
@@ -46,7 +45,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
             : base(namingOptions)
         {
             _imageProcessor = imageProcessor;
-            _stackResolver = new StackResolver(NamingOptions);
         }
 
         /// <summary>
@@ -62,7 +60,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
             string collectionType,
             IDirectoryService directoryService)
         {
-            var result = ResolveMultipleInternal(parent, files, collectionType, directoryService);
+            var result = ResolveMultipleInternal(parent, files, collectionType);
 
             if (result != null)
             {
@@ -92,16 +90,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                     return null;
                 }
 
+                Video movie = null;
                 var files = args.GetActualFileSystemChildren().ToList();
 
                 if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
                 {
-                    return FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
+                    movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
                 }
 
                 if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
                 {
-                    return FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
+                    movie = FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
                 }
 
                 if (string.IsNullOrEmpty(collectionType))
@@ -118,17 +117,16 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                         return null;
                     }
 
-                    {
-                        return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
-                    }
+                    movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
                 }
 
                 if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
                 {
-                    return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
+                    movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
                 }
 
-                return null;
+                // ignore extras
+                return movie?.ExtraType == null ? movie : null;
             }
 
             // Handle owned items
@@ -169,6 +167,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                 item = ResolveVideo<Video>(args, false);
             }
 
+            // Ignore extras
+            if (item?.ExtraType != null)
+            {
+                return null;
+            }
+
             if (item != null)
             {
                 item.IsInMixedFolder = true;
@@ -180,8 +184,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
         private MultiItemResolverResult ResolveMultipleInternal(
             Folder parent,
             List<FileSystemMetadata> files,
-            string collectionType,
-            IDirectoryService directoryService)
+            string collectionType)
         {
             if (IsInvalid(parent, collectionType))
             {
@@ -190,13 +193,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
 
             if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
             {
-                return ResolveVideos<MusicVideo>(parent, files, directoryService, true, collectionType, false);
+                return ResolveVideos<MusicVideo>(parent, files, true, collectionType, false);
             }
 
             if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
                             string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
             {
-                return ResolveVideos<Video>(parent, files, directoryService, false, collectionType, false);
+                return ResolveVideos<Video>(parent, files, false, collectionType, false);
             }
 
             if (string.IsNullOrEmpty(collectionType))
@@ -204,7 +207,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                 // Owned items should just use the plain video type
                 if (parent == null)
                 {
-                    return ResolveVideos<Video>(parent, files, directoryService, false, collectionType, false);
+                    return ResolveVideos<Video>(parent, files, false, collectionType, false);
                 }
 
                 if (parent is Series || parent.GetParents().OfType<Series>().Any())
@@ -212,12 +215,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                     return null;
                 }
 
-                return ResolveVideos<Movie>(parent, files, directoryService, false, collectionType, true);
+                return ResolveVideos<Movie>(parent, files, false, collectionType, true);
             }
 
             if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
             {
-                return ResolveVideos<Movie>(parent, files, directoryService, true, collectionType, true);
+                return ResolveVideos<Movie>(parent, files, true, collectionType, true);
             }
 
             return null;
@@ -226,21 +229,20 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
         private MultiItemResolverResult ResolveVideos<T>(
             Folder parent,
             IEnumerable<FileSystemMetadata> fileSystemEntries,
-            IDirectoryService directoryService,
-            bool suppportMultiEditions,
+            bool supportMultiEditions,
             string collectionType,
             bool parseName)
             where T : Video, new()
         {
             var files = new List<FileSystemMetadata>();
-            var videos = new List<BaseItem>();
             var leftOver = new List<FileSystemMetadata>();
+            var hasCollectionType = !string.IsNullOrEmpty(collectionType);
 
             // Loop through each child file/folder and see if we find a video
             foreach (var child in fileSystemEntries)
             {
                 // This is a hack but currently no better way to resolve a sometimes ambiguous situation
-                if (string.IsNullOrEmpty(collectionType))
+                if (!hasCollectionType)
                 {
                     if (string.Equals(child.Name, "tvshow.nfo", StringComparison.OrdinalIgnoreCase)
                         || string.Equals(child.Name, "season.nfo", StringComparison.OrdinalIgnoreCase))
@@ -259,29 +261,39 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                 }
             }
 
-            var resolverResult = VideoListResolver.Resolve(files, NamingOptions, suppportMultiEditions).ToList();
+            var videoInfos = files
+                .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName))
+                .Where(f => f != null)
+                .ToList();
+
+            var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName);
 
             var result = new MultiItemResolverResult
             {
-                ExtraFiles = leftOver,
-                Items = videos
+                ExtraFiles = leftOver
             };
 
-            var isInMixedFolder = resolverResult.Count > 1 || (parent != null && parent.IsTopParent);
+            var isInMixedFolder = resolverResult.Count > 1 || parent?.IsTopParent == true;
 
             foreach (var video in resolverResult)
             {
                 var firstVideo = video.Files[0];
+                var path = firstVideo.Path;
+                if (video.ExtraType != null)
+                {
+                    result.ExtraFiles.Add(files.Find(f => string.Equals(f.FullName, path, StringComparison.OrdinalIgnoreCase)));
+                    continue;
+                }
+
+                var additionalParts = video.Files.Count > 1 ? video.Files.Skip(1).Select(i => i.Path).ToArray() : Array.Empty<string>();
 
                 var videoItem = new T
                 {
-                    Path = video.Files[0].Path,
+                    Path = path,
                     IsInMixedFolder = isInMixedFolder,
                     ProductionYear = video.Year,
-                    Name = parseName ?
-                        video.Name :
-                        Path.GetFileNameWithoutExtension(video.Files[0].Path),
-                    AdditionalParts = video.Files.Skip(1).Select(i => i.Path).ToArray(),
+                    Name = parseName ? video.Name : firstVideo.Name,
+                    AdditionalParts = additionalParts,
                     LocalAlternateVersions = video.AlternateVersions.Select(i => i.Path).ToArray()
                 };
 
@@ -299,21 +311,34 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
         private static bool IsIgnored(string filename)
         {
             // Ignore samples
-            Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase);
+            Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
 
             return m.Success;
         }
 
-        private bool ContainsFile(List<VideoInfo> result, FileSystemMetadata file)
+        private static bool ContainsFile(IReadOnlyList<VideoInfo> result, FileSystemMetadata file)
         {
-            return result.Any(i => ContainsFile(i, file));
-        }
+            for (var i = 0; i < result.Count; i++)
+            {
+                var current = result[i];
+                for (var j = 0; j < current.Files.Count; j++)
+                {
+                    if (ContainsFile(current.Files[j], file))
+                    {
+                        return true;
+                    }
+                }
 
-        private bool ContainsFile(VideoInfo result, FileSystemMetadata file)
-        {
-            return result.Files.Any(i => ContainsFile(i, file)) ||
-                result.AlternateVersions.Any(i => ContainsFile(i, file)) ||
-                result.Extras.Any(i => ContainsFile(i, file));
+                for (var j = 0; j < current.AlternateVersions.Count; j++)
+                {
+                    if (ContainsFile(current.AlternateVersions[j], file))
+                    {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
         }
 
         private static bool ContainsFile(VideoFileInfo result, FileSystemMetadata file)
@@ -342,9 +367,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
             if (item is Movie || item is MusicVideo)
             {
                 // We need to only look at the name of this actual item (not parents)
-                var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path) : Path.GetFileName(item.ContainingFolderPath);
+                var justName = item.IsInMixedFolder ? Path.GetFileName(item.Path.AsSpan()) : Path.GetFileName(item.ContainingFolderPath.AsSpan());
 
-                if (!string.IsNullOrEmpty(justName))
+                if (!justName.IsEmpty)
                 {
                     // check for tmdb id
                     var tmdbid = justName.GetAttributeValue("tmdbid");
@@ -358,7 +383,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                 if (!string.IsNullOrEmpty(item.Path))
                 {
                     // check for imdb id - we use full media path, as we can assume, that this will match in any use case (wither id in parent dir or in file name)
-                    var imdbid = item.Path.GetAttributeValue("imdbid");
+                    var imdbid = item.Path.AsSpan().GetAttributeValue("imdbid");
 
                     if (!string.IsNullOrWhiteSpace(imdbid))
                     {
@@ -431,7 +456,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
             // TODO: Allow GetMultiDiscMovie in here
             const bool SupportsMultiVersion = true;
 
-            var result = ResolveVideos<T>(parent, fileSystemEntries, directoryService, SupportsMultiVersion, collectionType, parseName) ??
+            var result = ResolveVideos<T>(parent, fileSystemEntries, SupportsMultiVersion, collectionType, parseName) ??
                 new MultiItemResolverResult();
 
             if (result.Items.Count == 1)
@@ -510,7 +535,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                 return null;
             }
 
-            var result = _stackResolver.ResolveDirectories(folderPaths).ToList();
+            var result = StackResolver.ResolveDirectories(folderPaths, NamingOptions).ToList();
 
             if (result.Count != 1)
             {

+ 2 - 1
Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs

@@ -8,6 +8,7 @@ using System.IO;
 using System.Linq;
 using Emby.Naming.Common;
 using Emby.Naming.Video;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -109,7 +110,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
             }
 
             string extension = Path.GetExtension(path).TrimStart('.');
-            return imageProcessor.SupportedInputFormats.Contains(extension, StringComparer.OrdinalIgnoreCase);
+            return imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase);
         }
     }
 }

+ 3 - 2
Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs

@@ -5,6 +5,7 @@
 using System;
 using System.IO;
 using System.Linq;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Resolvers;
@@ -57,10 +58,10 @@ namespace Emby.Server.Implementations.Library.Resolvers
 
             // Check if this is a music playlist file
             // It should have the correct collection type and a supported file extension
-            else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+            else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
             {
                 var extension = Path.GetExtension(args.Path);
-                if (Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+                if (Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase))
                 {
                     return new Playlist
                     {

+ 23 - 22
Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs

@@ -3,7 +3,6 @@
 using System;
 using System.Linq;
 using Emby.Naming.Common;
-using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Entities;
@@ -45,34 +44,36 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
 
             // If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something
             // Also handle flat tv folders
-            if ((season != null ||
-                 string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
-                 args.HasParent<Series>())
-                && (parent is Series || !BaseItem.AllExtrasTypesFolderNames.ContainsKey(parent.Name)))
+            if (season != null ||
+                string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
+                args.HasParent<Series>())
             {
                 var episode = ResolveVideo<Episode>(args, false);
 
-                if (episode != null)
+                // Ignore extras
+                if (episode == null || episode.ExtraType != null)
                 {
-                    var series = parent as Series ?? parent.GetParents().OfType<Series>().FirstOrDefault();
+                    return null;
+                }
 
-                    if (series != null)
-                    {
-                        episode.SeriesId = series.Id;
-                        episode.SeriesName = series.Name;
-                    }
+                var series = parent as Series ?? parent.GetParents().OfType<Series>().FirstOrDefault();
 
-                    if (season != null)
-                    {
-                        episode.SeasonId = season.Id;
-                        episode.SeasonName = season.Name;
-                    }
+                if (series != null)
+                {
+                    episode.SeriesId = series.Id;
+                    episode.SeriesName = series.Name;
+                }
 
-                    // Assume season 1 if there's no season folder and a season number could not be determined
-                    if (season == null && !episode.ParentIndexNumber.HasValue && (episode.IndexNumber.HasValue || episode.PremiereDate.HasValue))
-                    {
-                        episode.ParentIndexNumber = 1;
-                    }
+                if (season != null)
+                {
+                    episode.SeasonId = season.Id;
+                    episode.SeasonName = season.Name;
+                }
+
+                // Assume season 1 if there's no season folder and a season number could not be determined
+                if (season == null && !episode.ParentIndexNumber.HasValue && (episode.IndexNumber.HasValue || episode.PremiereDate.HasValue))
+                {
+                    episode.ParentIndexNumber = 1;
                 }
 
                 return episode;

+ 33 - 7
Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs

@@ -5,12 +5,9 @@
 using System;
 using System.Collections.Generic;
 using System.IO;
-using System.Linq;
 using Emby.Naming.Common;
 using Emby.Naming.TV;
 using Emby.Naming.Video;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Resolvers;
@@ -185,13 +182,42 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
         /// <param name="path">The path.</param>
         private static void SetProviderIdFromPath(Series item, string path)
         {
-            var justName = Path.GetFileName(path);
+            var justName = Path.GetFileName(path.AsSpan());
 
-            var id = justName.GetAttributeValue("tvdbid");
+            var tvdbId = justName.GetAttributeValue("tvdbid");
+            if (!string.IsNullOrEmpty(tvdbId))
+            {
+                item.SetProviderId(MetadataProvider.Tvdb, tvdbId);
+            }
+
+            var tvmazeId = justName.GetAttributeValue("tvmazeid");
+            if (!string.IsNullOrEmpty(tvmazeId))
+            {
+                item.SetProviderId(MetadataProvider.TvMaze, tvmazeId);
+            }
+
+            var tmdbId = justName.GetAttributeValue("tmdbid");
+            if (!string.IsNullOrEmpty(tmdbId))
+            {
+                item.SetProviderId(MetadataProvider.Tmdb, tmdbId);
+            }
+
+            var anidbId = justName.GetAttributeValue("anidbid");
+            if (!string.IsNullOrEmpty(anidbId))
+            {
+                item.SetProviderId("AniDB", anidbId);
+            }
+
+            var aniListId = justName.GetAttributeValue("anilistid");
+            if (!string.IsNullOrEmpty(aniListId))
+            {
+                item.SetProviderId("AniList", aniListId);
+            }
 
-            if (!string.IsNullOrEmpty(id))
+            var aniSearchId = justName.GetAttributeValue("anisearchid");
+            if (!string.IsNullOrEmpty(aniSearchId))
             {
-                item.SetProviderId(MetadataProvider.Tvdb, id);
+                item.SetProviderId("AniSearch", aniSearchId);
             }
         }
     }

+ 23 - 26
Emby.Server.Implementations/Library/SearchEngine.cs

@@ -10,12 +10,9 @@ using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Search;
-using Genre = MediaBrowser.Controller.Entities.Genre;
-using Person = MediaBrowser.Controller.Entities.Person;
 
 namespace Emby.Server.Implementations.Library
 {
@@ -59,9 +56,9 @@ namespace Emby.Server.Implementations.Library
             };
         }
 
-        private static void AddIfMissing(List<string> list, string value)
+        private static void AddIfMissing(List<BaseItemKind> list, BaseItemKind value)
         {
-            if (!list.Contains(value, StringComparer.OrdinalIgnoreCase))
+            if (!list.Contains(value))
             {
                 list.Add(value);
             }
@@ -86,63 +83,63 @@ namespace Emby.Server.Implementations.Library
             searchTerm = searchTerm.Trim().RemoveDiacritics();
 
             var excludeItemTypes = query.ExcludeItemTypes.ToList();
-            var includeItemTypes = (query.IncludeItemTypes ?? Array.Empty<string>()).ToList();
+            var includeItemTypes = (query.IncludeItemTypes ?? Array.Empty<BaseItemKind>()).ToList();
 
-            excludeItemTypes.Add(nameof(Year));
-            excludeItemTypes.Add(nameof(Folder));
+            excludeItemTypes.Add(BaseItemKind.Year);
+            excludeItemTypes.Add(BaseItemKind.Folder);
 
-            if (query.IncludeGenres && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Genre", StringComparer.OrdinalIgnoreCase)))
+            if (query.IncludeGenres && (includeItemTypes.Count == 0 || includeItemTypes.Contains(BaseItemKind.Genre)))
             {
                 if (!query.IncludeMedia)
                 {
-                    AddIfMissing(includeItemTypes, nameof(Genre));
-                    AddIfMissing(includeItemTypes, nameof(MusicGenre));
+                    AddIfMissing(includeItemTypes, BaseItemKind.Genre);
+                    AddIfMissing(includeItemTypes, BaseItemKind.MusicGenre);
                 }
             }
             else
             {
-                AddIfMissing(excludeItemTypes, nameof(Genre));
-                AddIfMissing(excludeItemTypes, nameof(MusicGenre));
+                AddIfMissing(excludeItemTypes, BaseItemKind.Genre);
+                AddIfMissing(excludeItemTypes, BaseItemKind.MusicGenre);
             }
 
-            if (query.IncludePeople && (includeItemTypes.Count == 0 || includeItemTypes.Contains("People", StringComparer.OrdinalIgnoreCase) || includeItemTypes.Contains("Person", StringComparer.OrdinalIgnoreCase)))
+            if (query.IncludePeople && (includeItemTypes.Count == 0 || includeItemTypes.Contains(BaseItemKind.Person)))
             {
                 if (!query.IncludeMedia)
                 {
-                    AddIfMissing(includeItemTypes, nameof(Person));
+                    AddIfMissing(includeItemTypes, BaseItemKind.Person);
                 }
             }
             else
             {
-                AddIfMissing(excludeItemTypes, nameof(Person));
+                AddIfMissing(excludeItemTypes, BaseItemKind.Person);
             }
 
-            if (query.IncludeStudios && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Studio", StringComparer.OrdinalIgnoreCase)))
+            if (query.IncludeStudios && (includeItemTypes.Count == 0 || includeItemTypes.Contains(BaseItemKind.Studio)))
             {
                 if (!query.IncludeMedia)
                 {
-                    AddIfMissing(includeItemTypes, nameof(Studio));
+                    AddIfMissing(includeItemTypes, BaseItemKind.Studio);
                 }
             }
             else
             {
-                AddIfMissing(excludeItemTypes, nameof(Studio));
+                AddIfMissing(excludeItemTypes, BaseItemKind.Studio);
             }
 
-            if (query.IncludeArtists && (includeItemTypes.Count == 0 || includeItemTypes.Contains("MusicArtist", StringComparer.OrdinalIgnoreCase)))
+            if (query.IncludeArtists && (includeItemTypes.Count == 0 || includeItemTypes.Contains(BaseItemKind.MusicArtist)))
             {
                 if (!query.IncludeMedia)
                 {
-                    AddIfMissing(includeItemTypes, nameof(MusicArtist));
+                    AddIfMissing(includeItemTypes, BaseItemKind.MusicArtist);
                 }
             }
             else
             {
-                AddIfMissing(excludeItemTypes, nameof(MusicArtist));
+                AddIfMissing(excludeItemTypes, BaseItemKind.MusicArtist);
             }
 
-            AddIfMissing(excludeItemTypes, nameof(CollectionFolder));
-            AddIfMissing(excludeItemTypes, nameof(Folder));
+            AddIfMissing(excludeItemTypes, BaseItemKind.CollectionFolder);
+            AddIfMissing(excludeItemTypes, BaseItemKind.Folder);
             var mediaTypes = query.MediaTypes.ToList();
 
             if (includeItemTypes.Count > 0)
@@ -183,7 +180,7 @@ namespace Emby.Server.Implementations.Library
 
             List<BaseItem> mediaItems;
 
-            if (searchQuery.IncludeItemTypes.Length == 1 && string.Equals(searchQuery.IncludeItemTypes[0], "MusicArtist", StringComparison.OrdinalIgnoreCase))
+            if (searchQuery.IncludeItemTypes.Length == 1 && searchQuery.IncludeItemTypes[0] == BaseItemKind.MusicArtist)
             {
                 if (!searchQuery.ParentId.Equals(Guid.Empty))
                 {
@@ -192,7 +189,7 @@ namespace Emby.Server.Implementations.Library
 
                 searchQuery.ParentId = Guid.Empty;
                 searchQuery.IncludeItemsByName = true;
-                searchQuery.IncludeItemTypes = Array.Empty<string>();
+                searchQuery.IncludeItemTypes = Array.Empty<BaseItemKind>();
                 mediaItems = _libraryManager.GetAllArtists(searchQuery).Items.Select(i => i.Item1).ToList();
             }
             else

+ 11 - 13
Emby.Server.Implementations/Library/UserViewManager.cs

@@ -8,11 +8,11 @@ using System.Linq;
 using System.Threading;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.Channels;
@@ -20,8 +20,6 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Library;
 using MediaBrowser.Model.Querying;
-using Genre = MediaBrowser.Controller.Entities.Genre;
-using Person = MediaBrowser.Controller.Entities.Person;
 
 namespace Emby.Server.Implementations.Library
 {
@@ -80,7 +78,7 @@ namespace Emby.Server.Implementations.Library
                     continue;
                 }
 
-                if (query.PresetViews.Contains(folderViewType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+                if (query.PresetViews.Contains(folderViewType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
                 {
                     list.Add(GetUserView(folder, folderViewType, string.Empty));
                 }
@@ -180,7 +178,7 @@ namespace Emby.Server.Implementations.Library
         {
             if (parents.Count == 1 && parents.All(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase)))
             {
-                if (!presetViews.Contains(viewType, StringComparer.OrdinalIgnoreCase))
+                if (!presetViews.Contains(viewType, StringComparison.OrdinalIgnoreCase))
                 {
                     return (Folder)parents[0];
                 }
@@ -300,11 +298,11 @@ namespace Emby.Server.Implementations.Library
                 {
                     if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase)))
                     {
-                        includeItemTypes = new string[] { "Movie" };
+                        includeItemTypes = new[] { BaseItemKind.Movie };
                     }
                     else if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)))
                     {
-                        includeItemTypes = new string[] { "Episode" };
+                        includeItemTypes = new[] { BaseItemKind.Episode };
                     }
                 }
             }
@@ -344,13 +342,13 @@ namespace Emby.Server.Implementations.Library
             var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0
                 ? new[]
                 {
-                    nameof(Person),
-                    nameof(Studio),
-                    nameof(Year),
-                    nameof(MusicGenre),
-                    nameof(Genre)
+                    BaseItemKind.Person,
+                    BaseItemKind.Studio,
+                    BaseItemKind.Year,
+                    BaseItemKind.MusicGenre,
+                    BaseItemKind.Genre
                 }
-                : Array.Empty<string>();
+                : Array.Empty<BaseItemKind>();
 
             var query = new InternalItemsQuery(user)
             {

+ 2 - 1
Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs

@@ -3,6 +3,7 @@ using System.Globalization;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
@@ -81,7 +82,7 @@ namespace Emby.Server.Implementations.Library.Validators
 
             var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new[] { nameof(MusicArtist) },
+                IncludeItemTypes = new[] { BaseItemKind.MusicArtist },
                 IsDeadArtist = true,
                 IsLocked = false
             }).Cast<MusicArtist>().ToList();

+ 2 - 2
Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs

@@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.Library.Validators
                     var movies = _libraryManager.GetItemList(new InternalItemsQuery
                     {
                         MediaTypes = new string[] { MediaType.Video },
-                        IncludeItemTypes = new[] { nameof(Movie) },
+                        IncludeItemTypes = new[] { BaseItemKind.Movie },
                         IsVirtualItem = false,
                         OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
                         Parent = library,
@@ -108,7 +108,7 @@ namespace Emby.Server.Implementations.Library.Validators
 
             var boxSets = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new[] { nameof(BoxSet) },
+                IncludeItemTypes = new[] { BaseItemKind.BoxSet },
                 CollapseBoxSetItems = false,
                 Recursive = true
             });

+ 2 - 1
Emby.Server.Implementations/Library/Validators/PeopleValidator.cs

@@ -2,6 +2,7 @@ using System;
 using System.Globalization;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
@@ -91,7 +92,7 @@ namespace Emby.Server.Implementations.Library.Validators
 
             var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new[] { nameof(Person) },
+                IncludeItemTypes = new[] { BaseItemKind.Person },
                 IsDeadPerson = true,
                 IsLocked = false
             });

+ 2 - 1
Emby.Server.Implementations/Library/Validators/StudiosValidator.cs

@@ -2,6 +2,7 @@ using System;
 using System.Globalization;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
@@ -80,7 +81,7 @@ namespace Emby.Server.Implementations.Library.Validators
 
             var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new[] { nameof(Studio) },
+                IncludeItemTypes = new[] { BaseItemKind.Studio },
                 IsDeadStudio = true,
                 IsLocked = false
             });

+ 12 - 11
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -17,6 +17,7 @@ using System.Xml;
 using Emby.Server.Implementations.Library;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Events;
+using Jellyfin.Extensions;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Progress;
@@ -227,7 +228,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
             foreach (var virtualFolder in virtualFolders)
             {
-                if (!virtualFolder.Locations.Contains(path, StringComparer.OrdinalIgnoreCase))
+                if (!virtualFolder.Locations.Contains(path, StringComparison.OrdinalIgnoreCase))
                 {
                     continue;
                 }
@@ -891,7 +892,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 throw new ArgumentNullException(nameof(tunerHostId));
             }
 
-            return info.EnabledTuners.Contains(tunerHostId, StringComparer.OrdinalIgnoreCase);
+            return info.EnabledTuners.Contains(tunerHostId, StringComparison.OrdinalIgnoreCase);
         }
 
         public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)
@@ -1778,7 +1779,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             {
                 var program = string.IsNullOrWhiteSpace(timer.ProgramId) ? null : _libraryManager.GetItemList(new InternalItemsQuery
                 {
-                    IncludeItemTypes = new[] { nameof(LiveTvProgram) },
+                    IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
                     Limit = 1,
                     ExternalId = timer.ProgramId,
                     DtoOptions = new DtoOptions(true)
@@ -2137,7 +2138,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         {
             var query = new InternalItemsQuery
             {
-                IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
+                IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
                 Limit = 1,
                 DtoOptions = new DtoOptions(true)
                 {
@@ -2332,7 +2333,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
                 var deletes = _timerProvider.GetAll()
                     .Where(i => string.Equals(i.SeriesTimerId, seriesTimer.Id, StringComparison.OrdinalIgnoreCase))
-                    .Where(i => !allTimerIds.Contains(i.Id, StringComparer.OrdinalIgnoreCase) && i.StartDate > DateTime.UtcNow)
+                    .Where(i => !allTimerIds.Contains(i.Id, StringComparison.OrdinalIgnoreCase) && i.StartDate > DateTime.UtcNow)
                     .Where(i => deleteStatuses.Contains(i.Status))
                     .ToList();
 
@@ -2352,7 +2353,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
             var query = new InternalItemsQuery
             {
-                IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
+                IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
                 ExternalSeriesId = seriesTimer.SeriesId,
                 DtoOptions = new DtoOptions(true)
                 {
@@ -2387,7 +2388,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                     channel = _libraryManager.GetItemList(
                         new InternalItemsQuery
                         {
-                            IncludeItemTypes = new string[] { nameof(LiveTvChannel) },
+                            IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel },
                             ItemIds = new[] { parent.ChannelId },
                             DtoOptions = new DtoOptions()
                         }).FirstOrDefault() as LiveTvChannel;
@@ -2446,7 +2447,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                     channel = _libraryManager.GetItemList(
                         new InternalItemsQuery
                         {
-                            IncludeItemTypes = new string[] { nameof(LiveTvChannel) },
+                            IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel },
                             ItemIds = new[] { programInfo.ChannelId },
                             DtoOptions = new DtoOptions()
                         }).FirstOrDefault() as LiveTvChannel;
@@ -2511,7 +2512,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 var seriesIds = _libraryManager.GetItemIds(
                     new InternalItemsQuery
                     {
-                        IncludeItemTypes = new[] { nameof(Series) },
+                        IncludeItemTypes = new[] { BaseItemKind.Series },
                         Name = program.Name
                     }).ToArray();
 
@@ -2524,7 +2525,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 {
                     var result = _libraryManager.GetItemIds(new InternalItemsQuery
                     {
-                        IncludeItemTypes = new[] { nameof(Episode) },
+                        IncludeItemTypes = new[] { BaseItemKind.Episode },
                         ParentIndexNumber = program.SeasonNumber.Value,
                         IndexNumber = program.EpisodeNumber.Value,
                         AncestorIds = seriesIds,
@@ -2621,7 +2622,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
                 if (newDevicesOnly)
                 {
-                    discoveredDevices = discoveredDevices.Where(d => !configuredDeviceIds.Contains(d.DeviceId, StringComparer.OrdinalIgnoreCase))
+                    discoveredDevices = discoveredDevices.Where(d => !configuredDeviceIds.Contains(d.DeviceId, StringComparison.OrdinalIgnoreCase))
                             .ToList();
                 }
 

+ 9 - 10
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -10,7 +10,6 @@ using System.Linq;
 using System.Net;
 using System.Net.Http;
 using System.Net.Http.Json;
-using System.Net.Http.Headers;
 using System.Net.Mime;
 using System.Security.Cryptography;
 using System.Text;
@@ -242,19 +241,19 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
             if (programInfo.AudioProperties.Count != 0)
             {
-                if (programInfo.AudioProperties.Contains("atmos", StringComparer.OrdinalIgnoreCase))
+                if (programInfo.AudioProperties.Contains("atmos", StringComparison.OrdinalIgnoreCase))
                 {
                     audioType = ProgramAudio.Atmos;
                 }
-                else if (programInfo.AudioProperties.Contains("dd 5.1", StringComparer.OrdinalIgnoreCase))
+                else if (programInfo.AudioProperties.Contains("dd 5.1", StringComparison.OrdinalIgnoreCase))
                 {
                     audioType = ProgramAudio.DolbyDigital;
                 }
-                else if (programInfo.AudioProperties.Contains("dd", StringComparer.OrdinalIgnoreCase))
+                else if (programInfo.AudioProperties.Contains("dd", StringComparison.OrdinalIgnoreCase))
                 {
                     audioType = ProgramAudio.DolbyDigital;
                 }
-                else if (programInfo.AudioProperties.Contains("stereo", StringComparer.OrdinalIgnoreCase))
+                else if (programInfo.AudioProperties.Contains("stereo", StringComparison.OrdinalIgnoreCase))
                 {
                     audioType = ProgramAudio.Stereo;
                 }
@@ -316,8 +315,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
             if (programInfo.VideoProperties != null)
             {
-                info.IsHD = programInfo.VideoProperties.Contains("hdtv", StringComparer.OrdinalIgnoreCase);
-                info.Is3D = programInfo.VideoProperties.Contains("3d", StringComparer.OrdinalIgnoreCase);
+                info.IsHD = programInfo.VideoProperties.Contains("hdtv", StringComparison.OrdinalIgnoreCase);
+                info.Is3D = programInfo.VideoProperties.Contains("3d", StringComparison.OrdinalIgnoreCase);
             }
 
             if (details.ContentRating != null && details.ContentRating.Count > 0)
@@ -326,7 +325,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                     .Replace("--", "-", StringComparison.Ordinal);
 
                 var invalid = new[] { "N/A", "Approved", "Not Rated", "Passed" };
-                if (invalid.Contains(info.OfficialRating, StringComparer.OrdinalIgnoreCase))
+                if (invalid.Contains(info.OfficialRating, StringComparison.OrdinalIgnoreCase))
                 {
                     info.OfficialRating = null;
                 }
@@ -388,9 +387,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             if (details.Genres != null)
             {
                 info.Genres = details.Genres.Where(g => !string.IsNullOrWhiteSpace(g)).ToList();
-                info.IsNews = details.Genres.Contains("news", StringComparer.OrdinalIgnoreCase);
+                info.IsNews = details.Genres.Contains("news", StringComparison.OrdinalIgnoreCase);
 
-                if (info.Genres.Contains("children", StringComparer.OrdinalIgnoreCase))
+                if (info.Genres.Contains("children", StringComparison.OrdinalIgnoreCase))
                 {
                     info.IsKids = true;
                 }

+ 4 - 4
Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs

@@ -190,10 +190,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 IsSeries = program.Episode != null,
                 IsRepeat = program.IsPreviouslyShown && !program.IsNew,
                 IsPremiere = program.Premiere != null,
-                IsKids = program.Categories.Any(c => info.KidsCategories.Contains(c, StringComparer.OrdinalIgnoreCase)),
-                IsMovie = program.Categories.Any(c => info.MovieCategories.Contains(c, StringComparer.OrdinalIgnoreCase)),
-                IsNews = program.Categories.Any(c => info.NewsCategories.Contains(c, StringComparer.OrdinalIgnoreCase)),
-                IsSports = program.Categories.Any(c => info.SportsCategories.Contains(c, StringComparer.OrdinalIgnoreCase)),
+                IsKids = program.Categories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
+                IsMovie = program.Categories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
+                IsNews = program.Categories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
+                IsSports = program.Categories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
                 ImageUrl = program.Icon != null && !string.IsNullOrEmpty(program.Icon.Source) ? program.Icon.Source : null,
                 HasImage = program.Icon != null && !string.IsNullOrEmpty(program.Icon.Source),
                 OfficialRating = program.Rating != null && !string.IsNullOrEmpty(program.Rating.Value) ? program.Rating.Value : null,

+ 6 - 6
Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs

@@ -7,12 +7,12 @@ using System.Globalization;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.Dto;
@@ -161,7 +161,7 @@ namespace Emby.Server.Implementations.LiveTv
         {
             var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new string[] { nameof(Series) },
+                IncludeItemTypes = new[] { BaseItemKind.Series },
                 Name = seriesName,
                 Limit = 1,
                 ImageTypes = new ImageType[] { ImageType.Thumb },
@@ -204,7 +204,7 @@ namespace Emby.Server.Implementations.LiveTv
 
             var program = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
+                IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
                 ExternalSeriesId = programSeriesId,
                 Limit = 1,
                 ImageTypes = new ImageType[] { ImageType.Primary },
@@ -255,7 +255,7 @@ namespace Emby.Server.Implementations.LiveTv
         {
             var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new string[] { nameof(Series) },
+                IncludeItemTypes = new[] { BaseItemKind.Series },
                 Name = seriesName,
                 Limit = 1,
                 ImageTypes = new ImageType[] { ImageType.Thumb },
@@ -298,7 +298,7 @@ namespace Emby.Server.Implementations.LiveTv
 
             var program = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new string[] { nameof(Series) },
+                IncludeItemTypes = new[] { BaseItemKind.Series },
                 Name = seriesName,
                 Limit = 1,
                 ImageTypes = new ImageType[] { ImageType.Primary },
@@ -309,7 +309,7 @@ namespace Emby.Server.Implementations.LiveTv
             {
                 program = _libraryManager.GetItemList(new InternalItemsQuery
                 {
-                    IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
+                    IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
                     ExternalSeriesId = programSeriesId,
                     Limit = 1,
                     ImageTypes = new ImageType[] { ImageType.Primary },

+ 14 - 16
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -33,8 +33,6 @@ using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Tasks;
 using Microsoft.Extensions.Logging;
-using Episode = MediaBrowser.Controller.Entities.TV.Episode;
-using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
 
 namespace Emby.Server.Implementations.LiveTv
 {
@@ -191,7 +189,7 @@ namespace Emby.Server.Implementations.LiveTv
                 IsKids = query.IsKids,
                 IsSports = query.IsSports,
                 IsSeries = query.IsSeries,
-                IncludeItemTypes = new[] { nameof(LiveTvChannel) },
+                IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel },
                 TopParentIds = new[] { topFolder.Id },
                 IsFavorite = query.IsFavorite,
                 IsLiked = query.IsLiked,
@@ -810,7 +808,7 @@ namespace Emby.Server.Implementations.LiveTv
 
             var internalQuery = new InternalItemsQuery(user)
             {
-                IncludeItemTypes = new[] { nameof(LiveTvProgram) },
+                IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
                 MinEndDate = query.MinEndDate,
                 MinStartDate = query.MinStartDate,
                 MaxEndDate = query.MaxEndDate,
@@ -874,7 +872,7 @@ namespace Emby.Server.Implementations.LiveTv
 
             var internalQuery = new InternalItemsQuery(user)
             {
-                IncludeItemTypes = new[] { nameof(LiveTvProgram) },
+                IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
                 IsAiring = query.IsAiring,
                 HasAired = query.HasAired,
                 IsNews = query.IsNews,
@@ -1085,8 +1083,8 @@ namespace Emby.Server.Implementations.LiveTv
 
             if (cleanDatabase)
             {
-                CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { nameof(LiveTvChannel) }, progress, cancellationToken);
-                CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { nameof(LiveTvProgram) }, progress, cancellationToken);
+                CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { BaseItemKind.LiveTvChannel }, progress, cancellationToken);
+                CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { BaseItemKind.LiveTvProgram }, progress, cancellationToken);
             }
 
             var coreService = _services.OfType<EmbyTV.EmbyTV>().FirstOrDefault();
@@ -1177,7 +1175,7 @@ namespace Emby.Server.Implementations.LiveTv
 
                     var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery
                     {
-                        IncludeItemTypes = new string[] { nameof(LiveTvProgram) },
+                        IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
                         ChannelIds = new Guid[] { currentChannel.Id },
                         DtoOptions = new DtoOptions(true)
                     }).Cast<LiveTvProgram>().ToDictionary(i => i.Id);
@@ -1261,7 +1259,7 @@ namespace Emby.Server.Implementations.LiveTv
             return new Tuple<List<Guid>, List<Guid>>(channels, programs);
         }
 
-        private void CleanDatabaseInternal(Guid[] currentIdList, string[] validTypes, IProgress<double> progress, CancellationToken cancellationToken)
+        private void CleanDatabaseInternal(Guid[] currentIdList, BaseItemKind[] validTypes, IProgress<double> progress, CancellationToken cancellationToken)
         {
             var list = _itemRepo.GetItemIdsList(new InternalItemsQuery
             {
@@ -1328,25 +1326,25 @@ namespace Emby.Server.Implementations.LiveTv
                 .Select(i => i.Id)
                 .ToList();
 
-            var excludeItemTypes = new List<string>();
+            var excludeItemTypes = new List<BaseItemKind>();
 
             if (folderIds.Count == 0)
             {
                 return new QueryResult<BaseItem>();
             }
 
-            var includeItemTypes = new List<string>();
+            var includeItemTypes = new List<BaseItemKind>();
             var genres = new List<string>();
 
             if (query.IsMovie.HasValue)
             {
                 if (query.IsMovie.Value)
                 {
-                    includeItemTypes.Add(nameof(Movie));
+                    includeItemTypes.Add(BaseItemKind.Movie);
                 }
                 else
                 {
-                    excludeItemTypes.Add(nameof(Movie));
+                    excludeItemTypes.Add(BaseItemKind.Movie);
                 }
             }
 
@@ -1354,11 +1352,11 @@ namespace Emby.Server.Implementations.LiveTv
             {
                 if (query.IsSeries.Value)
                 {
-                    includeItemTypes.Add(nameof(Episode));
+                    includeItemTypes.Add(BaseItemKind.Episode);
                 }
                 else
                 {
-                    excludeItemTypes.Add(nameof(Episode));
+                    excludeItemTypes.Add(BaseItemKind.Episode);
                 }
             }
 
@@ -1878,7 +1876,7 @@ namespace Emby.Server.Implementations.LiveTv
 
             var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user)
             {
-                IncludeItemTypes = new[] { nameof(LiveTvProgram) },
+                IncludeItemTypes = new[] { BaseItemKind.LiveTvProgram },
                 ChannelIds = channelIds,
                 MaxStartDate = now,
                 MinEndDate = now,

+ 0 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs

@@ -3,7 +3,6 @@
 #pragma warning disable CS1591
 
 using System;
-using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Net;

+ 2 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs

@@ -10,6 +10,7 @@ using System.Linq;
 using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
@@ -119,7 +120,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             {
                 var extension = Path.GetExtension(mediaSource.Path) ?? string.Empty;
 
-                if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
+                if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
                 {
                     return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
                 }

+ 45 - 1
Emby.Server.Implementations/Localization/Core/cy.json

@@ -10,5 +10,49 @@
     "AuthenticationSucceededWithUserName": "{0} wedi’i ddilysu’n llwyddiannus",
     "Artists": "Artistiaid",
     "AppDeviceValues": "Ap: {0}, Dyfais: {1}",
-    "Albums": "Albwmau"
+    "Albums": "Albwmau",
+    "Genres": "Genres",
+    "Folders": "Ffolderi",
+    "Favorites": "Ffefrynnau",
+    "LabelRunningTimeValue": "Amser rhedeg: {0}",
+    "TaskOptimizeDatabase": "Cronfa ddata Optimeiddio",
+    "TaskRefreshChannels": "Adnewyddu Sianeli",
+    "TaskRefreshPeople": "Adnewyddu Pobl",
+    "TasksChannelsCategory": "Sianeli Internet",
+    "VersionNumber": "Fersiwn {0}",
+    "ScheduledTaskStartedWithName": "{0} wedi dechrau",
+    "ScheduledTaskFailedWithName": "{0} wedi methu",
+    "ProviderValue": "Darparwr: {0}",
+    "NotificationOptionInstallationFailed": "Fethu Gosod",
+    "NameSeasonUnknown": "Tymor Anhysbys",
+    "NameSeasonNumber": "Tymor {0}",
+    "MusicVideos": "Fideos Cerddoriaeth",
+    "MixedContent": "Cynnwys amrywiol",
+    "HomeVideos": "Fideos Cartref",
+    "HeaderNextUp": "Nesaf i Fyny",
+    "HeaderFavoriteArtists": "Ffefryn Artistiaid",
+    "HeaderFavoriteAlbums": "Ffefryn Albwmau",
+    "HeaderContinueWatching": "Parhewch i Weithio",
+    "TasksApplicationCategory": "Rhaglen",
+    "TasksLibraryCategory": "Llyfrgell",
+    "TasksMaintenanceCategory": "Cynnal a Chadw",
+    "System": "System",
+    "Plugin": "Ategyn",
+    "Music": "Cerddoriaeth",
+    "Latest": "Diweddaraf",
+    "Inherit": "Etifeddu",
+    "Forced": "Orfodi",
+    "Application": "Rhaglen",
+    "HeaderAlbumArtists": "Artistiaid albwm",
+    "Sync": "Cysoni",
+    "Songs": "Caneuon",
+    "Shows": "Rhaglenni",
+    "Playlists": "Rhestri Chwarae",
+    "Photos": "Lluniau",
+    "ValueSpecialEpisodeName": "Arbennig - {0}",
+    "Movies": "Ffilmiau",
+    "Undefined": "Heb ddiffiniad",
+    "TvShows": "Rhaglenni teledu",
+    "HeaderLiveTV": "Teledu Byw",
+    "User": "Defnyddiwr"
 }

+ 3 - 1
Emby.Server.Implementations/Localization/Core/fil.json

@@ -117,5 +117,7 @@
     "TaskCleanActivityLogDescription": "Tanggalin ang mga tala ng aktibidad na mas luma sa nakatakda na edad.",
     "Default": "Default",
     "Undefined": "Hindi tiyak",
-    "Forced": "Sapilitan"
+    "Forced": "Sapilitan",
+    "TaskOptimizeDatabaseDescription": "Iko-compact ang database at ita-truncate ang free space. Ang pagpapatakbo ng gawaing ito pagkatapos ng pag-scan sa library o paggawa ng iba pang mga pagbabago na nagpapahiwatig ng mga pagbabago sa database ay maaaring magpa-improve ng performance.",
+    "TaskOptimizeDatabase": "I-optimize ang database"
 }

+ 3 - 3
Emby.Server.Implementations/Localization/Core/ms.json

@@ -2,7 +2,7 @@
     "Albums": "Album-album",
     "AppDeviceValues": "Apl: {0}, Peranti: {1}",
     "Application": "Aplikasi",
-    "Artists": "Artis",
+    "Artists": "Artis-artis",
     "AuthenticationSucceededWithUserName": "{0} berjaya disahkan",
     "Books": "Buku-buku",
     "CameraImageUploadedFrom": "Gambar baharu telah dimuat naik melalui {0}",
@@ -39,7 +39,7 @@
     "MixedContent": "Kandungan campuran",
     "Movies": "Filem-filem",
     "Music": "Muzik",
-    "MusicVideos": "",
+    "MusicVideos": "Video muzik",
     "NameInstallFailed": "{0} pemasangan gagal",
     "NameSeasonNumber": "Musim {0}",
     "NameSeasonUnknown": "Musim Tidak Diketahui",
@@ -75,7 +75,7 @@
     "StartupEmbyServerIsLoading": "Pelayan Jellyfin sedang dimuatkan. Sila cuba sebentar lagi.",
     "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
     "SubtitleDownloadFailureFromForItem": "Muat turun sarikata gagal dari {0} untuk {1}",
-    "Sync": "",
+    "Sync": "Segerak",
     "System": "Sistem",
     "TvShows": "Tayangan TV",
     "User": "Pengguna",

+ 1 - 1
Emby.Server.Implementations/Localization/Core/ta.json

@@ -21,7 +21,7 @@
     "Inherit": "மரபுரிமையாகப் பெறு",
     "HeaderRecordingGroups": "பதிவு குழுக்கள்",
     "Folders": "கோப்புறைகள்",
-    "FailedLoginAttemptWithUserName": "{0} இல் இருந்து உள்நுழைவு முயற்சி தோல்வியடைந்தது",
+    "FailedLoginAttemptWithUserName": "{0} இன் உள்நுழைவு முயற்சி தோல்வியடைந்தது",
     "DeviceOnlineWithName": "{0} இணைக்கப்பட்டது",
     "DeviceOfflineWithName": "{0} துண்டிக்கப்பட்டது",
     "Collections": "தொகுப்புகள்",

+ 1 - 1
Emby.Server.Implementations/Localization/LocalizationManager.cs

@@ -310,7 +310,7 @@ namespace Emby.Server.Implementations.Localization
 
             return _dictionaries.GetOrAdd(
                 culture,
-                (key, localizationManager) => localizationManager.GetDictionary(Prefix, key, DefaultCulture + ".json").GetAwaiter().GetResult(),
+                static (key, localizationManager) => localizationManager.GetDictionary(Prefix, key, DefaultCulture + ".json").GetAwaiter().GetResult(),
                 this);
         }
 

+ 3 - 2
Emby.Server.Implementations/MediaEncoder/EncodingManager.cs

@@ -9,6 +9,7 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Chapters;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -120,7 +121,7 @@ namespace Emby.Server.Implementations.MediaEncoder
 
                 var path = GetChapterImagePath(video, chapter.StartPositionTicks);
 
-                if (!currentImages.Contains(path, StringComparer.OrdinalIgnoreCase))
+                if (!currentImages.Contains(path, StringComparison.OrdinalIgnoreCase))
                 {
                     if (extractImages)
                     {
@@ -219,7 +220,7 @@ namespace Emby.Server.Implementations.MediaEncoder
         {
             var deadImages = images
                 .Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase)
-                .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i), StringComparer.OrdinalIgnoreCase))
+                .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
                 .ToList();
 
             foreach (var image in deadImages)

+ 2 - 1
Emby.Server.Implementations/Playlists/PlaylistsFolder.cs

@@ -4,6 +4,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Text.Json.Serialization;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Model.Querying;
@@ -45,7 +46,7 @@ namespace Emby.Server.Implementations.Playlists
             }
 
             query.Recursive = true;
-            query.IncludeItemTypes = new[] { "Playlist" };
+            query.IncludeItemTypes = new[] { BaseItemKind.Playlist };
             query.Parent = null;
             return LibraryManager.GetItemsResult(query);
         }

+ 2 - 1
Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs

@@ -4,6 +4,7 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Extensions;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
@@ -143,7 +144,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
 
                 var key = video.Path + video.DateModified.Ticks;
 
-                var extract = !previouslyFailedImages.Contains(key, StringComparer.OrdinalIgnoreCase);
+                var extract = !previouslyFailedImages.Contains(key, StringComparison.OrdinalIgnoreCase);
 
                 try
                 {

+ 1 - 1
Emby.Server.Implementations/Serialization/MyXmlSerializer.cs

@@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.Serialization
         private static XmlSerializer GetSerializer(Type type)
             => _serializers.GetOrAdd(
                 type.FullName ?? throw new ArgumentException($"Invalid type {type}."),
-                (_, t) => new XmlSerializer(t),
+                static (_, t) => new XmlSerializer(t),
                 type);
 
         /// <summary>

+ 1 - 1
Emby.Server.Implementations/Session/SessionManager.cs

@@ -1292,7 +1292,7 @@ namespace Emby.Server.Implementations.Session
                 {
                     ["ItemId"] = command.ItemId,
                     ["ItemName"] = command.ItemName,
-                    ["ItemType"] = command.ItemType
+                    ["ItemType"] = command.ItemType.ToString()
                 }
             };
 

+ 4 - 4
Emby.Server.Implementations/TV/TVSeriesManager.cs

@@ -116,7 +116,7 @@ namespace Emby.Server.Implementations.TV
                 .GetItemList(
                     new InternalItemsQuery(user)
                     {
-                        IncludeItemTypes = new[] { nameof(Episode) },
+                        IncludeItemTypes = new[] { BaseItemKind.Episode },
                         OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) },
                         SeriesPresentationUniqueKey = presentationUniqueKey,
                         Limit = limit,
@@ -191,7 +191,7 @@ namespace Emby.Server.Implementations.TV
             {
                 AncestorWithPresentationUniqueKey = null,
                 SeriesPresentationUniqueKey = seriesKey,
-                IncludeItemTypes = new[] { nameof(Episode) },
+                IncludeItemTypes = new[] { BaseItemKind.Episode },
                 OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Descending) },
                 IsPlayed = true,
                 Limit = 1,
@@ -209,7 +209,7 @@ namespace Emby.Server.Implementations.TV
                 {
                     AncestorWithPresentationUniqueKey = null,
                     SeriesPresentationUniqueKey = seriesKey,
-                    IncludeItemTypes = new[] { nameof(Episode) },
+                    IncludeItemTypes = new[] { BaseItemKind.Episode },
                     OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) },
                     Limit = 1,
                     IsPlayed = false,
@@ -226,7 +226,7 @@ namespace Emby.Server.Implementations.TV
                         AncestorWithPresentationUniqueKey = null,
                         SeriesPresentationUniqueKey = seriesKey,
                         ParentIndexNumber = 0,
-                        IncludeItemTypes = new[] { nameof(Episode) },
+                        IncludeItemTypes = new[] { BaseItemKind.Episode },
                         IsPlayed = false,
                         IsVirtualItem = false,
                         DtoOptions = dtoOptions

+ 3 - 6
Emby.Server.Implementations/Updates/InstallationManager.cs

@@ -2,6 +2,7 @@ using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.IO;
+using System.IO.Compression;
 using System.Linq;
 using System.Net.Http;
 using System.Net.Http.Json;
@@ -19,7 +20,6 @@ using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Events.Updates;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Plugins;
 using MediaBrowser.Model.Updates;
 using Microsoft.Extensions.Logging;
@@ -47,7 +47,6 @@ namespace Emby.Server.Implementations.Updates
         /// </summary>
         /// <value>The application host.</value>
         private readonly IServerApplicationHost _applicationHost;
-        private readonly IZipClient _zipClient;
         private readonly object _currentInstallationsLock = new object();
 
         /// <summary>
@@ -69,7 +68,6 @@ namespace Emby.Server.Implementations.Updates
         /// <param name="eventManager">The <see cref="IEventManager"/>.</param>
         /// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/>.</param>
         /// <param name="config">The <see cref="IServerConfigurationManager"/>.</param>
-        /// <param name="zipClient">The <see cref="IZipClient"/>.</param>
         /// <param name="pluginManager">The <see cref="IPluginManager"/>.</param>
         public InstallationManager(
             ILogger<InstallationManager> logger,
@@ -78,7 +76,6 @@ namespace Emby.Server.Implementations.Updates
             IEventManager eventManager,
             IHttpClientFactory httpClientFactory,
             IServerConfigurationManager config,
-            IZipClient zipClient,
             IPluginManager pluginManager)
         {
             _currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>();
@@ -90,7 +87,6 @@ namespace Emby.Server.Implementations.Updates
             _eventManager = eventManager;
             _httpClientFactory = httpClientFactory;
             _config = config;
-            _zipClient = zipClient;
             _jsonSerializerOptions = JsonDefaults.Options;
             _pluginManager = pluginManager;
         }
@@ -560,7 +556,8 @@ namespace Emby.Server.Implementations.Updates
             }
 
             stream.Position = 0;
-            _zipClient.ExtractAllFromZip(stream, targetDir, true);
+            using var reader = new ZipArchive(stream);
+            reader.ExtractToDirectory(targetDir, true);
             await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false);
             _pluginManager.ImportPluginFrom(targetDir);
         }

+ 4 - 4
Jellyfin.Api/Controllers/ArtistsController.cs

@@ -133,8 +133,8 @@ namespace Jellyfin.Api.Controllers
 
             var query = new InternalItemsQuery(user)
             {
-                ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
-                IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
+                ExcludeItemTypes = excludeItemTypes,
+                IncludeItemTypes = includeItemTypes,
                 MediaTypes = mediaTypes,
                 StartIndex = startIndex,
                 Limit = limit,
@@ -337,8 +337,8 @@ namespace Jellyfin.Api.Controllers
 
             var query = new InternalItemsQuery(user)
             {
-                ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
-                IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
+                ExcludeItemTypes = excludeItemTypes,
+                IncludeItemTypes = includeItemTypes,
                 MediaTypes = mediaTypes,
                 StartIndex = startIndex,
                 Limit = limit,

+ 2 - 3
Jellyfin.Api/Controllers/FilterController.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Linq;
 using Jellyfin.Api.Constants;
-using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
@@ -71,7 +70,7 @@ namespace Jellyfin.Api.Controllers
             {
                 User = user,
                 MediaTypes = mediaTypes,
-                IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
+                IncludeItemTypes = includeItemTypes,
                 Recursive = true,
                 EnableTotalRecordCount = false,
                 DtoOptions = new DtoOptions
@@ -166,7 +165,7 @@ namespace Jellyfin.Api.Controllers
             var filters = new QueryFilters();
             var genreQuery = new InternalItemsQuery(user)
             {
-                IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
+                IncludeItemTypes = includeItemTypes,
                 DtoOptions = new DtoOptions
                 {
                     Fields = Array.Empty<ItemFields>(),

+ 7 - 7
Jellyfin.Api/Controllers/GenresController.cs

@@ -101,8 +101,8 @@ namespace Jellyfin.Api.Controllers
 
             var query = new InternalItemsQuery(user)
             {
-                ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
-                IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
+                ExcludeItemTypes = excludeItemTypes,
+                IncludeItemTypes = includeItemTypes,
                 StartIndex = startIndex,
                 Limit = limit,
                 IsFavorite = isFavorite,
@@ -160,7 +160,7 @@ namespace Jellyfin.Api.Controllers
             Genre item = new Genre();
             if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1)
             {
-                var result = GetItemFromSlugName<Genre>(_libraryManager, genreName, dtoOptions);
+                var result = GetItemFromSlugName<Genre>(_libraryManager, genreName, dtoOptions, BaseItemKind.Genre);
 
                 if (result != null)
                 {
@@ -182,27 +182,27 @@ namespace Jellyfin.Api.Controllers
             return _dtoService.GetBaseItemDto(item, dtoOptions);
         }
 
-        private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
+        private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions, BaseItemKind baseItemKind)
             where T : BaseItem, new()
         {
             var result = libraryManager.GetItemList(new InternalItemsQuery
             {
                 Name = name.Replace(BaseItem.SlugChar, '&'),
-                IncludeItemTypes = new[] { typeof(T).Name },
+                IncludeItemTypes = new[] { baseItemKind },
                 DtoOptions = dtoOptions
             }).OfType<T>().FirstOrDefault();
 
             result ??= libraryManager.GetItemList(new InternalItemsQuery
             {
                 Name = name.Replace(BaseItem.SlugChar, '/'),
-                IncludeItemTypes = new[] { typeof(T).Name },
+                IncludeItemTypes = new[] { baseItemKind },
                 DtoOptions = dtoOptions
             }).OfType<T>().FirstOrDefault();
 
             result ??= libraryManager.GetItemList(new InternalItemsQuery
             {
                 Name = name.Replace(BaseItem.SlugChar, '?'),
-                IncludeItemTypes = new[] { typeof(T).Name },
+                IncludeItemTypes = new[] { baseItemKind },
                 DtoOptions = dtoOptions
             }).OfType<T>().FirstOrDefault();
 

+ 6 - 7
Jellyfin.Api/Controllers/ItemsController.cs

@@ -8,7 +8,6 @@ using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Dto;
@@ -296,8 +295,8 @@ namespace Jellyfin.Api.Controllers
                 {
                     IsPlayed = isPlayed,
                     MediaTypes = mediaTypes,
-                    IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
-                    ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
+                    IncludeItemTypes = includeItemTypes,
+                    ExcludeItemTypes = excludeItemTypes,
                     Recursive = recursive ?? false,
                     OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
                     IsFavorite = isFavorite,
@@ -459,7 +458,7 @@ namespace Jellyfin.Api.Controllers
                 {
                     query.AlbumIds = albums.SelectMany(i =>
                     {
-                        return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { nameof(MusicAlbum) }, Name = i, Limit = 1 });
+                        return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { BaseItemKind.MusicAlbum }, Name = i, Limit = 1 });
                     }).ToArray();
                 }
 
@@ -483,7 +482,7 @@ namespace Jellyfin.Api.Controllers
                 if (query.OrderBy.Count == 0)
                 {
                     // Albums by artist
-                    if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], "MusicAlbum", StringComparison.OrdinalIgnoreCase))
+                    if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.MusicAlbum)
                     {
                         query.OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.ProductionYear, SortOrder.Descending), new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) };
                     }
@@ -831,8 +830,8 @@ namespace Jellyfin.Api.Controllers
                 CollapseBoxSetItems = false,
                 EnableTotalRecordCount = enableTotalRecordCount,
                 AncestorIds = ancestorIds,
-                IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
-                ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
+                IncludeItemTypes = includeItemTypes,
+                ExcludeItemTypes = excludeItemTypes,
                 SearchTerm = searchTerm,
                 ExcludeItemIds = excludeItemIds
             });

+ 26 - 25
Jellyfin.Api/Controllers/LibraryController.cs

@@ -14,6 +14,8 @@ using Jellyfin.Api.Extensions;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.Models.LibraryDtos;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
@@ -22,7 +24,6 @@ using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Activity;
@@ -36,7 +37,6 @@ using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.Logging;
-using Book = MediaBrowser.Controller.Entities.Book;
 
 namespace Jellyfin.Api.Controllers
 {
@@ -413,14 +413,14 @@ namespace Jellyfin.Api.Controllers
 
             var counts = new ItemCounts
             {
-                AlbumCount = GetCount(typeof(MusicAlbum), user, isFavorite),
-                EpisodeCount = GetCount(typeof(Episode), user, isFavorite),
-                MovieCount = GetCount(typeof(Movie), user, isFavorite),
-                SeriesCount = GetCount(typeof(Series), user, isFavorite),
-                SongCount = GetCount(typeof(Audio), user, isFavorite),
-                MusicVideoCount = GetCount(typeof(MusicVideo), user, isFavorite),
-                BoxSetCount = GetCount(typeof(BoxSet), user, isFavorite),
-                BookCount = GetCount(typeof(Book), user, isFavorite)
+                AlbumCount = GetCount(BaseItemKind.MusicAlbum, user, isFavorite),
+                EpisodeCount = GetCount(BaseItemKind.Episode, user, isFavorite),
+                MovieCount = GetCount(BaseItemKind.Movie, user, isFavorite),
+                SeriesCount = GetCount(BaseItemKind.Series, user, isFavorite),
+                SongCount = GetCount(BaseItemKind.Audio, user, isFavorite),
+                MusicVideoCount = GetCount(BaseItemKind.MusicVideo, user, isFavorite),
+                BoxSetCount = GetCount(BaseItemKind.BoxSet, user, isFavorite),
+                BookCount = GetCount(BaseItemKind.Book, user, isFavorite)
             };
 
             return counts;
@@ -529,7 +529,7 @@ namespace Jellyfin.Api.Controllers
         {
             var series = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new[] { nameof(Series) },
+                IncludeItemTypes = new[] { BaseItemKind.Series },
                 DtoOptions = new DtoOptions(false)
                 {
                     EnableImages = false
@@ -559,7 +559,7 @@ namespace Jellyfin.Api.Controllers
         {
             var movies = _libraryManager.GetItemList(new InternalItemsQuery
             {
-                IncludeItemTypes = new[] { nameof(Movie) },
+                IncludeItemTypes = new[] { BaseItemKind.Movie },
                 DtoOptions = new DtoOptions(false)
                 {
                     EnableImages = false
@@ -715,30 +715,31 @@ namespace Jellyfin.Api.Controllers
             bool? isMovie = item is Movie || (program != null && program.IsMovie) || item is Trailer;
             bool? isSeries = item is Series || (program != null && program.IsSeries);
 
-            var includeItemTypes = new List<string>();
+            var includeItemTypes = new List<BaseItemKind>();
             if (isMovie.Value)
             {
-                includeItemTypes.Add(nameof(Movie));
+                includeItemTypes.Add(BaseItemKind.Movie);
                 if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
                 {
-                    includeItemTypes.Add(nameof(Trailer));
-                    includeItemTypes.Add(nameof(LiveTvProgram));
+                    includeItemTypes.Add(BaseItemKind.Trailer);
+                    includeItemTypes.Add(BaseItemKind.LiveTvProgram);
                 }
             }
             else if (isSeries.Value)
             {
-                includeItemTypes.Add(nameof(Series));
+                includeItemTypes.Add(BaseItemKind.Series);
             }
             else
             {
                 // For non series and movie types these columns are typically null
                 isSeries = null;
                 isMovie = null;
-                includeItemTypes.Add(item.GetType().Name);
+                includeItemTypes.Add(item.GetBaseItemKind());
             }
 
             var query = new InternalItemsQuery(user)
             {
+                Genres = item.Genres,
                 Limit = limit,
                 IncludeItemTypes = includeItemTypes.ToArray(),
                 SimilarTo = item,
@@ -785,7 +786,7 @@ namespace Jellyfin.Api.Controllers
             var typesList = types.ToList();
 
             var plugins = _providerManager.GetAllMetadataPlugins()
-                .Where(i => types.Contains(i.ItemType, StringComparer.OrdinalIgnoreCase))
+                .Where(i => types.Contains(i.ItemType, StringComparison.OrdinalIgnoreCase))
                 .OrderBy(i => typesList.IndexOf(i.ItemType))
                 .ToList();
 
@@ -871,11 +872,11 @@ namespace Jellyfin.Api.Controllers
             return result;
         }
 
-        private int GetCount(Type type, User? user, bool? isFavorite)
+        private int GetCount(BaseItemKind itemKind, User? user, bool? isFavorite)
         {
             var query = new InternalItemsQuery(user)
             {
-                IncludeItemTypes = new[] { type.Name },
+                IncludeItemTypes = new[] { itemKind },
                 Limit = 0,
                 Recursive = true,
                 IsVirtualItem = false,
@@ -940,10 +941,10 @@ namespace Jellyfin.Api.Controllers
             }
 
             var metadataOptions = _serverConfigurationManager.Configuration.MetadataOptions
-                .Where(i => itemTypes.Contains(i.ItemType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+                .Where(i => itemTypes.Contains(i.ItemType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
                 .ToArray();
 
-            return metadataOptions.Length == 0 || metadataOptions.Any(i => !i.DisabledMetadataSavers.Contains(name, StringComparer.OrdinalIgnoreCase));
+            return metadataOptions.Length == 0 || metadataOptions.Any(i => !i.DisabledMetadataSavers.Contains(name, StringComparison.OrdinalIgnoreCase));
         }
 
         private bool IsMetadataFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
@@ -967,7 +968,7 @@ namespace Jellyfin.Api.Controllers
                 .ToArray();
 
             return metadataOptions.Length == 0
-               || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase));
+               || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase));
         }
 
         private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary)
@@ -997,7 +998,7 @@ namespace Jellyfin.Api.Controllers
                 return true;
             }
 
-            return metadataOptions.Any(i => !i.DisabledImageFetchers.Contains(name, StringComparer.OrdinalIgnoreCase));
+            return metadataOptions.Any(i => !i.DisabledImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase));
         }
     }
 }

+ 13 - 15
Jellyfin.Api/Controllers/MoviesController.cs

@@ -11,9 +11,7 @@ using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
@@ -84,7 +82,7 @@ namespace Jellyfin.Api.Controllers
             {
                 IncludeItemTypes = new[]
                 {
-                    nameof(Movie),
+                    BaseItemKind.Movie,
                     // nameof(Trailer),
                     // nameof(LiveTvProgram)
                 },
@@ -99,11 +97,11 @@ namespace Jellyfin.Api.Controllers
 
             var recentlyPlayedMovies = _libraryManager.GetItemList(query);
 
-            var itemTypes = new List<string> { nameof(Movie) };
+            var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
             if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
             {
-                itemTypes.Add(nameof(Trailer));
-                itemTypes.Add(nameof(LiveTvProgram));
+                itemTypes.Add(BaseItemKind.Trailer);
+                itemTypes.Add(BaseItemKind.LiveTvProgram);
             }
 
             var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user)
@@ -182,11 +180,11 @@ namespace Jellyfin.Api.Controllers
             DtoOptions dtoOptions,
             RecommendationType type)
         {
-            var itemTypes = new List<string> { nameof(Movie) };
+            var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
             if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
             {
-                itemTypes.Add(nameof(Trailer));
-                itemTypes.Add(nameof(LiveTvProgram));
+                itemTypes.Add(BaseItemKind.Trailer);
+                itemTypes.Add(BaseItemKind.LiveTvProgram);
             }
 
             foreach (var name in names)
@@ -224,11 +222,11 @@ namespace Jellyfin.Api.Controllers
 
         private IEnumerable<RecommendationDto> GetWithActor(User? user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
         {
-            var itemTypes = new List<string> { nameof(Movie) };
+            var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
             if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
             {
-                itemTypes.Add(nameof(Trailer));
-                itemTypes.Add(nameof(LiveTvProgram));
+                itemTypes.Add(BaseItemKind.Trailer);
+                itemTypes.Add(BaseItemKind.LiveTvProgram);
             }
 
             foreach (var name in names)
@@ -264,11 +262,11 @@ namespace Jellyfin.Api.Controllers
 
         private IEnumerable<RecommendationDto> GetSimilarTo(User? user, IEnumerable<BaseItem> baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
         {
-            var itemTypes = new List<string> { nameof(Movie) };
+            var itemTypes = new List<BaseItemKind> { BaseItemKind.Movie };
             if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
             {
-                itemTypes.Add(nameof(Trailer));
-                itemTypes.Add(nameof(LiveTvProgram));
+                itemTypes.Add(BaseItemKind.Trailer);
+                itemTypes.Add(BaseItemKind.LiveTvProgram);
             }
 
             foreach (var item in baselineItems)

+ 7 - 7
Jellyfin.Api/Controllers/MusicGenresController.cs

@@ -101,8 +101,8 @@ namespace Jellyfin.Api.Controllers
 
             var query = new InternalItemsQuery(user)
             {
-                ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
-                IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
+                ExcludeItemTypes = excludeItemTypes,
+                IncludeItemTypes = includeItemTypes,
                 StartIndex = startIndex,
                 Limit = limit,
                 IsFavorite = isFavorite,
@@ -149,7 +149,7 @@ namespace Jellyfin.Api.Controllers
 
             if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1)
             {
-                item = GetItemFromSlugName<MusicGenre>(_libraryManager, genreName, dtoOptions);
+                item = GetItemFromSlugName<MusicGenre>(_libraryManager, genreName, dtoOptions, BaseItemKind.MusicGenre);
             }
             else
             {
@@ -166,27 +166,27 @@ namespace Jellyfin.Api.Controllers
             return _dtoService.GetBaseItemDto(item, dtoOptions);
         }
 
-        private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
+        private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions, BaseItemKind baseItemKind)
             where T : BaseItem, new()
         {
             var result = libraryManager.GetItemList(new InternalItemsQuery
             {
                 Name = name.Replace(BaseItem.SlugChar, '&'),
-                IncludeItemTypes = new[] { typeof(T).Name },
+                IncludeItemTypes = new[] { baseItemKind },
                 DtoOptions = dtoOptions
             }).OfType<T>().FirstOrDefault();
 
             result ??= libraryManager.GetItemList(new InternalItemsQuery
             {
                 Name = name.Replace(BaseItem.SlugChar, '/'),
-                IncludeItemTypes = new[] { typeof(T).Name },
+                IncludeItemTypes = new[] { baseItemKind },
                 DtoOptions = dtoOptions
             }).OfType<T>().FirstOrDefault();
 
             result ??= libraryManager.GetItemList(new InternalItemsQuery
             {
                 Name = name.Replace(BaseItem.SlugChar, '?'),
-                IncludeItemTypes = new[] { typeof(T).Name },
+                IncludeItemTypes = new[] { baseItemKind },
                 DtoOptions = dtoOptions
             }).OfType<T>().FirstOrDefault();
 

+ 0 - 1
Jellyfin.Api/Controllers/PluginsController.cs

@@ -9,7 +9,6 @@ using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Models.PluginDtos;
 using Jellyfin.Extensions.Json;
-using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Updates;
 using MediaBrowser.Model.Net;

+ 0 - 5
Jellyfin.Api/Controllers/RemoteImageController.cs

@@ -3,18 +3,13 @@ using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.IO;
 using System.Linq;
-using System.Net.Http;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
-using Jellyfin.Extensions;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Providers;
 using Microsoft.AspNetCore.Authorization;
 using Microsoft.AspNetCore.Http;

+ 2 - 3
Jellyfin.Api/Controllers/SearchController.cs

@@ -4,7 +4,6 @@ using System.ComponentModel.DataAnnotations;
 using System.Globalization;
 using System.Linq;
 using Jellyfin.Api.Constants;
-using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Drawing;
@@ -110,8 +109,8 @@ namespace Jellyfin.Api.Controllers
                 IncludeStudios = includeStudios,
                 StartIndex = startIndex,
                 UserId = userId ?? Guid.Empty,
-                IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
-                ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
+                IncludeItemTypes = includeItemTypes,
+                ExcludeItemTypes = excludeItemTypes,
                 MediaTypes = mediaTypes,
                 ParentId = parentId,
 

+ 1 - 1
Jellyfin.Api/Controllers/SessionController.cs

@@ -127,7 +127,7 @@ namespace Jellyfin.Api.Controllers
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> DisplayContent(
             [FromRoute, Required] string sessionId,
-            [FromQuery, Required] string itemType,
+            [FromQuery, Required] BaseItemKind itemType,
             [FromQuery, Required] string itemId,
             [FromQuery, Required] string itemName)
         {

+ 2 - 2
Jellyfin.Api/Controllers/StudiosController.cs

@@ -97,8 +97,8 @@ namespace Jellyfin.Api.Controllers
 
             var query = new InternalItemsQuery(user)
             {
-                ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
-                IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
+                ExcludeItemTypes = excludeItemTypes,
+                IncludeItemTypes = includeItemTypes,
                 StartIndex = startIndex,
                 Limit = limit,
                 IsFavorite = isFavorite,

+ 1 - 1
Jellyfin.Api/Controllers/SuggestionsController.cs

@@ -58,7 +58,7 @@ namespace Jellyfin.Api.Controllers
         public ActionResult<QueryResult<BaseItemDto>> GetSuggestions(
             [FromRoute, Required] Guid userId,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaType,
-            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] type,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] type,
             [FromQuery] int? startIndex,
             [FromQuery] int? limit,
             [FromQuery] bool enableTotalRecordCount = false)

+ 0 - 1
Jellyfin.Api/Controllers/SystemController.cs

@@ -3,7 +3,6 @@ using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.IO;
 using System.Linq;
-using System.Net;
 using System.Net.Mime;
 using System.Threading.Tasks;
 using Jellyfin.Api.Attributes;

+ 7 - 7
Jellyfin.Api/Controllers/TvShowsController.cs

@@ -61,7 +61,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="seriesId">Optional. Filter by series id.</param>
         /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
-        /// <param name="enableImges">Optional. Include image information in output.</param>
+        /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
@@ -78,7 +78,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] string? seriesId,
             [FromQuery] Guid? parentId,
-            [FromQuery] bool? enableImges,
+            [FromQuery] bool? enableImages,
             [FromQuery] int? imageTypeLimit,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
             [FromQuery] bool? enableUserData,
@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
         {
             var options = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
-                .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes);
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 
             var result = _tvSeriesManager.GetNextUp(
                 new NextUpQuery
@@ -125,7 +125,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
         /// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
-        /// <param name="enableImges">Optional. Include image information in output.</param>
+        /// <param name="enableImages">Optional. Include image information in output.</param>
         /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
         /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
         /// <param name="enableUserData">Optional. Include user data.</param>
@@ -138,7 +138,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? limit,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
             [FromQuery] Guid? parentId,
-            [FromQuery] bool? enableImges,
+            [FromQuery] bool? enableImages,
             [FromQuery] int? imageTypeLimit,
             [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
             [FromQuery] bool? enableUserData)
@@ -153,11 +153,11 @@ namespace Jellyfin.Api.Controllers
 
             var options = new DtoOptions { Fields = fields }
                 .AddClientFields(Request)
-                .AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes);
+                .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 
             var itemsResult = _libraryManager.GetItemList(new InternalItemsQuery(user)
             {
-                IncludeItemTypes = new[] { nameof(Episode) },
+                IncludeItemTypes = new[] { BaseItemKind.Episode },
                 OrderBy = new[] { (ItemSortBy.PremiereDate, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) },
                 MinPremiereDate = minPremiereDate,
                 StartIndex = startIndex,

+ 2 - 3
Jellyfin.Api/Controllers/UserLibraryController.cs

@@ -6,7 +6,6 @@ using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
-using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Enums;
 using Jellyfin.Extensions;
@@ -213,7 +212,7 @@ namespace Jellyfin.Api.Controllers
 
             if (item is IHasTrailers hasTrailers)
             {
-                var trailers = hasTrailers.GetTrailers();
+                var trailers = hasTrailers.LocalTrailers;
                 var dtosTrailers = _dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item);
                 var allTrailers = new BaseItemDto[dtosExtras.Length + dtosTrailers.Count];
                 dtosExtras.CopyTo(allTrailers, 0);
@@ -297,7 +296,7 @@ namespace Jellyfin.Api.Controllers
                 new LatestItemsQuery
                 {
                     GroupItems = groupItems,
-                    IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
+                    IncludeItemTypes = includeItemTypes,
                     IsPlayed = isPlayed,
                     Limit = limit,
                     ParentId = parentId ?? Guid.Empty,

+ 4 - 3
Jellyfin.Api/Controllers/YearsController.cs

@@ -8,6 +8,7 @@ using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -101,8 +102,8 @@ namespace Jellyfin.Api.Controllers
 
             var query = new InternalItemsQuery(user)
             {
-                ExcludeItemTypes = RequestHelpers.GetItemTypeStrings(excludeItemTypes),
-                IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
+                ExcludeItemTypes = excludeItemTypes,
+                IncludeItemTypes = includeItemTypes,
                 MediaTypes = mediaTypes,
                 DtoOptions = dtoOptions
             };
@@ -209,7 +210,7 @@ namespace Jellyfin.Api.Controllers
             }
 
             // Include MediaTypes
-            if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+            if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
             {
                 return false;
             }

+ 0 - 16
Jellyfin.Api/Helpers/RequestHelpers.cs

@@ -137,21 +137,5 @@ namespace Jellyfin.Api.Helpers
                 TotalRecordCount = result.TotalRecordCount
             };
         }
-
-        internal static string[] GetItemTypeStrings(IReadOnlyList<BaseItemKind> itemKinds)
-        {
-            if (itemKinds.Count == 0)
-            {
-                return Array.Empty<string>();
-            }
-
-            var itemTypes = new string[itemKinds.Count];
-            for (var i = 0; i < itemKinds.Count; i++)
-            {
-                itemTypes[i] = itemKinds[i].ToString();
-            }
-
-            return itemTypes;
-        }
     }
 }

+ 1 - 1
Jellyfin.Api/Jellyfin.Api.csproj

@@ -13,7 +13,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.0" />
+    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.1" />
     <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
     <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
     <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.2.3" />

+ 2 - 1
Jellyfin.Server.Implementations/Devices/DeviceManager.cs

@@ -7,6 +7,7 @@ using Jellyfin.Data.Entities.Security;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Events;
 using Jellyfin.Data.Queries;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Devices;
@@ -219,7 +220,7 @@ namespace Jellyfin.Server.Implementations.Devices
                 return true;
             }
 
-            return user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparer.OrdinalIgnoreCase)
+            return user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparison.OrdinalIgnoreCase)
                    || !GetCapabilities(deviceId).SupportsPersistentIdentifier;
         }
 

+ 5 - 5
Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj

@@ -18,14 +18,14 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="System.Linq.Async" Version="5.0.0" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.0" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0">
+    <PackageReference Include="System.Linq.Async" Version="5.1.0" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.1" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.0">
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.1">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>

+ 2 - 2
Jellyfin.Server/Jellyfin.Server.csproj

@@ -33,8 +33,8 @@
     <PackageReference Include="CommandLineParser" Version="2.8.0" />
     <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.0" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
-    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.0" />
-    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.1" />
+    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.1" />
     <PackageReference Include="prometheus-net" Version="5.0.2" />
     <PackageReference Include="prometheus-net.AspNetCore" Version="5.0.2" />
     <PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />

+ 7 - 2
Jellyfin.Server/Migrations/MigrationRunner.cs

@@ -75,10 +75,15 @@ namespace Jellyfin.Server.Migrations
 
             var xmlSerializer = new MyXmlSerializer();
             var migrationConfigPath = Path.Join(appPaths.ConfigurationDirectoryPath, MigrationsListStore.StoreKey.ToLowerInvariant() + ".xml");
-            var migrationOptions = (MigrationOptions)xmlSerializer.DeserializeFromFile(typeof(MigrationOptions), migrationConfigPath)!;
+            var migrationOptions = File.Exists(migrationConfigPath)
+                 ? (MigrationOptions)xmlSerializer.DeserializeFromFile(typeof(MigrationOptions), migrationConfigPath)!
+                 : new MigrationOptions();
 
             // We have to deserialize it manually since the configuration manager may overwrite it
-            var serverConfig = (ServerConfiguration)xmlSerializer.DeserializeFromFile(typeof(ServerConfiguration), appPaths.SystemConfigurationFilePath)!;
+            var serverConfig = File.Exists(appPaths.SystemConfigurationFilePath)
+                ? (ServerConfiguration)xmlSerializer.DeserializeFromFile(typeof(ServerConfiguration), appPaths.SystemConfigurationFilePath)!
+                : new ServerConfiguration();
+
             HandleStartupWizardCondition(migrations, migrationOptions, serverConfig.IsStartupWizardCompleted, logger);
             PerformMigrations(migrations, migrationOptions, options => xmlSerializer.SerializeToFile(options, migrationConfigPath), logger);
         }

+ 0 - 10
MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs

@@ -55,11 +55,6 @@ namespace MediaBrowser.Controller.BaseItemManager
                 return typeOptions.MetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
             }
 
-            if (!libraryOptions.EnableInternetProviders)
-            {
-                return false;
-            }
-
             var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, baseItem.GetType().Name, StringComparison.OrdinalIgnoreCase));
 
             return itemConfig == null || !itemConfig.DisabledMetadataFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
@@ -86,11 +81,6 @@ namespace MediaBrowser.Controller.BaseItemManager
                 return typeOptions.ImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);
             }
 
-            if (!libraryOptions.EnableInternetProviders)
-            {
-                return false;
-            }
-
             var itemConfig = _serverConfigurationManager.Configuration.MetadataOptions.FirstOrDefault(i => string.Equals(i.ItemType, baseItem.GetType().Name, StringComparison.OrdinalIgnoreCase));
 
             return itemConfig == null || !itemConfig.DisabledImageFetchers.Contains(name.AsSpan(), StringComparison.OrdinalIgnoreCase);

+ 0 - 2
MediaBrowser.Controller/Entities/Audio/Audio.cs

@@ -8,10 +8,8 @@ using System.Globalization;
 using System.Linq;
 using System.Text.Json.Serialization;
 using Jellyfin.Data.Enums;
-using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
 
 namespace MediaBrowser.Controller.Entities.Audio
 {

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels