Просмотр исходного кода

Merge branch 'master' into what_could_go_wrong

Claus Vium 3 лет назад
Родитель
Сommit
9a0618552b
96 измененных файлов с 1036 добавлено и 770 удалено
  1. 20 0
      .ci/azure-pipelines-package.yml
  2. 1 0
      CONTRIBUTORS.md
  3. 23 23
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  4. 1 1
      Emby.Dlna/Main/DlnaEntryPoint.cs
  5. 0 18
      Emby.Server.Implementations/ApplicationHost.cs
  6. 1 1
      Emby.Server.Implementations/Channels/ChannelManager.cs
  7. 2 1
      Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
  8. 61 78
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  9. 1 1
      Emby.Server.Implementations/Dto/DtoService.cs
  10. 8 8
      Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
  11. 2 1
      Emby.Server.Implementations/Images/DynamicImageProvider.cs
  12. 1 1
      Emby.Server.Implementations/Images/GenreImageProvider.cs
  13. 3 3
      Emby.Server.Implementations/Images/MusicGenreImageProvider.cs
  14. 1 1
      Emby.Server.Implementations/Library/LibraryManager.cs
  15. 2 2
      Emby.Server.Implementations/Library/MusicManager.cs
  16. 22 8
      Emby.Server.Implementations/Library/PathExtensions.cs
  17. 1 1
      Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
  18. 3 3
      Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
  19. 33 4
      Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
  20. 23 23
      Emby.Server.Implementations/Library/SearchEngine.cs
  21. 8 8
      Emby.Server.Implementations/Library/UserViewManager.cs
  22. 2 1
      Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
  23. 2 2
      Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
  24. 2 1
      Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
  25. 2 1
      Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
  26. 7 7
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  27. 6 5
      Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
  28. 14 14
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  29. 1 1
      Emby.Server.Implementations/Localization/Core/ar.json
  30. 1 1
      Emby.Server.Implementations/Localization/Core/eo.json
  31. 2 1
      Emby.Server.Implementations/Localization/Core/fa.json
  32. 1 1
      Emby.Server.Implementations/Localization/Core/hr.json
  33. 1 1
      Emby.Server.Implementations/Localization/Core/ja.json
  34. 1 1
      Emby.Server.Implementations/Localization/Core/lt-LT.json
  35. 4 1
      Emby.Server.Implementations/Localization/Core/mk.json
  36. 5 5
      Emby.Server.Implementations/Localization/Core/ms.json
  37. 1 1
      Emby.Server.Implementations/Localization/Core/ro.json
  38. 2 2
      Emby.Server.Implementations/Localization/Core/sv.json
  39. 29 1
      Emby.Server.Implementations/Localization/Core/zu.json
  40. 2 1
      Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
  41. 1 1
      Emby.Server.Implementations/Session/SessionManager.cs
  42. 4 4
      Emby.Server.Implementations/TV/TVSeriesManager.cs
  43. 4 4
      Jellyfin.Api/Controllers/ArtistsController.cs
  44. 2 2
      Jellyfin.Api/Controllers/FilterController.cs
  45. 7 7
      Jellyfin.Api/Controllers/GenresController.cs
  46. 6 6
      Jellyfin.Api/Controllers/ItemsController.cs
  47. 19 18
      Jellyfin.Api/Controllers/LibraryController.cs
  48. 13 13
      Jellyfin.Api/Controllers/MoviesController.cs
  49. 7 7
      Jellyfin.Api/Controllers/MusicGenresController.cs
  50. 2 2
      Jellyfin.Api/Controllers/SearchController.cs
  51. 1 1
      Jellyfin.Api/Controllers/SessionController.cs
  52. 1 1
      Jellyfin.Api/Controllers/StartupController.cs
  53. 2 2
      Jellyfin.Api/Controllers/StudiosController.cs
  54. 1 1
      Jellyfin.Api/Controllers/SuggestionsController.cs
  55. 1 1
      Jellyfin.Api/Controllers/TvShowsController.cs
  56. 1 1
      Jellyfin.Api/Controllers/UserLibraryController.cs
  57. 2 2
      Jellyfin.Api/Controllers/YearsController.cs
  58. 0 71
      Jellyfin.Api/Helpers/ClassMigrationHelper.cs
  59. 0 16
      Jellyfin.Api/Helpers/RequestHelpers.cs
  60. 1 5
      Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs
  61. 24 0
      Jellyfin.Networking/Configuration/NetworkConfigurationStore.cs
  62. 1 1
      Jellyfin.Networking/Manager/NetworkManager.cs
  63. 1 1
      Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
  64. 58 7
      Jellyfin.Server/Migrations/MigrationRunner.cs
  65. 138 0
      Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs
  66. 1 0
      Jellyfin.Server/Program.cs
  67. 1 1
      MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
  68. 2 1
      MediaBrowser.Controller/Entities/Audio/MusicGenre.cs
  69. 3 1
      MediaBrowser.Controller/Entities/BaseItem.cs
  70. 3 3
      MediaBrowser.Controller/Entities/Folder.cs
  71. 5 5
      MediaBrowser.Controller/Entities/Genre.cs
  72. 6 6
      MediaBrowser.Controller/Entities/InternalItemsQuery.cs
  73. 6 6
      MediaBrowser.Controller/Entities/TV/Series.cs
  74. 20 20
      MediaBrowser.Controller/Entities/UserViewBuilder.cs
  75. 7 0
      MediaBrowser.Controller/Entities/Video.cs
  76. 23 4
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  77. 2 2
      MediaBrowser.Controller/Playlists/Playlist.cs
  78. 18 5
      MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
  79. 0 222
      MediaBrowser.Model/Configuration/ServerConfiguration.cs
  80. 2 2
      MediaBrowser.Model/Net/MimeTypes.cs
  81. 2 1
      MediaBrowser.Model/Querying/LatestItemsQuery.cs
  82. 5 4
      MediaBrowser.Model/Search/SearchQuery.cs
  83. 3 2
      MediaBrowser.Model/Session/BrowseRequest.cs
  84. 2 1
      MediaBrowser.Providers/Manager/ProviderManager.cs
  85. 176 0
      MediaBrowser.Providers/MediaInfo/AudioResolver.cs
  86. 17 5
      MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs
  87. 31 1
      MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
  88. 2 1
      MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
  89. 13 9
      bump_version
  90. 14 1
      debian/jellyfin.service
  91. 37 22
      fedora/Makefile
  92. 4 0
      fedora/jellyfin-server-lowports.conf
  93. 19 1
      fedora/jellyfin.spec
  94. 0 30
      tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs
  95. 1 6
      tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs
  96. 12 0
      tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs

+ 20 - 0
.ci/azure-pipelines-package.yml

@@ -39,6 +39,14 @@ jobs:
     vmImage: 'ubuntu-latest'
 
   steps:
+  - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
+    displayName: Set release version (stable)
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+
+  - script: './bump-version $(JellyfinVersion)'
+    displayName: Bump internal version (stable)
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+
   - script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
     displayName: 'Build Dockerfile'
 
@@ -80,6 +88,14 @@ jobs:
     vmImage: 'ubuntu-latest'
 
   steps:
+  - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
+    displayName: Set release version (stable)
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+
+  - script: './bump-version $(JellyfinVersion)'
+    displayName: Bump internal version (stable)
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+
   - task: DownloadPipelineArtifact@2
     displayName: 'Download OpenAPI Spec'
     inputs:
@@ -127,6 +143,10 @@ jobs:
     displayName: Set release version (stable)
     condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
 
+  - script: './bump-version $(JellyfinVersion)'
+    displayName: Bump internal version (stable)
+    condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+
   - task: Docker@2
     displayName: 'Push Unstable Image'
     condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')

+ 1 - 0
CONTRIBUTORS.md

@@ -150,6 +150,7 @@
  - [ianjazz246](https://github.com/ianjazz246)
  - [peterspenler](https://github.com/peterspenler)
  - [MBR-0001](https://github.com/MBR-0001)
+ - [jonas-resch](https://github.com/jonas-resch)
 
 # Emby Contributors
 

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

@@ -539,7 +539,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 +619,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 +644,7 @@ namespace Emby.Dlna.ContentDirectory
             {
                 StartIndex = startIndex,
                 Limit = limit,
-                IncludeItemTypes = new[] { nameof(LiveTvChannel) },
+                IncludeItemTypes = new[] { BaseItemKind.LiveTvChannel },
                 OrderBy = GetOrderBy(sort, false)
             };
 
@@ -675,23 +675,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 +746,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 +831,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 +898,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 +913,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 +1013,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 +1052,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 +1086,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 +1115,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 +1144,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(),

+ 1 - 1
Emby.Dlna/Main/DlnaEntryPoint.cs

@@ -124,7 +124,7 @@ namespace Emby.Dlna.Main
                 config);
             Current = this;
 
-            var netConfig = config.GetConfiguration<NetworkConfiguration>("network");
+            var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
             _disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
 
             if (_disabled && _config.GetDlnaConfiguration().EnableServer)

+ 0 - 18
Emby.Server.Implementations/ApplicationHost.cs

@@ -313,22 +313,6 @@ namespace Emby.Server.Implementations
                 ? Environment.MachineName
                 : ConfigurationManager.Configuration.ServerName;
 
-        /// <summary>
-        /// Temporary function to migration network settings out of system.xml and into network.xml.
-        /// TODO: remove at the point when a fixed migration path has been decided upon.
-        /// </summary>
-        private void MigrateNetworkConfiguration()
-        {
-            string path = Path.Combine(ConfigurationManager.CommonApplicationPaths.ConfigurationDirectoryPath, "network.xml");
-            if (!File.Exists(path))
-            {
-                var networkSettings = new NetworkConfiguration();
-                ClassMigrationHelper.CopyProperties(ConfigurationManager.Configuration, networkSettings);
-                _xmlSerializer.SerializeToFile(networkSettings, path);
-                Logger.LogDebug("Successfully migrated network settings.");
-            }
-        }
-
         public string ExpandVirtualPath(string path)
         {
             var appPaths = ApplicationPaths;
@@ -513,8 +497,6 @@ namespace Emby.Server.Implementations
 
             ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
 
-            // Have to migrate settings here as migration subsystem not yet initialised.
-            MigrateNetworkConfiguration();
             NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
 
             // Initialize runtime stat collection

+ 1 - 1
Emby.Server.Implementations/Channels/ChannelManager.cs

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

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

+ 61 - 78
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -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 =
@@ -2212,7 +2211,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 +2226,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 +2241,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 +2261,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 +2271,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 +2291,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;
             }
@@ -3487,8 +3486,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,15 +3562,15 @@ 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]);
+                    statement?.TryBind("@type", excludeTypes[0].ToString());
                 }
                 else if (excludeTypes.Length > 1)
                 {
@@ -3582,7 +3581,7 @@ namespace Emby.Server.Implementations.Data
             else if (includeTypes.Length == 1)
             {
                 whereClauses.Add("type=@type");
-                statement?.TryBind("@type", includeTypes[0]);
+                statement?.TryBind("@type", includeTypes[0].ToString());
             }
             else if (includeTypes.Length > 1)
             {
@@ -3911,7 +3910,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 +4760,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 +4788,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 +4835,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 +4889,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)
@@ -5569,7 +5552,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();
 

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

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

+ 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);

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

@@ -6,6 +6,7 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
@@ -41,7 +42,7 @@ namespace Emby.Server.Implementations.Images
                 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 - 1
Emby.Server.Implementations/Images/GenreImageProvider.cs

@@ -43,7 +43,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,

+ 1 - 1
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -963,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>()

+ 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;

+ 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");
 

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

@@ -362,9 +362,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");
@@ -378,7 +378,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))
                     {

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

@@ -185,13 +185,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 - 23
Emby.Server.Implementations/Library/SearchEngine.cs

@@ -59,9 +59,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 +86,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 +183,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 +192,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

+ 8 - 8
Emby.Server.Implementations/Library/UserViewManager.cs

@@ -300,11 +300,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 +344,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
             });

+ 7 - 7
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -1778,7 +1778,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 +2137,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)
                 {
@@ -2352,7 +2352,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 +2387,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 +2446,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 +2511,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 +2524,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,

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

@@ -7,6 +7,7 @@ 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;
@@ -161,7 +162,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 +205,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 +256,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 +299,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 +310,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 - 14
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -191,7 +191,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 +810,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 +874,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 +1085,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 +1177,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 +1261,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 +1328,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 +1354,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 +1878,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,

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

@@ -15,7 +15,7 @@
     "Favorites": "مفضلات",
     "Folders": "المجلدات",
     "Genres": "التضنيفات",
-    "HeaderAlbumArtists": "ألبوم الفنان",
+    "HeaderAlbumArtists": "فناني الألبوم",
     "HeaderContinueWatching": "استمر بالمشاهدة",
     "HeaderFavoriteAlbums": "الألبومات المفضلة",
     "HeaderFavoriteArtists": "الفنانون المفضلون",

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

@@ -104,7 +104,7 @@
     "TaskRefreshChannelsDescription": "Refreŝigas informon pri interretaj kanaloj.",
     "TaskDownloadMissingSubtitles": "Elŝuti mankantajn subtekstojn",
     "TaskCleanTranscode": "Malplenigi Transkodadan Katalogon",
-    "TaskRefreshChapterImages": "Eltiri Ĉapitraj Bildojn",
+    "TaskRefreshChapterImages": "Eltiri Ĉapitrajn Bildojn",
     "TaskCleanCache": "Malplenigi Staplan Katalogon",
     "TaskCleanActivityLog": "Malplenigi Aktivecan Ĵurnalon",
     "PluginUpdatedWithName": "{0} estis ĝisdatigita",

+ 2 - 1
Emby.Server.Implementations/Localization/Core/fa.json

@@ -118,5 +118,6 @@
     "Default": "پیشفرض",
     "TaskCleanActivityLogDescription": "ورودی‌های قدیمی‌تر از سن تنظیم شده در سیاهه فعالیت را حذف می‌کند.",
     "TaskCleanActivityLog": "پاکسازی سیاهه فعالیت",
-    "Undefined": "تعریف نشده"
+    "Undefined": "تعریف نشده",
+    "TaskOptimizeDatabase": "بهینه سازی پایگاه داده"
 }

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

@@ -15,7 +15,7 @@
     "Favorites": "Favoriti",
     "Folders": "Mape",
     "Genres": "Žanrovi",
-    "HeaderAlbumArtists": "Album od izvođača",
+    "HeaderAlbumArtists": "Izvođači albuma",
     "HeaderContinueWatching": "Nastavi gledati",
     "HeaderFavoriteAlbums": "Omiljeni albumi",
     "HeaderFavoriteArtists": "Omiljeni izvođači",

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

@@ -16,7 +16,7 @@
     "Folders": "フォルダー",
     "Genres": "ジャンル",
     "HeaderAlbumArtists": "アルバムアーティスト",
-    "HeaderContinueWatching": "視聴を続ける",
+    "HeaderContinueWatching": "続きを見る",
     "HeaderFavoriteAlbums": "お気に入りのアルバム",
     "HeaderFavoriteArtists": "お気に入りのアーティスト",
     "HeaderFavoriteEpisodes": "お気に入りのエピソード",

+ 1 - 1
Emby.Server.Implementations/Localization/Core/lt-LT.json

@@ -1,7 +1,7 @@
 {
     "Albums": "Albumai",
     "AppDeviceValues": "Programa: {0}, Įrenginys: {1}",
-    "Application": "Programa",
+    "Application": "Programėlė",
     "Artists": "Atlikėjai",
     "AuthenticationSucceededWithUserName": "{0} sėkmingai autentifikuota",
     "Books": "Knygos",

+ 4 - 1
Emby.Server.Implementations/Localization/Core/mk.json

@@ -97,5 +97,8 @@
     "TasksChannelsCategory": "Интернет Канали",
     "TasksApplicationCategory": "Апликација",
     "TasksLibraryCategory": "Библиотека",
-    "TasksMaintenanceCategory": "Одржување"
+    "TasksMaintenanceCategory": "Одржување",
+    "Undefined": "Недефинирано",
+    "Forced": "Принудно",
+    "Default": "Зададено"
 }

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

@@ -55,7 +55,7 @@
     "NotificationOptionPluginInstalled": "Plugin telah dipasang",
     "NotificationOptionPluginUninstalled": "Plugin telah dinyahpasang",
     "NotificationOptionPluginUpdateInstalled": "Kemaskini plugin telah dipasang",
-    "NotificationOptionServerRestartRequired": "Server restart required",
+    "NotificationOptionServerRestartRequired": "",
     "NotificationOptionTaskFailed": "Kegagalan tugas berjadual",
     "NotificationOptionUserLockedOut": "Pengguna telah dikunci",
     "NotificationOptionVideoPlayback": "Ulangmain video bermula",
@@ -66,19 +66,19 @@
     "PluginInstalledWithName": "{0} telah dipasang",
     "PluginUninstalledWithName": "{0} telah dinyahpasang",
     "PluginUpdatedWithName": "{0} telah dikemaskini",
-    "ProviderValue": "Provider: {0}",
+    "ProviderValue": "Pembekal: {0}",
     "ScheduledTaskFailedWithName": "{0} gagal",
     "ScheduledTaskStartedWithName": "{0} bermula",
     "ServerNameNeedsToBeRestarted": "{0} perlu di ulangmula",
-    "Shows": "Series",
+    "Shows": "Tayangan",
     "Songs": "Lagu-lagu",
     "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",
+    "Sync": "",
     "System": "Sistem",
     "TvShows": "Tayangan TV",
-    "User": "User",
+    "User": "Pengguna",
     "UserCreatedWithName": "Pengguna {0} telah diwujudkan",
     "UserDeletedWithName": "Pengguna {0} telah dipadamkan",
     "UserDownloadingItemWithValues": "{0} sedang memuat turun {1}",

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

@@ -74,7 +74,7 @@
     "HeaderFavoriteArtists": "Artiști Favoriți",
     "HeaderFavoriteAlbums": "Albume Favorite",
     "HeaderContinueWatching": "Vizionează în continuare",
-    "HeaderAlbumArtists": "Album Artiști",
+    "HeaderAlbumArtists": "Albume Artiști",
     "Genres": "Genuri",
     "Folders": "Dosare",
     "Favorites": "Favorite",

+ 2 - 2
Emby.Server.Implementations/Localization/Core/sv.json

@@ -96,8 +96,8 @@
     "TaskDownloadMissingSubtitles": "Ladda ned saknade undertexter",
     "TaskRefreshChannelsDescription": "Uppdaterar information för internetkanaler.",
     "TaskRefreshChannels": "Uppdatera kanaler",
-    "TaskCleanTranscodeDescription": "Raderar transkodningsfiler som är mer än en dag gamla.",
-    "TaskCleanTranscode": "Töm transkodningskatalog",
+    "TaskCleanTranscodeDescription": "Raderar omkodningsfiler som är mer än en dag gamla.",
+    "TaskCleanTranscode": "Töm omkodningskatalog",
     "TaskUpdatePluginsDescription": "Laddar ned och installerar uppdateringar till insticksprogram som är konfigurerade att uppdateras automatiskt.",
     "TaskUpdatePlugins": "Uppdatera insticksprogram",
     "TaskRefreshPeopleDescription": "Uppdaterar metadata för skådespelare och regissörer i ditt mediabibliotek.",

+ 29 - 1
Emby.Server.Implementations/Localization/Core/zu.json

@@ -1 +1,29 @@
-{}
+{
+    "TasksApplicationCategory": "Ukusetshenziswa",
+    "TasksLibraryCategory": "Umtapo",
+    "TasksMaintenanceCategory": "Ukunakekela",
+    "User": "Umsebenzisi",
+    "Undefined": "Akuchaziwe",
+    "System": "Isistimu",
+    "Sync": "Vumelanisa",
+    "Songs": "Amaculo",
+    "Shows": "Izinhlelo",
+    "Plugin": "Isijobelelo",
+    "Playlists": "Izinhla Zokudlalayo",
+    "Photos": "Izithombe",
+    "Music": "Umculo",
+    "Movies": "Amamuvi",
+    "Latest": "lwakamuva",
+    "Inherit": "Ngefa",
+    "Forced": "Kuphoqiwe",
+    "Application": "Ukusetshenziswa",
+    "Genres": "Izinhlobo",
+    "Folders": "Izikhwama",
+    "Favorites": "Izintandokazi",
+    "Default": "Okumisiwe",
+    "Collections": "Amaqoqo",
+    "Channels": "Amashaneli",
+    "Books": "Izincwadi",
+    "Artists": "Abadlali",
+    "Albums": "Ama-albhamu"
+}

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

+ 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

+ 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 - 2
Jellyfin.Api/Controllers/FilterController.cs

@@ -71,7 +71,7 @@ namespace Jellyfin.Api.Controllers
             {
                 User = user,
                 MediaTypes = mediaTypes,
-                IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
+                IncludeItemTypes = includeItemTypes,
                 Recursive = true,
                 EnableTotalRecordCount = false,
                 DtoOptions = new DtoOptions
@@ -166,7 +166,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 - 6
Jellyfin.Api/Controllers/ItemsController.cs

@@ -296,8 +296,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 +459,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 +483,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 +831,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
             });

+ 19 - 18
Jellyfin.Api/Controllers/LibraryController.cs

@@ -14,6 +14,7 @@ using Jellyfin.Api.Extensions;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.Models.LibraryDtos;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Progress;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
@@ -413,14 +414,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 +530,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 +560,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,26 +716,26 @@ 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)
@@ -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,

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

@@ -84,7 +84,7 @@ namespace Jellyfin.Api.Controllers
             {
                 IncludeItemTypes = new[]
                 {
-                    nameof(Movie),
+                    BaseItemKind.Movie,
                     // nameof(Trailer),
                     // nameof(LiveTvProgram)
                 },
@@ -99,11 +99,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 +182,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 +224,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 +264,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();
 

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

@@ -110,8 +110,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)
         {

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

@@ -93,7 +93,7 @@ namespace Jellyfin.Api.Controllers
             NetworkConfiguration settings = _config.GetNetworkConfiguration();
             settings.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess;
             settings.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping;
-            _config.SaveConfiguration("network", settings);
+            _config.SaveConfiguration(NetworkConfigurationStore.StoreKey, settings);
             return NoContent();
         }
 

+ 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)

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

@@ -157,7 +157,7 @@ namespace Jellyfin.Api.Controllers
 
             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,

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

@@ -297,7 +297,7 @@ namespace Jellyfin.Api.Controllers
                 new LatestItemsQuery
                 {
                     GroupItems = groupItems,
-                    IncludeItemTypes = RequestHelpers.GetItemTypeStrings(includeItemTypes),
+                    IncludeItemTypes = includeItemTypes,
                     IsPlayed = isPlayed,
                     Limit = limit,
                     ParentId = parentId ?? Guid.Empty,

+ 2 - 2
Jellyfin.Api/Controllers/YearsController.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,
                 MediaTypes = mediaTypes,
                 DtoOptions = dtoOptions
             };

+ 0 - 71
Jellyfin.Api/Helpers/ClassMigrationHelper.cs

@@ -1,71 +0,0 @@
-using System;
-using System.Reflection;
-
-namespace Jellyfin.Api.Helpers
-{
-    /// <summary>
-    /// A static class for copying matching properties from one object to another.
-    /// TODO: remove at the point when a fixed migration path has been decided upon.
-    /// </summary>
-    public static class ClassMigrationHelper
-    {
-        /// <summary>
-        /// Extension for 'Object' that copies the properties to a destination object.
-        /// </summary>
-        /// <param name="source">The source.</param>
-        /// <param name="destination">The destination.</param>
-        public static void CopyProperties(this object source, object destination)
-        {
-            // If any this null throw an exception.
-            if (source == null || destination == null)
-            {
-                throw new ArgumentException("Source or/and Destination Objects are null");
-            }
-
-            // Getting the Types of the objects.
-            Type typeDest = destination.GetType();
-            Type typeSrc = source.GetType();
-
-            // Iterate the Properties of the source instance and populate them from their destination counterparts.
-            PropertyInfo[] srcProps = typeSrc.GetProperties();
-            foreach (PropertyInfo srcProp in srcProps)
-            {
-                if (!srcProp.CanRead)
-                {
-                    continue;
-                }
-
-                var targetProperty = typeDest.GetProperty(srcProp.Name);
-                if (targetProperty == null)
-                {
-                    continue;
-                }
-
-                if (!targetProperty.CanWrite)
-                {
-                    continue;
-                }
-
-                var obj = targetProperty.GetSetMethod(true);
-                if (obj != null && obj.IsPrivate)
-                {
-                    continue;
-                }
-
-                var target = targetProperty.GetSetMethod();
-                if (target != null && (target.Attributes & MethodAttributes.Static) != 0)
-                {
-                    continue;
-                }
-
-                if (!targetProperty.PropertyType.IsAssignableFrom(srcProp.PropertyType))
-                {
-                    continue;
-                }
-
-                // Passed all tests, lets set the value.
-                targetProperty.SetValue(destination, srcProp.GetValue(source, null), null);
-            }
-        }
-    }
-}

+ 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 - 5
Jellyfin.Networking/Configuration/NetworkConfigurationFactory.cs

@@ -16,11 +16,7 @@ namespace Jellyfin.Networking.Configuration
         {
             return new[]
             {
-                new ConfigurationStore
-                {
-                    Key = "network",
-                    ConfigurationType = typeof(NetworkConfiguration)
-                }
+                new NetworkConfigurationStore()
             };
         }
     }

+ 24 - 0
Jellyfin.Networking/Configuration/NetworkConfigurationStore.cs

@@ -0,0 +1,24 @@
+using MediaBrowser.Common.Configuration;
+
+namespace Jellyfin.Networking.Configuration
+{
+    /// <summary>
+    /// A configuration that stores network related settings.
+    /// </summary>
+    public class NetworkConfigurationStore : ConfigurationStore
+    {
+        /// <summary>
+        /// The name of the configuration in the storage.
+        /// </summary>
+        public const string StoreKey = "network";
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="NetworkConfigurationStore"/> class.
+        /// </summary>
+        public NetworkConfigurationStore()
+        {
+            ConfigurationType = typeof(NetworkConfiguration);
+            Key = StoreKey;
+        }
+    }
+}

+ 1 - 1
Jellyfin.Networking/Manager/NetworkManager.cs

@@ -727,7 +727,7 @@ namespace Jellyfin.Networking.Manager
 
         private void ConfigurationUpdated(object? sender, ConfigurationUpdateEventArgs evt)
         {
-            if (evt.Key.Equals("network", StringComparison.Ordinal))
+            if (evt.Key.Equals(NetworkConfigurationStore.StoreKey, StringComparison.Ordinal))
             {
                 UpdateSettings((NetworkConfiguration)evt.NewConfiguration);
             }

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

@@ -18,7 +18,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="System.Linq.Async" Version="5.0.0" />
+    <PackageReference Include="System.Linq.Async" Version="5.1.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">

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

@@ -1,6 +1,11 @@
 using System;
+using System.Collections.Generic;
+using System.IO;
 using System.Linq;
+using Emby.Server.Implementations;
+using Emby.Server.Implementations.Serialization;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 
@@ -11,6 +16,14 @@ namespace Jellyfin.Server.Migrations
     /// </summary>
     public sealed class MigrationRunner
     {
+        /// <summary>
+        /// The list of known pre-startup migrations, in order of applicability.
+        /// </summary>
+        private static readonly Type[] _preStartupMigrationTypes =
+        {
+            typeof(PreStartupRoutines.CreateNetworkConfiguration)
+        };
+
         /// <summary>
         /// The list of known migrations, in order of applicability.
         /// </summary>
@@ -41,17 +54,55 @@ namespace Jellyfin.Server.Migrations
                 .Select(m => ActivatorUtilities.CreateInstance(host.ServiceProvider, m))
                 .OfType<IMigrationRoutine>()
                 .ToArray();
+
             var migrationOptions = host.ConfigurationManager.GetConfiguration<MigrationOptions>(MigrationsListStore.StoreKey);
+            HandleStartupWizardCondition(migrations, migrationOptions, host.ConfigurationManager.Configuration.IsStartupWizardCompleted, logger);
+            PerformMigrations(migrations, migrationOptions, options => host.ConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, options), logger);
+        }
 
-            if (!host.ConfigurationManager.Configuration.IsStartupWizardCompleted && migrationOptions.Applied.Count == 0)
+        /// <summary>
+        /// Run all needed pre-startup migrations.
+        /// </summary>
+        /// <param name="appPaths">Application paths.</param>
+        /// <param name="loggerFactory">Factory for making the logger.</param>
+        public static void RunPreStartup(ServerApplicationPaths appPaths, ILoggerFactory loggerFactory)
+        {
+            var logger = loggerFactory.CreateLogger<MigrationRunner>();
+            var migrations = _preStartupMigrationTypes
+                .Select(m => Activator.CreateInstance(m, appPaths, loggerFactory))
+                .OfType<IMigrationRoutine>()
+                .ToArray();
+
+            var xmlSerializer = new MyXmlSerializer();
+            var migrationConfigPath = Path.Join(appPaths.ConfigurationDirectoryPath, MigrationsListStore.StoreKey.ToLowerInvariant() + ".xml");
+            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 = 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);
+        }
+
+        private static void HandleStartupWizardCondition(IEnumerable<IMigrationRoutine> migrations, MigrationOptions migrationOptions, bool isStartWizardCompleted, ILogger logger)
+        {
+            if (isStartWizardCompleted || migrationOptions.Applied.Count != 0)
             {
-                // If startup wizard is not finished, this is a fresh install.
-                // Don't run any migrations, just mark all of them as applied.
-                logger.LogInformation("Marking all known migrations as applied because this is a fresh install");
-                migrationOptions.Applied.AddRange(migrations.Where(m => !m.PerformOnNewInstall).Select(m => (m.Id, m.Name)));
-                host.ConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions);
+                return;
             }
 
+            // If startup wizard is not finished, this is a fresh install.
+            var onlyOldInstalls = migrations.Where(m => !m.PerformOnNewInstall).ToArray();
+            logger.LogInformation("Marking following migrations as applied because this is a fresh install: {@OnlyOldInstalls}", onlyOldInstalls.Select(m => m.Name));
+            migrationOptions.Applied.AddRange(onlyOldInstalls.Select(m => (m.Id, m.Name)));
+        }
+
+        private static void PerformMigrations(IMigrationRoutine[] migrations, MigrationOptions migrationOptions, Action<MigrationOptions> saveConfiguration, ILogger logger)
+        {
             var appliedMigrationIds = migrationOptions.Applied.Select(m => m.Id).ToHashSet();
 
             for (var i = 0; i < migrations.Length; i++)
@@ -78,7 +129,7 @@ namespace Jellyfin.Server.Migrations
                 // Mark the migration as completed
                 logger.LogInformation("Migration '{Name}' applied successfully", migrationRoutine.Name);
                 migrationOptions.Applied.Add((migrationRoutine.Id, migrationRoutine.Name));
-                host.ConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions);
+                saveConfiguration(migrationOptions);
                 logger.LogDebug("Migration '{Name}' marked as applied in configuration.", migrationRoutine.Name);
             }
         }

+ 138 - 0
Jellyfin.Server/Migrations/PreStartupRoutines/CreateNetworkConfiguration.cs

@@ -0,0 +1,138 @@
+using System;
+using System.IO;
+using System.Xml;
+using System.Xml.Serialization;
+using Emby.Server.Implementations;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Migrations.PreStartupRoutines;
+
+/// <inheritdoc />
+public class CreateNetworkConfiguration : IMigrationRoutine
+{
+    private readonly ServerApplicationPaths _applicationPaths;
+    private readonly ILogger<CreateNetworkConfiguration> _logger;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="CreateNetworkConfiguration"/> class.
+    /// </summary>
+    /// <param name="applicationPaths">An instance of <see cref="ServerApplicationPaths"/>.</param>
+    /// <param name="loggerFactory">An instance of the <see cref="ILoggerFactory"/> interface.</param>
+    public CreateNetworkConfiguration(ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory)
+    {
+        _applicationPaths = applicationPaths;
+        _logger = loggerFactory.CreateLogger<CreateNetworkConfiguration>();
+    }
+
+    /// <inheritdoc />
+    public Guid Id => Guid.Parse("9B354818-94D5-4B68-AC49-E35CB85F9D84");
+
+    /// <inheritdoc />
+    public string Name => nameof(CreateNetworkConfiguration);
+
+    /// <inheritdoc />
+    public bool PerformOnNewInstall => false;
+
+    /// <inheritdoc />
+    public void Perform()
+    {
+        string path = Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "network.xml");
+        if (File.Exists(path))
+        {
+            _logger.LogDebug("Network configuration file already exists, skipping");
+            return;
+        }
+
+        var serverConfigSerializer = new XmlSerializer(typeof(OldNetworkConfiguration), new XmlRootAttribute("ServerConfiguration"));
+        using var xmlReader = XmlReader.Create(_applicationPaths.SystemConfigurationFilePath);
+        var networkSettings = serverConfigSerializer.Deserialize(xmlReader);
+
+        var networkConfigSerializer = new XmlSerializer(typeof(OldNetworkConfiguration), new XmlRootAttribute("NetworkConfiguration"));
+        var xmlWriterSettings = new XmlWriterSettings { Indent = true };
+        using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings);
+        networkConfigSerializer.Serialize(xmlWriter, networkSettings);
+    }
+
+#pragma warning disable CS1591
+    public sealed class OldNetworkConfiguration
+    {
+        public const int DefaultHttpPort = 8096;
+
+        public const int DefaultHttpsPort = 8920;
+
+        private string _baseUrl = string.Empty;
+
+        public bool RequireHttps { get; set; }
+
+        public string CertificatePath { get; set; } = string.Empty;
+
+        public string CertificatePassword { get; set; } = string.Empty;
+
+        public string BaseUrl
+        {
+            get => _baseUrl;
+            set
+            {
+                // Normalize the start of the string
+                if (string.IsNullOrWhiteSpace(value))
+                {
+                    // If baseUrl is empty, set an empty prefix string
+                    _baseUrl = string.Empty;
+                    return;
+                }
+
+                if (value[0] != '/')
+                {
+                    // If baseUrl was not configured with a leading slash, append one for consistency
+                    value = "/" + value;
+                }
+
+                // Normalize the end of the string
+                if (value[^1] == '/')
+                {
+                    // If baseUrl was configured with a trailing slash, remove it for consistency
+                    value = value.Remove(value.Length - 1);
+                }
+
+                _baseUrl = value;
+            }
+        }
+
+        public int PublicHttpsPort { get; set; } = DefaultHttpsPort;
+
+        public int HttpServerPortNumber { get; set; } = DefaultHttpPort;
+
+        public int HttpsPortNumber { get; set; } = DefaultHttpsPort;
+
+        public bool EnableHttps { get; set; }
+
+        public int PublicPort { get; set; } = DefaultHttpPort;
+
+        public bool EnableIPV6 { get; set; }
+
+        public bool EnableIPV4 { get; set; } = true;
+
+        public bool IgnoreVirtualInterfaces { get; set; } = true;
+
+        public string VirtualInterfaceNames { get; set; } = "vEthernet*";
+
+        public bool TrustAllIP6Interfaces { get; set; }
+
+        public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty<string>();
+
+        public string[] RemoteIPFilter { get; set; } = Array.Empty<string>();
+
+        public bool IsRemoteIPFilterBlacklist { get; set; }
+
+        public bool EnableUPnP { get; set; }
+
+        public bool EnableRemoteAccess { get; set; } = true;
+
+        public string[] LocalNetworkSubnets { get; set; } = Array.Empty<string>();
+
+        public string[] LocalNetworkAddresses { get; set; } = Array.Empty<string>();
+
+        public string[] KnownProxies { get; set; } = Array.Empty<string>();
+    }
+#pragma warning restore CS1591
+}

+ 1 - 0
Jellyfin.Server/Program.cs

@@ -175,6 +175,7 @@ namespace Jellyfin.Server
             }
 
             PerformStaticInitialization();
+            Migrations.MigrationRunner.RunPreStartup(appPaths, _loggerFactory);
 
             var appHost = new CoreAppHost(
                 appPaths,

+ 1 - 1
MediaBrowser.Controller/Entities/Audio/MusicArtist.cs

@@ -88,7 +88,7 @@ namespace MediaBrowser.Controller.Entities.Audio
         {
             if (query.IncludeItemTypes.Length == 0)
             {
-                query.IncludeItemTypes = new[] { nameof(Audio), nameof(MusicVideo), nameof(MusicAlbum) };
+                query.IncludeItemTypes = new[] { BaseItemKind.Audio, BaseItemKind.MusicVideo, BaseItemKind.MusicAlbum };
                 query.ArtistIds = new[] { Id };
             }
 

+ 2 - 1
MediaBrowser.Controller/Entities/Audio/MusicGenre.cs

@@ -6,6 +6,7 @@ using System;
 using System.Collections.Generic;
 using System.Text.Json.Serialization;
 using Diacritics.Extensions;
+using Jellyfin.Data.Enums;
 using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Controller.Entities.Audio
@@ -66,7 +67,7 @@ namespace MediaBrowser.Controller.Entities.Audio
         public IList<BaseItem> GetTaggedItems(InternalItemsQuery query)
         {
             query.GenreIds = new[] { Id };
-            query.IncludeItemTypes = new[] { nameof(MusicVideo), nameof(Audio), nameof(MusicAlbum), nameof(MusicArtist) };
+            query.IncludeItemTypes = new[] { BaseItemKind.MusicVideo, BaseItemKind.Audio, BaseItemKind.MusicAlbum, BaseItemKind.MusicArtist };
 
             return LibraryManager.GetItemList(query);
         }

+ 3 - 1
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -40,6 +40,8 @@ namespace MediaBrowser.Controller.Entities
     /// </summary>
     public abstract class BaseItem : IHasProviderIds, IHasLookupInfo<ItemLookupInfo>, IEquatable<BaseItem>
     {
+        private BaseItemKind? _baseItemKind;
+
         public const string TrailerFileName = "trailer";
         public const string TrailersFolderName = "trailers";
         public const string ThemeSongsFolderName = "theme-music";
@@ -1808,7 +1810,7 @@ namespace MediaBrowser.Controller.Entities
 
         public BaseItemKind GetBaseItemKind()
         {
-            return Enum.Parse<BaseItemKind>(GetClientTypeName());
+            return _baseItemKind ??= Enum.Parse<BaseItemKind>(GetClientTypeName());
         }
 
         /// <summary>

+ 3 - 3
MediaBrowser.Controller/Entities/Folder.cs

@@ -792,7 +792,7 @@ namespace MediaBrowser.Controller.Entities
 
         private bool RequiresPostFiltering2(InternalItemsQuery query)
         {
-            if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase))
+            if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes[0] == BaseItemKind.BoxSet)
             {
                 Logger.LogDebug("Query requires post-filtering due to BoxSet query");
                 return true;
@@ -882,7 +882,7 @@ namespace MediaBrowser.Controller.Entities
 
             if (query.IsPlayed.HasValue)
             {
-                if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(nameof(Series)))
+                if (query.IncludeItemTypes.Length == 1 && query.IncludeItemTypes.Contains(BaseItemKind.Series))
                 {
                     Logger.LogDebug("Query requires post-filtering due to IsPlayed");
                     return true;
@@ -1101,7 +1101,7 @@ namespace MediaBrowser.Controller.Entities
                     return false;
                 }
 
-                if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains("Movie", StringComparer.OrdinalIgnoreCase))
+                if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(BaseItemKind.Movie))
                 {
                     param = true;
                 }

+ 5 - 5
MediaBrowser.Controller/Entities/Genre.cs

@@ -6,7 +6,7 @@ using System;
 using System.Collections.Generic;
 using System.Text.Json.Serialization;
 using Diacritics.Extensions;
-using MediaBrowser.Controller.Entities.Audio;
+using Jellyfin.Data.Enums;
 using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Controller.Entities
@@ -66,10 +66,10 @@ namespace MediaBrowser.Controller.Entities
             query.GenreIds = new[] { Id };
             query.ExcludeItemTypes = new[]
             {
-                nameof(MusicVideo),
-                nameof(Entities.Audio.Audio),
-                nameof(MusicAlbum),
-                nameof(MusicArtist)
+                BaseItemKind.MusicVideo,
+                BaseItemKind.Audio,
+                BaseItemKind.MusicAlbum,
+                BaseItemKind.MusicArtist
             };
 
             return LibraryManager.GetItemList(query);

+ 6 - 6
MediaBrowser.Controller/Entities/InternalItemsQuery.cs

@@ -27,13 +27,13 @@ namespace MediaBrowser.Controller.Entities
             ExcludeArtistIds = Array.Empty<Guid>();
             ExcludeInheritedTags = Array.Empty<string>();
             ExcludeItemIds = Array.Empty<Guid>();
-            ExcludeItemTypes = Array.Empty<string>();
+            ExcludeItemTypes = Array.Empty<BaseItemKind>();
             ExcludeTags = Array.Empty<string>();
             GenreIds = Array.Empty<Guid>();
             Genres = Array.Empty<string>();
             GroupByPresentationUniqueKey = true;
             ImageTypes = Array.Empty<ImageType>();
-            IncludeItemTypes = Array.Empty<string>();
+            IncludeItemTypes = Array.Empty<BaseItemKind>();
             ItemIds = Array.Empty<Guid>();
             MediaTypes = Array.Empty<string>();
             MinSimilarityScore = 20;
@@ -87,9 +87,9 @@ namespace MediaBrowser.Controller.Entities
 
         public string[] MediaTypes { get; set; }
 
-        public string[] IncludeItemTypes { get; set; }
+        public BaseItemKind[] IncludeItemTypes { get; set; }
 
-        public string[] ExcludeItemTypes { get; set; }
+        public BaseItemKind[] ExcludeItemTypes { get; set; }
 
         public string[] ExcludeTags { get; set; }
 
@@ -229,7 +229,7 @@ namespace MediaBrowser.Controller.Entities
 
         public Guid ParentId { get; set; }
 
-        public string? ParentType { get; set; }
+        public BaseItemKind? ParentType { get; set; }
 
         public Guid[] AncestorIds { get; set; }
 
@@ -314,7 +314,7 @@ namespace MediaBrowser.Controller.Entities
                 else
                 {
                     ParentId = value.Id;
-                    ParentType = value.GetType().Name;
+                    ParentType = value.GetBaseItemKind();
                 }
             }
         }

+ 6 - 6
MediaBrowser.Controller/Entities/TV/Series.cs

@@ -127,7 +127,7 @@ namespace MediaBrowser.Controller.Entities.TV
             {
                 AncestorWithPresentationUniqueKey = null,
                 SeriesPresentationUniqueKey = seriesKey,
-                IncludeItemTypes = new[] { nameof(Season) },
+                IncludeItemTypes = new[] { BaseItemKind.Season },
                 IsVirtualItem = false,
                 Limit = 0,
                 DtoOptions = new DtoOptions(false)
@@ -155,7 +155,7 @@ namespace MediaBrowser.Controller.Entities.TV
 
             if (query.IncludeItemTypes.Length == 0)
             {
-                query.IncludeItemTypes = new[] { nameof(Episode) };
+                query.IncludeItemTypes = new[] { BaseItemKind.Episode };
             }
 
             query.IsVirtualItem = false;
@@ -209,7 +209,7 @@ namespace MediaBrowser.Controller.Entities.TV
 
             query.AncestorWithPresentationUniqueKey = null;
             query.SeriesPresentationUniqueKey = seriesKey;
-            query.IncludeItemTypes = new[] { nameof(Season) };
+            query.IncludeItemTypes = new[] { BaseItemKind.Season };
             query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
 
             if (user != null && !user.DisplayMissingEpisodes)
@@ -235,7 +235,7 @@ namespace MediaBrowser.Controller.Entities.TV
 
                 if (query.IncludeItemTypes.Length == 0)
                 {
-                    query.IncludeItemTypes = new[] { nameof(Episode), nameof(Season) };
+                    query.IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Season };
                 }
 
                 query.IsVirtualItem = false;
@@ -255,7 +255,7 @@ namespace MediaBrowser.Controller.Entities.TV
             {
                 AncestorWithPresentationUniqueKey = null,
                 SeriesPresentationUniqueKey = seriesKey,
-                IncludeItemTypes = new[] { nameof(Episode), nameof(Season) },
+                IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Season },
                 OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
                 DtoOptions = options
             };
@@ -359,7 +359,7 @@ namespace MediaBrowser.Controller.Entities.TV
             {
                 AncestorWithPresentationUniqueKey = queryFromSeries ? null : seriesKey,
                 SeriesPresentationUniqueKey = queryFromSeries ? seriesKey : null,
-                IncludeItemTypes = new[] { nameof(Episode) },
+                IncludeItemTypes = new[] { BaseItemKind.Episode },
                 OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
                 DtoOptions = options
             };

+ 20 - 20
MediaBrowser.Controller/Entities/UserViewBuilder.cs

@@ -140,7 +140,7 @@ namespace MediaBrowser.Controller.Entities
 
                 if (query.IncludeItemTypes.Length == 0)
                 {
-                    query.IncludeItemTypes = new[] { nameof(Movie) };
+                    query.IncludeItemTypes = new[] { BaseItemKind.Movie };
                 }
 
                 return parent.QueryRecursive(query);
@@ -165,7 +165,7 @@ namespace MediaBrowser.Controller.Entities
             query.Parent = parent;
             query.SetUser(user);
             query.IsFavorite = true;
-            query.IncludeItemTypes = new[] { nameof(Movie) };
+            query.IncludeItemTypes = new[] { BaseItemKind.Movie };
 
             return _libraryManager.GetItemsResult(query);
         }
@@ -176,7 +176,7 @@ namespace MediaBrowser.Controller.Entities
             query.Parent = parent;
             query.SetUser(user);
             query.IsFavorite = true;
-            query.IncludeItemTypes = new[] { nameof(Series) };
+            query.IncludeItemTypes = new[] { BaseItemKind.Series };
 
             return _libraryManager.GetItemsResult(query);
         }
@@ -187,7 +187,7 @@ namespace MediaBrowser.Controller.Entities
             query.Parent = parent;
             query.SetUser(user);
             query.IsFavorite = true;
-            query.IncludeItemTypes = new[] { nameof(Episode) };
+            query.IncludeItemTypes = new[] { BaseItemKind.Episode };
 
             return _libraryManager.GetItemsResult(query);
         }
@@ -198,7 +198,7 @@ namespace MediaBrowser.Controller.Entities
             query.Parent = parent;
             query.SetUser(user);
 
-            query.IncludeItemTypes = new[] { nameof(Movie) };
+            query.IncludeItemTypes = new[] { BaseItemKind.Movie };
 
             return _libraryManager.GetItemsResult(query);
         }
@@ -206,7 +206,7 @@ namespace MediaBrowser.Controller.Entities
         private QueryResult<BaseItem> GetMovieCollections(User user, InternalItemsQuery query)
         {
             query.Parent = null;
-            query.IncludeItemTypes = new[] { nameof(BoxSet) };
+            query.IncludeItemTypes = new[] { BaseItemKind.BoxSet };
             query.SetUser(user);
             query.Recursive = true;
 
@@ -220,7 +220,7 @@ namespace MediaBrowser.Controller.Entities
             query.Parent = parent;
             query.SetUser(user);
             query.Limit = GetSpecialItemsLimit();
-            query.IncludeItemTypes = new[] { nameof(Movie) };
+            query.IncludeItemTypes = new[] { BaseItemKind.Movie };
 
             return ConvertToResult(_libraryManager.GetItemList(query));
         }
@@ -233,7 +233,7 @@ namespace MediaBrowser.Controller.Entities
             query.Parent = parent;
             query.SetUser(user);
             query.Limit = GetSpecialItemsLimit();
-            query.IncludeItemTypes = new[] { nameof(Movie) };
+            query.IncludeItemTypes = new[] { BaseItemKind.Movie };
 
             return ConvertToResult(_libraryManager.GetItemList(query));
         }
@@ -252,7 +252,7 @@ namespace MediaBrowser.Controller.Entities
         {
             var genres = parent.QueryRecursive(new InternalItemsQuery(user)
             {
-                IncludeItemTypes = new[] { nameof(Movie) },
+                IncludeItemTypes = new[] { BaseItemKind.Movie },
                 Recursive = true,
                 EnableTotalRecordCount = false
             }).Items
@@ -283,7 +283,7 @@ namespace MediaBrowser.Controller.Entities
             query.GenreIds = new[] { displayParent.Id };
             query.SetUser(user);
 
-            query.IncludeItemTypes = new[] { nameof(Movie) };
+            query.IncludeItemTypes = new[] { BaseItemKind.Movie };
 
             return _libraryManager.GetItemsResult(query);
         }
@@ -299,9 +299,9 @@ namespace MediaBrowser.Controller.Entities
                 {
                     query.IncludeItemTypes = new[]
                     {
-                        nameof(Series),
-                        nameof(Season),
-                        nameof(Episode)
+                        BaseItemKind.Series,
+                        BaseItemKind.Season,
+                        BaseItemKind.Episode
                     };
                 }
 
@@ -329,7 +329,7 @@ namespace MediaBrowser.Controller.Entities
             query.Parent = parent;
             query.SetUser(user);
             query.Limit = GetSpecialItemsLimit();
-            query.IncludeItemTypes = new[] { nameof(Episode) };
+            query.IncludeItemTypes = new[] { BaseItemKind.Episode };
             query.IsVirtualItem = false;
 
             return ConvertToResult(_libraryManager.GetItemList(query));
@@ -360,7 +360,7 @@ namespace MediaBrowser.Controller.Entities
             query.Parent = parent;
             query.SetUser(user);
             query.Limit = GetSpecialItemsLimit();
-            query.IncludeItemTypes = new[] { nameof(Episode) };
+            query.IncludeItemTypes = new[] { BaseItemKind.Episode };
 
             return ConvertToResult(_libraryManager.GetItemList(query));
         }
@@ -371,7 +371,7 @@ namespace MediaBrowser.Controller.Entities
             query.Parent = parent;
             query.SetUser(user);
 
-            query.IncludeItemTypes = new[] { nameof(Series) };
+            query.IncludeItemTypes = new[] { BaseItemKind.Series };
 
             return _libraryManager.GetItemsResult(query);
         }
@@ -380,7 +380,7 @@ namespace MediaBrowser.Controller.Entities
         {
             var genres = parent.QueryRecursive(new InternalItemsQuery(user)
             {
-                IncludeItemTypes = new[] { nameof(Series) },
+                IncludeItemTypes = new[] { BaseItemKind.Series },
                 Recursive = true,
                 EnableTotalRecordCount = false
             }).Items
@@ -411,7 +411,7 @@ namespace MediaBrowser.Controller.Entities
             query.GenreIds = new[] { displayParent.Id };
             query.SetUser(user);
 
-            query.IncludeItemTypes = new[] { nameof(Series) };
+            query.IncludeItemTypes = new[] { BaseItemKind.Series };
 
             return _libraryManager.GetItemsResult(query);
         }
@@ -499,12 +499,12 @@ namespace MediaBrowser.Controller.Entities
                 return false;
             }
 
-            if (query.IncludeItemTypes.Length > 0 && !query.IncludeItemTypes.Contains(item.GetClientTypeName(), StringComparer.OrdinalIgnoreCase))
+            if (query.IncludeItemTypes.Length > 0 && !query.IncludeItemTypes.Contains(item.GetBaseItemKind()))
             {
                 return false;
             }
 
-            if (query.ExcludeItemTypes.Length > 0 && query.ExcludeItemTypes.Contains(item.GetClientTypeName(), StringComparer.OrdinalIgnoreCase))
+            if (query.ExcludeItemTypes.Length > 0 && query.ExcludeItemTypes.Contains(item.GetBaseItemKind()))
             {
                 return false;
             }

+ 7 - 0
MediaBrowser.Controller/Entities/Video.cs

@@ -33,6 +33,7 @@ namespace MediaBrowser.Controller.Entities
             AdditionalParts = Array.Empty<string>();
             LocalAlternateVersions = Array.Empty<string>();
             SubtitleFiles = Array.Empty<string>();
+            AudioFiles = Array.Empty<string>();
             LinkedAlternateVersions = Array.Empty<LinkedChild>();
         }
 
@@ -97,6 +98,12 @@ namespace MediaBrowser.Controller.Entities
         /// <value>The subtitle paths.</value>
         public string[] SubtitleFiles { get; set; }
 
+        /// <summary>
+        /// Gets or sets the audio paths.
+        /// </summary>
+        /// <value>The audio paths.</value>
+        public string[] AudioFiles { get; set; }
+
         /// <summary>
         /// Gets or sets a value indicating whether this instance has subtitles.
         /// </summary>

+ 23 - 4
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -696,6 +696,11 @@ namespace MediaBrowser.Controller.MediaEncoding
                 arg.Append(" -i \"").Append(subtitlePath).Append('\"');
             }
 
+            if (state.AudioStream != null && state.AudioStream.IsExternal)
+            {
+                arg.Append(" -i \"").Append(state.AudioStream.Path).Append('"');
+            }
+
             return arg.ToString();
         }
 
@@ -1999,10 +2004,24 @@ namespace MediaBrowser.Controller.MediaEncoding
 
             if (state.AudioStream != null)
             {
-                args += string.Format(
-                    CultureInfo.InvariantCulture,
-                    " -map 0:{0}",
-                    state.AudioStream.Index);
+                if (state.AudioStream.IsExternal)
+                {
+                    int externalAudioMapIndex = state.SubtitleStream != null && state.SubtitleStream.IsExternal ? 2 : 1;
+                    int externalAudioStream = state.MediaSource.MediaStreams.Where(i => i.Path == state.AudioStream.Path).ToList().IndexOf(state.AudioStream);
+
+                    args += string.Format(
+                        CultureInfo.InvariantCulture,
+                        " -map {0}:{1}",
+                        externalAudioMapIndex,
+                        externalAudioStream);
+                }
+                else
+                {
+                    args += string.Format(
+                        CultureInfo.InvariantCulture,
+                        " -map 0:{0}",
+                        state.AudioStream.Index);
+                }
             }
             else
             {

+ 2 - 2
MediaBrowser.Controller/Playlists/Playlist.cs

@@ -189,7 +189,7 @@ namespace MediaBrowser.Controller.Playlists
                 return LibraryManager.GetItemList(new InternalItemsQuery(user)
                 {
                     Recursive = true,
-                    IncludeItemTypes = new[] { nameof(Audio) },
+                    IncludeItemTypes = new[] { BaseItemKind.Audio },
                     GenreIds = new[] { musicGenre.Id },
                     OrderBy = new[] { (ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) },
                     DtoOptions = options
@@ -201,7 +201,7 @@ namespace MediaBrowser.Controller.Playlists
                 return LibraryManager.GetItemList(new InternalItemsQuery(user)
                 {
                     Recursive = true,
-                    IncludeItemTypes = new[] { nameof(Audio) },
+                    IncludeItemTypes = new[] { BaseItemKind.Audio },
                     ArtistIds = new[] { musicArtist.Id },
                     OrderBy = new[] { (ItemSortBy.AlbumArtist, SortOrder.Ascending), (ItemSortBy.Album, SortOrder.Ascending), (ItemSortBy.SortName, SortOrder.Ascending) },
                     DtoOptions = options

+ 18 - 5
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs

@@ -45,6 +45,7 @@ namespace MediaBrowser.MediaEncoding.Probing
         {
             "AC/DC",
             "Au/Ra",
+            "Bremer/McCoy",
             "이달의 소녀 1/3",
             "LOONA 1/3",
             "LOONA / yyxy",
@@ -648,11 +649,6 @@ namespace MediaBrowser.MediaEncoding.Probing
                 stream.IsAVC = false;
             }
 
-            if (!string.IsNullOrWhiteSpace(streamInfo.FieldOrder) && !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase))
-            {
-                stream.IsInterlaced = true;
-            }
-
             // Filter out junk
             if (!string.IsNullOrWhiteSpace(streamInfo.CodecTagString) && !streamInfo.CodecTagString.Contains("[0]", StringComparison.OrdinalIgnoreCase))
             {
@@ -724,6 +720,23 @@ namespace MediaBrowser.MediaEncoding.Probing
                 stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate);
                 stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate);
 
+                // Some interlaced H.264 files in mp4 containers using MBAFF coding aren't flagged as being interlaced by FFprobe,
+                // so for H.264 files we also calculate the frame rate from the codec time base and check if it is double the reported
+                // frame rate (both rounded to the nearest integer) to determine if the file is interlaced
+                int roundedTimeBaseFPS = Convert.ToInt32(1 / GetFrameRate(stream.CodecTimeBase) ?? 0);
+                int roundedDoubleFrameRate = Convert.ToInt32(stream.AverageFrameRate * 2 ?? 0);
+
+                bool videoInterlaced = !string.IsNullOrWhiteSpace(streamInfo.FieldOrder)
+                    && !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase);
+                bool h264MbaffCoded = string.Equals(stream.Codec, "h264", StringComparison.OrdinalIgnoreCase)
+                    && string.IsNullOrWhiteSpace(streamInfo.FieldOrder)
+                    && roundedTimeBaseFPS == roundedDoubleFrameRate;
+
+                if (videoInterlaced || h264MbaffCoded)
+                {
+                    stream.IsInterlaced = true;
+                }
+
                 if (isAudio
                     || string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)
                     || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)

+ 0 - 222
MediaBrowser.Model/Configuration/ServerConfiguration.cs

@@ -13,18 +13,6 @@ namespace MediaBrowser.Model.Configuration
     /// </summary>
     public class ServerConfiguration : BaseApplicationConfiguration
     {
-        /// <summary>
-        /// The default value for <see cref="HttpServerPortNumber"/>.
-        /// </summary>
-        public const int DefaultHttpPort = 8096;
-
-        /// <summary>
-        /// The default value for <see cref="PublicHttpsPort"/> and <see cref="HttpsPortNumber"/>.
-        /// </summary>
-        public const int DefaultHttpsPort = 8920;
-
-        private string _baseUrl = string.Empty;
-
         /// <summary>
         /// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
         /// </summary>
@@ -75,149 +63,13 @@ namespace MediaBrowser.Model.Configuration
             };
         }
 
-        /// <summary>
-        /// Gets or sets a value indicating whether to enable automatic port forwarding.
-        /// </summary>
-        public bool EnableUPnP { get; set; } = false;
-
         /// <summary>
         /// Gets or sets a value indicating whether to enable prometheus metrics exporting.
         /// </summary>
         public bool EnableMetrics { get; set; } = false;
 
-        /// <summary>
-        /// Gets or sets the public mapped port.
-        /// </summary>
-        /// <value>The public mapped port.</value>
-        public int PublicPort { get; set; } = DefaultHttpPort;
-
-        /// <summary>
-        /// Gets or sets a value indicating whether the http port should be mapped as part of UPnP automatic port forwarding.
-        /// </summary>
-        public bool UPnPCreateHttpPortMap { get; set; } = false;
-
-        /// <summary>
-        /// Gets or sets client udp port range.
-        /// </summary>
-        public string UDPPortRange { get; set; } = string.Empty;
-
-        /// <summary>
-        /// Gets or sets a value indicating whether IPV6 capability is enabled.
-        /// </summary>
-        public bool EnableIPV6 { get; set; } = false;
-
-        /// <summary>
-        /// Gets or sets a value indicating whether IPV4 capability is enabled.
-        /// </summary>
-        public bool EnableIPV4 { get; set; } = true;
-
-        /// <summary>
-        /// Gets or sets a value indicating whether detailed ssdp logs are sent to the console/log.
-        /// "Emby.Dlna": "Debug" must be set in logging.default.json for this property to work.
-        /// </summary>
-        public bool EnableSSDPTracing { get; set; } = false;
-
-        /// <summary>
-        /// Gets or sets a value indicating whether an IP address is to be used to filter the detailed ssdp logs that are being sent to the console/log.
-        /// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work.
-        /// </summary>
-        public string SSDPTracingFilter { get; set; } = string.Empty;
-
-        /// <summary>
-        /// Gets or sets the number of times SSDP UDP messages are sent.
-        /// </summary>
-        public int UDPSendCount { get; set; } = 2;
-
-        /// <summary>
-        /// Gets or sets the delay between each groups of SSDP messages (in ms).
-        /// </summary>
-        public int UDPSendDelay { get; set; } = 100;
-
-        /// <summary>
-        /// Gets or sets a value indicating whether address names that match <see cref="VirtualInterfaceNames"/> should be Ignore for the purposes of binding.
-        /// </summary>
-        public bool IgnoreVirtualInterfaces { get; set; } = true;
-
-        /// <summary>
-        /// Gets or sets a value indicating the interfaces that should be ignored. The list can be comma separated. <seealso cref="IgnoreVirtualInterfaces"/>.
-        /// </summary>
-        public string VirtualInterfaceNames { get; set; } = "vEthernet*";
-
-        /// <summary>
-        /// Gets or sets the time (in seconds) between the pings of SSDP gateway monitor.
-        /// </summary>
-        public int GatewayMonitorPeriod { get; set; } = 60;
-
-        /// <summary>
-        /// Gets a value indicating whether multi-socket binding is available.
-        /// </summary>
-        public bool EnableMultiSocketBinding { get; } = true;
-
-        /// <summary>
-        /// Gets or sets a value indicating whether all IPv6 interfaces should be treated as on the internal network.
-        /// Depending on the address range implemented ULA ranges might not be used.
-        /// </summary>
-        public bool TrustAllIP6Interfaces { get; set; } = false;
-
-        /// <summary>
-        /// Gets or sets the ports that HDHomerun uses.
-        /// </summary>
-        public string HDHomerunPortRange { get; set; } = string.Empty;
-
-        /// <summary>
-        /// Gets or sets PublishedServerUri to advertise for specific subnets.
-        /// </summary>
-        public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty<string>();
-
-        /// <summary>
-        /// Gets or sets a value indicating whether Autodiscovery tracing is enabled.
-        /// </summary>
-        public bool AutoDiscoveryTracing { get; set; } = false;
-
-        /// <summary>
-        /// Gets or sets a value indicating whether Autodiscovery is enabled.
-        /// </summary>
-        public bool AutoDiscovery { get; set; } = true;
-
-        /// <summary>
-        /// Gets or sets the public HTTPS port.
-        /// </summary>
-        /// <value>The public HTTPS port.</value>
-        public int PublicHttpsPort { get; set; } = DefaultHttpsPort;
-
-        /// <summary>
-        /// Gets or sets the HTTP server port number.
-        /// </summary>
-        /// <value>The HTTP server port number.</value>
-        public int HttpServerPortNumber { get; set; } = DefaultHttpPort;
-
-        /// <summary>
-        /// Gets or sets the HTTPS server port number.
-        /// </summary>
-        /// <value>The HTTPS server port number.</value>
-        public int HttpsPortNumber { get; set; } = DefaultHttpsPort;
-
-        /// <summary>
-        /// Gets or sets a value indicating whether to use HTTPS.
-        /// </summary>
-        /// <remarks>
-        /// In order for HTTPS to be used, in addition to setting this to true, valid values must also be
-        /// provided for <see cref="CertificatePath"/> and <see cref="CertificatePassword"/>.
-        /// </remarks>
-        public bool EnableHttps { get; set; } = false;
-
         public bool EnableNormalizedItemByNameIds { get; set; } = true;
 
-        /// <summary>
-        /// Gets or sets the filesystem path of an X.509 certificate to use for SSL.
-        /// </summary>
-        public string CertificatePath { get; set; } = string.Empty;
-
-        /// <summary>
-        /// Gets or sets the password required to access the X.509 certificate data in the file specified by <see cref="CertificatePath"/>.
-        /// </summary>
-        public string CertificatePassword { get; set; } = string.Empty;
-
         /// <summary>
         /// Gets or sets a value indicating whether this instance is port authorized.
         /// </summary>
@@ -229,11 +81,6 @@ namespace MediaBrowser.Model.Configuration
         /// </summary>
         public bool QuickConnectAvailable { get; set; } = false;
 
-        /// <summary>
-        /// Gets or sets a value indicating whether access outside of the LAN is permitted.
-        /// </summary>
-        public bool EnableRemoteAccess { get; set; } = true;
-
         /// <summary>
         /// Gets or sets a value indicating whether [enable case sensitive item ids].
         /// </summary>
@@ -318,13 +165,6 @@ namespace MediaBrowser.Model.Configuration
         /// <value>The file watcher delay.</value>
         public int LibraryMonitorDelay { get; set; } = 60;
 
-        /// <summary>
-        /// Gets or sets a value indicating whether [enable dashboard response caching].
-        /// Allows potential contributors without visual studio to modify production dashboard code and test changes.
-        /// </summary>
-        /// <value><c>true</c> if [enable dashboard response caching]; otherwise, <c>false</c>.</value>
-        public bool EnableDashboardResponseCaching { get; set; } = true;
-
         /// <summary>
         /// Gets or sets the image saving convention.
         /// </summary>
@@ -337,36 +177,6 @@ namespace MediaBrowser.Model.Configuration
 
         public string ServerName { get; set; } = string.Empty;
 
-        public string BaseUrl
-        {
-            get => _baseUrl;
-            set
-            {
-                // Normalize the start of the string
-                if (string.IsNullOrWhiteSpace(value))
-                {
-                    // If baseUrl is empty, set an empty prefix string
-                    _baseUrl = string.Empty;
-                    return;
-                }
-
-                if (value[0] != '/')
-                {
-                    // If baseUrl was not configured with a leading slash, append one for consistency
-                    value = "/" + value;
-                }
-
-                // Normalize the end of the string
-                if (value[value.Length - 1] == '/')
-                {
-                    // If baseUrl was configured with a trailing slash, remove it for consistency
-                    value = value.Remove(value.Length - 1);
-                }
-
-                _baseUrl = value;
-            }
-        }
-
         public string UICulture { get; set; } = "en-US";
 
         public bool SaveMetadataHidden { get; set; } = false;
@@ -381,43 +191,16 @@ namespace MediaBrowser.Model.Configuration
 
         public bool DisplaySpecialsWithinSeasons { get; set; } = true;
 
-        /// <summary>
-        /// Gets or sets the subnets that are deemed to make up the LAN.
-        /// </summary>
-        public string[] LocalNetworkSubnets { get; set; } = Array.Empty<string>();
-
-        /// <summary>
-        /// Gets or sets the interface addresses which Jellyfin will bind to. If empty, all interfaces will be used.
-        /// </summary>
-        public string[] LocalNetworkAddresses { get; set; } = Array.Empty<string>();
-
         public string[] CodecsUsed { get; set; } = Array.Empty<string>();
 
         public List<RepositoryInfo> PluginRepositories { get; set; } = new List<RepositoryInfo>();
 
         public bool EnableExternalContentInSuggestions { get; set; } = true;
 
-        /// <summary>
-        /// Gets or sets a value indicating whether the server should force connections over HTTPS.
-        /// </summary>
-        public bool RequireHttps { get; set; } = false;
-
-        /// <summary>
-        /// Gets or sets the filter for remote IP connectivity. Used in conjuntion with <seealso cref="IsRemoteIPFilterBlacklist"/>.
-        /// </summary>
-        public string[] RemoteIPFilter { get; set; } = Array.Empty<string>();
-
-        /// <summary>
-        /// Gets or sets a value indicating whether <seealso cref="RemoteIPFilter"/> contains a blacklist or a whitelist. Default is a whitelist.
-        /// </summary>
-        public bool IsRemoteIPFilterBlacklist { get; set; } = false;
-
         public int ImageExtractionTimeoutMs { get; set; } = 0;
 
         public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty<PathSubstitution>();
 
-        public string[] UninstalledPlugins { get; set; } = Array.Empty<string>();
-
         /// <summary>
         /// Gets or sets a value indicating whether slow server responses should be logged as a warning.
         /// </summary>
@@ -433,11 +216,6 @@ namespace MediaBrowser.Model.Configuration
         /// </summary>
         public string[] CorsHosts { get; set; } = new[] { "*" };
 
-        /// <summary>
-        /// Gets or sets the known proxies.
-        /// </summary>
-        public string[] KnownProxies { get; set; } = Array.Empty<string>();
-
         /// <summary>
         /// Gets or sets the number of days we should retain activity logs.
         /// </summary>

+ 2 - 2
MediaBrowser.Model/Net/MimeTypes.cs

@@ -116,7 +116,7 @@ namespace MediaBrowser.Model.Net
             { "audio/x-wavpack", ".wv" },
 
             // Type image
-            { "image/jpg", ".jpg" },
+            { "image/jpeg", ".jpg" },
             { "image/x-png", ".png" },
 
             // Type text
@@ -137,7 +137,7 @@ namespace MediaBrowser.Model.Net
         /// <param name="filename">The filename to find the MIME type of.</param>
         /// <param name="defaultValue">The default value to return if no fitting MIME type is found.</param>
         /// <returns>The correct MIME type for the given filename, or <paramref name="defaultValue"/> if it wasn't found.</returns>
-        [return: NotNullIfNotNullAttribute("defaultValue")]
+        [return: NotNullIfNotNull("defaultValue")]
         public static string? GetMimeType(string filename, string? defaultValue = null)
         {
             if (filename.Length == 0)

+ 2 - 1
MediaBrowser.Model/Querying/LatestItemsQuery.cs

@@ -2,6 +2,7 @@
 #pragma warning disable CS1591
 
 using System;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Model.Entities;
 
 namespace MediaBrowser.Model.Querying
@@ -48,7 +49,7 @@ namespace MediaBrowser.Model.Querying
         /// Gets or sets the include item types.
         /// </summary>
         /// <value>The include item types.</value>
-        public string[] IncludeItemTypes { get; set; }
+        public BaseItemKind[] IncludeItemTypes { get; set; }
 
         /// <summary>
         /// Gets or sets a value indicating whether this instance is played.

+ 5 - 4
MediaBrowser.Model/Search/SearchQuery.cs

@@ -2,6 +2,7 @@
 #pragma warning disable CS1591
 
 using System;
+using Jellyfin.Data.Enums;
 
 namespace MediaBrowser.Model.Search
 {
@@ -16,8 +17,8 @@ namespace MediaBrowser.Model.Search
             IncludeStudios = true;
 
             MediaTypes = Array.Empty<string>();
-            IncludeItemTypes = Array.Empty<string>();
-            ExcludeItemTypes = Array.Empty<string>();
+            IncludeItemTypes = Array.Empty<BaseItemKind>();
+            ExcludeItemTypes = Array.Empty<BaseItemKind>();
         }
 
         /// <summary>
@@ -56,9 +57,9 @@ namespace MediaBrowser.Model.Search
 
         public string[] MediaTypes { get; set; }
 
-        public string[] IncludeItemTypes { get; set; }
+        public BaseItemKind[] IncludeItemTypes { get; set; }
 
-        public string[] ExcludeItemTypes { get; set; }
+        public BaseItemKind[] ExcludeItemTypes { get; set; }
 
         public Guid? ParentId { get; set; }
 

+ 3 - 2
MediaBrowser.Model/Session/BrowseRequest.cs

@@ -1,3 +1,5 @@
+using Jellyfin.Data.Enums;
+
 #nullable disable
 namespace MediaBrowser.Model.Session
 {
@@ -8,10 +10,9 @@ namespace MediaBrowser.Model.Session
     {
         /// <summary>
         /// Gets or sets the item type.
-        /// Artist, Genre, Studio, Person, or any kind of BaseItem.
         /// </summary>
         /// <value>The type of the item.</value>
-        public string ItemType { get; set; }
+        public BaseItemKind ItemType { get; set; }
 
         /// <summary>
         /// Gets or sets the item id.

+ 2 - 1
MediaBrowser.Providers/Manager/ProviderManager.cs

@@ -11,6 +11,7 @@ using System.Net.Http;
 using System.Net.Mime;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
 using Jellyfin.Data.Events;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Progress;
@@ -1133,7 +1134,7 @@ namespace MediaBrowser.Providers.Manager
             var albums = _libraryManager
                 .GetItemList(new InternalItemsQuery
                 {
-                    IncludeItemTypes = new[] { nameof(MusicAlbum) },
+                    IncludeItemTypes = new[] { BaseItemKind.MusicAlbum },
                     ArtistIds = new[] { item.Id },
                     DtoOptions = new DtoOptions(false)
                     {

+ 176 - 0
MediaBrowser.Providers/MediaInfo/AudioResolver.cs

@@ -0,0 +1,176 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Emby.Naming.Audio;
+using Emby.Naming.Common;
+using Jellyfin.Extensions;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.MediaInfo;
+
+namespace MediaBrowser.Providers.MediaInfo
+{
+    /// <summary>
+    /// Resolves external audios for videos.
+    /// </summary>
+    public class AudioResolver
+    {
+        private readonly ILocalizationManager _localizationManager;
+        private readonly IMediaEncoder _mediaEncoder;
+        private readonly NamingOptions _namingOptions;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AudioResolver"/> class.
+        /// </summary>
+        /// <param name="localizationManager">The localization manager.</param>
+        /// <param name="mediaEncoder">The media encoder.</param>
+        /// <param name="namingOptions">The naming options.</param>
+        public AudioResolver(
+            ILocalizationManager localizationManager,
+            IMediaEncoder mediaEncoder,
+            NamingOptions namingOptions)
+        {
+            _localizationManager = localizationManager;
+            _mediaEncoder = mediaEncoder;
+            _namingOptions = namingOptions;
+        }
+
+        /// <summary>
+        /// Returns the audio streams found in the external audio files for the given video.
+        /// </summary>
+        /// <param name="video">The video to get the external audio streams from.</param>
+        /// <param name="startIndex">The stream index to start adding audio streams at.</param>
+        /// <param name="directoryService">The directory service to search for files.</param>
+        /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
+        /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
+        /// <returns>A list of external audio streams.</returns>
+        public async IAsyncEnumerable<MediaStream> GetExternalAudioStreams(
+            Video video,
+            int startIndex,
+            IDirectoryService directoryService,
+            bool clearCache,
+            [EnumeratorCancellation] CancellationToken cancellationToken)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+
+            if (!video.IsFileProtocol)
+            {
+                yield break;
+            }
+
+            IEnumerable<string> paths = GetExternalAudioFiles(video, directoryService, clearCache);
+            foreach (string path in paths)
+            {
+                string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
+                Model.MediaInfo.MediaInfo mediaInfo = await GetMediaInfo(path, cancellationToken).ConfigureAwait(false);
+
+                foreach (MediaStream mediaStream in mediaInfo.MediaStreams)
+                {
+                    mediaStream.Index = startIndex++;
+                    mediaStream.Type = MediaStreamType.Audio;
+                    mediaStream.IsExternal = true;
+                    mediaStream.Path = path;
+                    mediaStream.IsDefault = false;
+                    mediaStream.Title = null;
+
+                    if (string.IsNullOrEmpty(mediaStream.Language))
+                    {
+                        // Try to translate to three character code
+                        // Be flexible and check against both the full and three character versions
+                        var language = StringExtensions.RightPart(fileNameWithoutExtension, '.').ToString();
+
+                        if (language != fileNameWithoutExtension)
+                        {
+                            var culture = _localizationManager.FindLanguageInfo(language);
+
+                            language = culture == null ? language : culture.ThreeLetterISOLanguageName;
+                            mediaStream.Language = language;
+                        }
+                    }
+
+                    yield return mediaStream;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Returns the external audio file paths for the given video.
+        /// </summary>
+        /// <param name="video">The video to get the external audio file paths from.</param>
+        /// <param name="directoryService">The directory service to search for files.</param>
+        /// <param name="clearCache">True if the directory service cache should be cleared before searching.</param>
+        /// <returns>A list of external audio file paths.</returns>
+        public IEnumerable<string> GetExternalAudioFiles(
+            Video video,
+            IDirectoryService directoryService,
+            bool clearCache)
+        {
+            if (!video.IsFileProtocol)
+            {
+                yield break;
+            }
+
+            // Check if video folder exists
+            string folder = video.ContainingFolderPath;
+            if (!Directory.Exists(folder))
+            {
+                yield break;
+            }
+
+            string videoFileNameWithoutExtension = Path.GetFileNameWithoutExtension(video.Path);
+
+            var files = directoryService.GetFilePaths(folder, clearCache, true);
+            for (int i = 0; i < files.Count; i++)
+            {
+                string file = files[i];
+                if (string.Equals(video.Path, file, StringComparison.OrdinalIgnoreCase)
+                    || !AudioFileParser.IsAudioFile(file, _namingOptions)
+                    || Path.GetExtension(file.AsSpan()).Equals(".strm", StringComparison.OrdinalIgnoreCase))
+                {
+                    continue;
+                }
+
+                string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file);
+                // The audio filename must either be equal to the video filename or start with the video filename followed by a dot
+                if (videoFileNameWithoutExtension.Equals(fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)
+                    || (fileNameWithoutExtension.Length > videoFileNameWithoutExtension.Length
+                        && fileNameWithoutExtension[videoFileNameWithoutExtension.Length] == '.'
+                        && fileNameWithoutExtension.StartsWith(videoFileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)))
+                {
+                    yield return file;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Returns the media info of the given audio file.
+        /// </summary>
+        /// <param name="path">The path to the audio file.</param>
+        /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
+        /// <returns>The media info for the given audio file.</returns>
+        private Task<Model.MediaInfo.MediaInfo> GetMediaInfo(string path, CancellationToken cancellationToken)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+
+            return _mediaEncoder.GetMediaInfo(
+                new MediaInfoRequest
+                {
+                    MediaType = DlnaProfileType.Audio,
+                    MediaSource = new MediaSourceInfo
+                    {
+                        Path = path,
+                        Protocol = MediaProtocol.File
+                    }
+                },
+                cancellationToken);
+        }
+    }
+}

+ 17 - 5
MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs

@@ -7,6 +7,7 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Emby.Naming.Common;
 using MediaBrowser.Controller.Chapters;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
@@ -39,6 +40,7 @@ namespace MediaBrowser.Providers.MediaInfo
     {
         private readonly ILogger<FFProbeProvider> _logger;
         private readonly SubtitleResolver _subtitleResolver;
+        private readonly AudioResolver _audioResolver;
         private readonly FFProbeVideoInfo _videoProber;
         private readonly FFProbeAudioInfo _audioProber;
 
@@ -55,10 +57,11 @@ namespace MediaBrowser.Providers.MediaInfo
             IServerConfigurationManager config,
             ISubtitleManager subtitleManager,
             IChapterManager chapterManager,
-            ILibraryManager libraryManager)
+            ILibraryManager libraryManager,
+            NamingOptions namingOptions)
         {
             _logger = logger;
-
+            _audioResolver = new AudioResolver(localization, mediaEncoder, namingOptions);
             _subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager);
             _videoProber = new FFProbeVideoInfo(
                 _logger,
@@ -71,7 +74,8 @@ namespace MediaBrowser.Providers.MediaInfo
                 config,
                 subtitleManager,
                 chapterManager,
-                libraryManager);
+                libraryManager,
+                _audioResolver);
             _audioProber = new FFProbeAudioInfo(mediaSourceManager, mediaEncoder, itemRepo, libraryManager);
         }
 
@@ -92,7 +96,7 @@ namespace MediaBrowser.Providers.MediaInfo
                     var file = directoryService.GetFile(path);
                     if (file != null && file.LastWriteTimeUtc != item.DateModified)
                     {
-                        _logger.LogDebug("Refreshing {0} due to date modified timestamp change.", path);
+                        _logger.LogDebug("Refreshing {ItemPath} due to date modified timestamp change.", path);
                         return true;
                     }
                 }
@@ -102,7 +106,15 @@ namespace MediaBrowser.Providers.MediaInfo
                 && !video.SubtitleFiles.SequenceEqual(
                         _subtitleResolver.GetExternalSubtitleFiles(video, directoryService, false), StringComparer.Ordinal))
             {
-                _logger.LogDebug("Refreshing {0} due to external subtitles change.", item.Path);
+                _logger.LogDebug("Refreshing {ItemPath} due to external subtitles change.", item.Path);
+                return true;
+            }
+
+            if (item.SupportsLocalMetadata && video != null && !video.IsPlaceHolder
+                && !video.AudioFiles.SequenceEqual(
+                        _audioResolver.GetExternalAudioFiles(video, directoryService, false), StringComparer.Ordinal))
+            {
+                _logger.LogDebug("Refreshing {ItemPath} due to external audio change.", item.Path);
                 return true;
             }
 

+ 31 - 1
MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs

@@ -44,6 +44,7 @@ namespace MediaBrowser.Providers.MediaInfo
         private readonly ISubtitleManager _subtitleManager;
         private readonly IChapterManager _chapterManager;
         private readonly ILibraryManager _libraryManager;
+        private readonly AudioResolver _audioResolver;
         private readonly IMediaSourceManager _mediaSourceManager;
 
         private readonly long _dummyChapterDuration = TimeSpan.FromMinutes(5).Ticks;
@@ -59,7 +60,8 @@ namespace MediaBrowser.Providers.MediaInfo
             IServerConfigurationManager config,
             ISubtitleManager subtitleManager,
             IChapterManager chapterManager,
-            ILibraryManager libraryManager)
+            ILibraryManager libraryManager,
+            AudioResolver audioResolver)
         {
             _logger = logger;
             _mediaEncoder = mediaEncoder;
@@ -71,6 +73,7 @@ namespace MediaBrowser.Providers.MediaInfo
             _subtitleManager = subtitleManager;
             _chapterManager = chapterManager;
             _libraryManager = libraryManager;
+            _audioResolver = audioResolver;
             _mediaSourceManager = mediaSourceManager;
         }
 
@@ -214,6 +217,8 @@ namespace MediaBrowser.Providers.MediaInfo
 
             await AddExternalSubtitles(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
 
+            await AddExternalAudioAsync(video, mediaStreams, options, cancellationToken).ConfigureAwait(false);
+
             var libraryOptions = _libraryManager.GetLibraryOptions(video);
 
             if (mediaInfo != null)
@@ -574,6 +579,31 @@ namespace MediaBrowser.Providers.MediaInfo
             currentStreams.AddRange(externalSubtitleStreams);
         }
 
+        /// <summary>
+        /// Adds the external audio.
+        /// </summary>
+        /// <param name="video">The video.</param>
+        /// <param name="currentStreams">The current streams.</param>
+        /// <param name="options">The refreshOptions.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        private async Task AddExternalAudioAsync(
+            Video video,
+            List<MediaStream> currentStreams,
+            MetadataRefreshOptions options,
+            CancellationToken cancellationToken)
+        {
+            var startIndex = currentStreams.Count == 0 ? 0 : currentStreams.Max(i => i.Index) + 1;
+            var externalAudioStreams = _audioResolver.GetExternalAudioStreams(video, startIndex, options.DirectoryService, false, cancellationToken);
+
+            await foreach (MediaStream externalAudioStream in externalAudioStreams)
+            {
+                currentStreams.Add(externalAudioStream);
+            }
+
+            // Select all external audio file paths
+            video.AudioFiles = currentStreams.Where(i => i.Type == MediaStreamType.Audio && i.IsExternal).Select(i => i.Path).Distinct().ToArray();
+        }
+
         /// <summary>
         /// Creates dummy chapters.
         /// </summary>

+ 2 - 1
MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs

@@ -7,6 +7,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
@@ -66,7 +67,7 @@ namespace MediaBrowser.Providers.MediaInfo
         {
             var options = GetOptions();
 
-            var types = new[] { "Episode", "Movie" };
+            var types = new[] { BaseItemKind.Episode, BaseItemKind.Movie };
 
             var dict = new Dictionary<Guid, BaseItem>();
 

+ 13 - 9
bump_version

@@ -52,7 +52,8 @@ echo $old_version
 
 # Set the build.yaml version to the specified new_version
 old_version_sed="$( sed 's/\./\\./g' <<<"${old_version}" )" # Escape the '.' chars
-sed -i "s/${old_version_sed}/${new_version}/g" ${build_file}
+new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
+sed -i "s/${old_version_sed}/${new_version_sed}/g" ${build_file}
 
 # update nuget package version
 for subproject in ${jellyfin_subprojects[@]}; do
@@ -64,26 +65,29 @@ for subproject in ${jellyfin_subprojects[@]}; do
             | sed -E 's/<VersionPrefix>([0-9\.]+[-a-z0-9]*)<\/VersionPrefix>/\1/'
     )"
     echo old nuget version: $old_version
+    new_version_sed="$( cut -f1 -d'-' <<<"${new_version}" )"
 
     # Set the nuget version to the specified new_version
-    sed -i "s|${old_version}|${new_version}|g" ${subproject}
+    sed -i "s|${old_version}|${new_version_sed}|g" ${subproject}
 done
 
 if [[ ${new_version} == *"-"* ]]; then
-    new_version_deb="$( sed 's/-/~/g' <<<"${new_version}" )"
+    new_version_pkg="$( sed 's/-/~/g' <<<"${new_version}" )"
+    new_version_deb_sup=""
 else
-    new_version_deb="${new_version}-1"
+    new_version_pkg="${new_version}"
+    new_version_deb_sup="-1"
 fi
 
 # Update the metapackage equivs file
 debian_equivs_file="debian/metapackage/jellyfin"
-sed -i "s/${old_version_sed}/${new_version}/g" ${debian_equivs_file}
+sed -i "s/${old_version_sed}/${new_version_pkg}/g" ${debian_equivs_file}
 
 # Write out a temporary Debian changelog with our new stuff appended and some templated formatting
 debian_changelog_file="debian/changelog"
 debian_changelog_temp="$( mktemp )"
 # Create new temp file with our changelog
-echo -e "jellyfin-server (${new_version_deb}) unstable; urgency=medium
+echo -e "jellyfin-server (${new_version_pkg}${new_version_deb_sup}) unstable; urgency=medium
 
   * New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v${new_version}
 
@@ -104,7 +108,7 @@ pushd ${fedora_spec_temp_dir}
 # Split out the stuff before and after changelog
 csplit jellyfin.spec  "/^%changelog/" # produces xx00 xx01
 # Update the version in xx00
-sed -i "s/${old_version_sed}/${new_version_sed}/g" xx00
+sed -i "s/${old_version_sed}/${new_version_pkg}/g" xx00
 # Remove the header from xx01
 sed -i '/^%changelog/d' xx01
 # Create new temp file with our changelog
@@ -121,5 +125,5 @@ mv ${fedora_spec_temp} ${fedora_spec_file}
 rm -rf ${fedora_spec_temp_dir}
 
 # Stage the changed files for commit
-git add ${shared_version_file} ${build_file} ${debian_equivs_file} ${debian_changelog_file} ${fedora_spec_file}
-git status
+git add .
+git status -v

+ 14 - 1
debian/jellyfin.service

@@ -13,7 +13,20 @@ TimeoutSec = 15
 NoNewPrivileges=true
 SystemCallArchitectures=native
 RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
-ProtectKernelModules=True
+RestrictNamespaces=true
+RestrictRealtime=true
+RestrictSUIDSGID=true
+ProtectClock=true
+ProtectControlGroups=true
+ProtectHostname=true
+ProtectKernelLogs=true
+ProtectKernelModules=true
+ProtectKernelTunables=true
+LockPersonality=true
+PrivateTmp=true
+PrivateDevices=false
+PrivateUsers=true
+RemoveIPC=true
 SystemCallFilter=~@clock
 SystemCallFilter=~@aio
 SystemCallFilter=~@chown

+ 37 - 22
fedora/Makefile

@@ -1,26 +1,41 @@
 VERSION := $(shell sed -ne '/^Version:/s/.*  *//p' fedora/jellyfin.spec)
+outdir  ?= fedora/
+TARGET  ?= fedora-35-x86_64
 
 srpm:
-	cd fedora/;                                                                   \
-	SOURCE_DIR=..                                                                 \
-	WORKDIR="$${PWD}";                                                            \
-	tar                                                                           \
-	--transform "s,^\.,jellyfin-server-$(VERSION),"                               \
-	--exclude='.git*'                                                             \
-	--exclude='**/.git'                                                           \
-	--exclude='**/.hg'                                                            \
-	--exclude='**/.vs'                                                            \
-	--exclude='**/.vscode'                                                        \
-	--exclude='deployment'                                                        \
-	--exclude='**/bin'                                                            \
-	--exclude='**/obj'                                                            \
-	--exclude='**/.nuget'                                                         \
-	--exclude='*.deb'                                                             \
-	--exclude='*.rpm'                                                             \
-	--exclude='jellyfin-server-$(VERSION).tar.gz'                                 \
-	-czf "jellyfin-server-$(VERSION).tar.gz"                                      \
-	-C $${SOURCE_DIR} ./
-	cd fedora/;                                                                   \
-	rpmbuild -bs jellyfin.spec                                                    \
-	         --define "_sourcedir $$PWD/"                                         \
+	pushd fedora/;                                              \
+	if [ "$$(id -u)" = "0" ]; then                              \
+	    dnf -y install git;                                     \
+	fi;                                                         \
+	version=$$(git describe --tags | sed -e 's/^v//'            \
+	                                     -e 's/-[0-9]*-g.*$$//' \
+	                                     -e 's/-/~/');          \
+	SOURCE_DIR=..                                               \
+	WORKDIR="$${PWD}";                                          \
+	tar                                                         \
+	--transform "s,^\.,jellyfin-server-$$version,"              \
+	--exclude='.git*'                                           \
+	--exclude='**/.git'                                         \
+	--exclude='**/.hg'                                          \
+	--exclude='**/.vs'                                          \
+	--exclude='**/.vscode'                                      \
+	--exclude=deployment                                        \
+	--exclude='**/bin'                                          \
+	--exclude='**/obj'                                          \
+	--exclude='**/.nuget'                                       \
+	--exclude='*.deb'                                           \
+	--exclude='*.rpm'                                           \
+	--exclude=jellyfin-server-$$version.tar.gz                  \
+	-czf "jellyfin-server-$$version.tar.gz"                     \
+	-C $${SOURCE_DIR} ./;                                       \
+	popd;                                                       \
+	./bump_version $$version
+	cd fedora/;                              \
+	rpmbuild -bs jellyfin.spec               \
+	         --define "_sourcedir $$PWD/"    \
 	         --define "_srcrpmdir $(outdir)"
+
+rpms: fedora/jellyfin-$(shell git describe --tags | sed -e 's/^v//' -e 's/-[0-9]*-g.*$$//' -e 's/-/~/')-1$(shell rpm --eval %dist).src.rpm
+	mock --addrepo=https://download.copr.fedorainfracloud.org/results/@dotnet-sig/dotnet-preview/$(TARGET)/ \
+	     --enable-network                                                                                   \
+	     -r $(TARGET) $<

+ 4 - 0
fedora/jellyfin-server-lowports.conf

@@ -0,0 +1,4 @@
+# This allows Jellyfin to bind to low ports such as 80 and/or 443
+
+[Service]
+AmbientCapabilities=CAP_NET_BIND_SERVICE

+ 19 - 1
fedora/jellyfin.spec

@@ -12,7 +12,7 @@ Release:        1%{?dist}
 Summary:        The Free Software Media System
 License:        GPLv3
 URL:            https://jellyfin.org
-# Jellyfin Server tarball created by `make -f .copr/Makefile srpm`, real URL ends with `v%{version}.tar.gz`
+# Jellyfin Server tarball created by `make -f .copr/Makefile srpm`, real URL ends with `v%%{version}.tar.gz`
 Source0:        jellyfin-server-%{version}.tar.gz
 Source11:       jellyfin.service
 Source12:       jellyfin.env
@@ -20,6 +20,7 @@ Source13:       jellyfin.sudoers
 Source14:       restart.sh
 Source15:       jellyfin.override.conf
 Source16:       jellyfin-firewalld.xml
+Source17:       jellyfin-server-lowports.conf
 
 %{?systemd_requires}
 BuildRequires:  systemd
@@ -45,6 +46,16 @@ Requires:       libcurl, fontconfig, freetype, openssl, glibc, libicu, at, sudo
 %description server
 The Jellyfin media server backend.
 
+%package server-lowports
+# RPMfusion free
+Summary:        The Free Software Media System Server backend.  Low-port binding.
+Requires:       jellyfin-server
+
+%description server-lowports
+The Jellyfin media server backend low port binding package.  This package
+enables binding to ports < 1024.  You would install this if you want
+the Jellyfin server to bind to ports 80 and/or 443 for example.
+
 %prep
 %autosetup -n jellyfin-server-%{version} -b 0
 
@@ -57,6 +68,7 @@ dotnet publish --configuration Release --output='%{buildroot}%{_libdir}/jellyfin
     "-p:DebugSymbols=false;DebugType=none" Jellyfin.Server
 %{__install} -D -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/jellyfin/LICENSE
 %{__install} -D -m 0644 %{SOURCE15} %{buildroot}%{_sysconfdir}/systemd/system/jellyfin.service.d/override.conf
+%{__install} -D -m 0644 %{SOURCE17} %{buildroot}%{_unitdir}/jellyfin.service.d/jellyfin-server-lowports.conf
 %{__install} -D -m 0644 Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json
 %{__mkdir} -p %{buildroot}%{_bindir}
 tee %{buildroot}%{_bindir}/jellyfin << EOF
@@ -95,6 +107,9 @@ EOF
 %attr(750,jellyfin,jellyfin) %dir %{_var}/cache/jellyfin
 %{_datadir}/licenses/jellyfin/LICENSE
 
+%files server-lowports
+%{_unitdir}/jellyfin.service.d/jellyfin-server-lowports.conf
+
 %pre server
 getent group jellyfin >/dev/null || groupadd -r jellyfin
 getent passwd jellyfin >/dev/null || \
@@ -137,6 +152,9 @@ fi
 %systemd_postun_with_restart jellyfin.service
 
 %changelog
+* Mon Nov 29 2021 Brian J. Murrell <brian@interlinx.bc.ca>
+- Add jellyfin-server-lowports.service drop-in in a server-lowports
+  subpackage to allow binding to low ports
 * Fri Dec 04 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
 - Forthcoming stable release
 * Mon Jul 27 2020 Jellyfin Packaging Team <packaging@jellyfin.org>

+ 0 - 30
tests/Jellyfin.Api.Tests/Helpers/RequestHelpersTests.cs

@@ -55,35 +55,5 @@ namespace Jellyfin.Api.Tests.Helpers
 
             return data;
         }
-
-        [Fact]
-        public static void GetItemTypeStrings_Empty_Empty()
-        {
-            Assert.Empty(RequestHelpers.GetItemTypeStrings(Array.Empty<BaseItemKind>()));
-        }
-
-        [Fact]
-        public static void GetItemTypeStrings_Valid_Success()
-        {
-            BaseItemKind[] input =
-            {
-                BaseItemKind.AggregateFolder,
-                BaseItemKind.Audio,
-                BaseItemKind.BasePluginFolder,
-                BaseItemKind.CollectionFolder
-            };
-
-            string[] expected =
-            {
-                "AggregateFolder",
-                "Audio",
-                "BasePluginFolder",
-                "CollectionFolder"
-            };
-
-            var res = RequestHelpers.GetItemTypeStrings(input);
-
-            Assert.Equal(expected, res);
-        }
     }
 }

+ 1 - 6
tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs

@@ -1,8 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
 using MediaBrowser.Model.Net;
 using Xunit;
 
@@ -129,7 +124,7 @@ namespace Jellyfin.Model.Tests.Net
         [InlineData("font/woff2", ".woff2")]
         [InlineData("image/bmp", ".bmp")]
         [InlineData("image/gif", ".gif")]
-        [InlineData("image/jpg", ".jpg")]
+        [InlineData("image/jpeg", ".jpg")]
         [InlineData("image/png", ".png")]
         [InlineData("image/svg+xml", ".svg")]
         [InlineData("image/tiff", ".tif")]

+ 12 - 0
tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs

@@ -11,6 +11,18 @@ namespace Jellyfin.Server.Implementations.Tests.Library
         [InlineData("Superman: Red Son - tt10985510", "imdbid", "tt10985510")]
         [InlineData("Superman: Red Son", "imdbid", null)]
         [InlineData("Superman: Red Son", "something", null)]
+        [InlineData("Superman: Red Son [imdbid1=tt11111111][imdbid=tt10985510]", "imdbid", "tt10985510")]
+        [InlineData("Superman: Red Son [tmdbid=618355][imdbid=tt10985510]", "imdbid", "tt10985510")]
+        [InlineData("Superman: Red Son [tmdbid=618355][imdbid=tt10985510]", "tmdbid", "618355")]
+        [InlineData("[tmdbid=618355]", "tmdbid", "618355")]
+        [InlineData("tmdbid=111111][tmdbid=618355]", "tmdbid", "618355")]
+        [InlineData("[tmdbid=618355]tmdbid=111111]", "tmdbid", "618355")]
+        [InlineData("tmdbid=618355]", "tmdbid", null)]
+        [InlineData("[tmdbid=618355", "tmdbid", null)]
+        [InlineData("tmdbid=618355", "tmdbid", null)]
+        [InlineData("tmdbid=", "tmdbid", null)]
+        [InlineData("tmdbid", "tmdbid", null)]
+        [InlineData("[tmdbid=][imdbid=tt10985510]", "tmdbid", null)]
         public void GetAttributeValue_ValidArgs_Correct(string input, string attribute, string? expectedResult)
         {
             Assert.Equal(expectedResult, PathExtensions.GetAttributeValue(input, attribute));