Quellcode durchsuchen

Merge branch 'master' into RemoteAccessFix

BaronGreenback vor 4 Jahren
Ursprung
Commit
5d16d1f66d
100 geänderte Dateien mit 648 neuen und 1222 gelöschten Zeilen
  1. 0 4
      .ci/azure-pipelines-package.yml
  2. 7 2
      Emby.Dlna/DlnaManager.cs
  3. 2 1
      Emby.Dlna/Main/DlnaEntryPoint.cs
  4. 1 1
      Emby.Dlna/Ssdp/DeviceDiscovery.cs
  5. 6 1
      Emby.Drawing/ImageProcessor.cs
  6. 8 7
      Emby.Naming/Video/CleanStringParser.cs
  7. 8 7
      Emby.Naming/Video/VideoListResolver.cs
  8. 2 1
      Emby.Naming/Video/VideoResolver.cs
  9. 1 4
      Emby.Server.Implementations/ApplicationHost.cs
  10. 1 1
      Emby.Server.Implementations/Channels/ChannelManager.cs
  11. 14 1
      Emby.Server.Implementations/Collections/CollectionManager.cs
  12. 1 1
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  13. 1 0
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  14. 1 1
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  15. 1 1
      Emby.Server.Implementations/Library/LiveStreamHelper.cs
  16. 1 1
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  17. 9 2
      Emby.Server.Implementations/Library/PathExtensions.cs
  18. 2 2
      Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
  19. 1 1
      Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
  20. 104 104
      Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
  21. 16 14
      Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
  22. 1 1
      Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
  23. 1 1
      Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs
  24. 1 1
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  25. 1 1
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
  26. 70 186
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
  27. 22 1
      Emby.Server.Implementations/Localization/Core/eo.json
  28. 1 1
      Emby.Server.Implementations/Localization/Core/kk.json
  29. 5 1
      Emby.Server.Implementations/Localization/Core/lt-LT.json
  30. 1 1
      Emby.Server.Implementations/Localization/Core/pt-BR.json
  31. 1 1
      Emby.Server.Implementations/Localization/LocalizationManager.cs
  32. 2 2
      Emby.Server.Implementations/Plugins/PluginManager.cs
  33. 1 1
      Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
  34. 1 1
      Emby.Server.Implementations/Updates/InstallationManager.cs
  35. 1 1
      Jellyfin.Api/Controllers/ConfigurationController.cs
  36. 20 3
      Jellyfin.Api/Controllers/HlsSegmentController.cs
  37. 20 3
      Jellyfin.Api/Controllers/ImageByNameController.cs
  38. 11 1
      Jellyfin.Api/Controllers/ImageController.cs
  39. 82 8
      Jellyfin.Api/Controllers/InstantMixController.cs
  40. 4 4
      Jellyfin.Api/Controllers/LibraryController.cs
  41. 7 14
      Jellyfin.Api/Controllers/NotificationsController.cs
  42. 1 1
      Jellyfin.Api/Controllers/PluginsController.cs
  43. 4 1
      Jellyfin.Api/Controllers/StartupController.cs
  44. 6 9
      Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs
  45. 19 0
      Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoPathDto.cs
  46. 30 0
      Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs
  47. 7 53
      Jellyfin.Data/DayOfWeekHelper.cs
  48. 0 29
      Jellyfin.Data/Entities/AccessSchedule.cs
  49. 5 16
      Jellyfin.Data/Entities/ActivityLog.cs
  50. 5 15
      Jellyfin.Data/Entities/CustomItemDisplayPreferences.cs
  51. 2 12
      Jellyfin.Data/Entities/DisplayPreferences.cs
  52. 0 11
      Jellyfin.Data/Entities/Group.cs
  53. 0 1
      Jellyfin.Data/Entities/HomeSection.cs
  54. 0 11
      Jellyfin.Data/Entities/ImageInfo.cs
  55. 0 9
      Jellyfin.Data/Entities/ItemDisplayPreferences.cs
  56. 1 15
      Jellyfin.Data/Entities/Libraries/Artwork.cs
  57. 2 1
      Jellyfin.Data/Entities/Libraries/Book.cs
  58. 1 22
      Jellyfin.Data/Entities/Libraries/BookMetadata.cs
  59. 2 21
      Jellyfin.Data/Entities/Libraries/Chapter.cs
  60. 2 1
      Jellyfin.Data/Entities/Libraries/Collection.cs
  61. 5 35
      Jellyfin.Data/Entities/Libraries/CollectionItem.cs
  62. 2 15
      Jellyfin.Data/Entities/Libraries/Company.cs
  63. 5 20
      Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs
  64. 2 1
      Jellyfin.Data/Entities/Libraries/CustomItem.cs
  65. 1 20
      Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs
  66. 2 20
      Jellyfin.Data/Entities/Libraries/Episode.cs
  67. 4 22
      Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs
  68. 1 26
      Jellyfin.Data/Entities/Libraries/Genre.cs
  69. 2 14
      Jellyfin.Data/Entities/Libraries/ItemMetadata.cs
  70. 3 19
      Jellyfin.Data/Entities/Libraries/Library.cs
  71. 0 8
      Jellyfin.Data/Entities/Libraries/LibraryItem.cs
  72. 1 20
      Jellyfin.Data/Entities/Libraries/MediaFile.cs
  73. 3 20
      Jellyfin.Data/Entities/Libraries/MediaFileStream.cs
  74. 0 11
      Jellyfin.Data/Entities/Libraries/MetadataProvider.cs
  75. 3 20
      Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs
  76. 2 1
      Jellyfin.Data/Entities/Libraries/Movie.cs
  77. 5 20
      Jellyfin.Data/Entities/Libraries/MovieMetadata.cs
  78. 2 1
      Jellyfin.Data/Entities/Libraries/MusicAlbum.cs
  79. 4 17
      Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs
  80. 1 12
      Jellyfin.Data/Entities/Libraries/Person.cs
  81. 5 23
      Jellyfin.Data/Entities/Libraries/PersonRole.cs
  82. 2 1
      Jellyfin.Data/Entities/Libraries/Photo.cs
  83. 1 20
      Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs
  84. 2 21
      Jellyfin.Data/Entities/Libraries/Rating.cs
  85. 3 22
      Jellyfin.Data/Entities/Libraries/RatingSource.cs
  86. 1 15
      Jellyfin.Data/Entities/Libraries/Release.cs
  87. 2 20
      Jellyfin.Data/Entities/Libraries/Season.cs
  88. 2 20
      Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs
  89. 2 1
      Jellyfin.Data/Entities/Libraries/Series.cs
  90. 5 25
      Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs
  91. 2 20
      Jellyfin.Data/Entities/Libraries/Track.cs
  92. 1 20
      Jellyfin.Data/Entities/Libraries/TrackMetadata.cs
  93. 2 8
      Jellyfin.Data/Entities/Permission.cs
  94. 0 9
      Jellyfin.Data/Entities/Preference.cs
  95. 25 33
      Jellyfin.Data/Entities/User.cs
  96. 1 1
      Jellyfin.Data/Interfaces/IHasPermissions.cs
  97. 4 6
      Jellyfin.Data/Jellyfin.Data.csproj
  98. 7 4
      Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
  99. 4 9
      Jellyfin.Drawing.Skia/SkiaEncoder.cs
  100. 2 4
      Jellyfin.Networking/Jellyfin.Networking.csproj

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

@@ -160,7 +160,6 @@ jobs:
   dependsOn:
   - BuildPackage
   - BuildDocker
-  condition: and(succeeded('BuildPackage'), succeeded('BuildDocker'))
 
   pool:
     vmImage: 'ubuntu-latest'
@@ -186,9 +185,6 @@ jobs:
 
 - job: PublishNuget
   displayName: 'Publish NuGet packages'
-  dependsOn:
-  - BuildPackage
-  condition: succeeded('BuildPackage')
 
   pool:
     vmImage: 'ubuntu-latest'

+ 7 - 2
Emby.Dlna/DlnaManager.cs

@@ -36,7 +36,7 @@ namespace Emby.Dlna
         private readonly ILogger<DlnaManager> _logger;
         private readonly IServerApplicationHost _appHost;
         private static readonly Assembly _assembly = typeof(DlnaManager).Assembly;
-        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
 
         private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
 
@@ -333,7 +333,12 @@ namespace Emby.Dlna
                 throw new ArgumentNullException(nameof(id));
             }
 
-            var info = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
+            var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
+
+            if (info == null)
+            {
+                return null;
+            }
 
             return ParseProfileFile(info.Path, info.Info.Type);
         }

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

@@ -128,7 +128,8 @@ namespace Emby.Dlna.Main
 
             _netConfig = config.GetConfiguration<NetworkConfiguration>("network");
             _disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
-            if (_disabled)
+
+            if (_disabled && _config.GetDlnaConfiguration().EnableServer)
             {
                 _logger.LogError("The DLNA specification does not support HTTPS.");
             }

+ 1 - 1
Emby.Dlna/Ssdp/DeviceDiscovery.cs

@@ -69,7 +69,7 @@ namespace Emby.Dlna.Ssdp
         {
             lock (_syncLock)
             {
-                if (_listenerCount > 0 && _deviceLocator == null)
+                if (_listenerCount > 0 && _deviceLocator == null && _commsServer != null)
                 {
                     _deviceLocator = new SsdpDeviceLocator(_commsServer);
 

+ 6 - 1
Emby.Drawing/ImageProcessor.cs

@@ -352,8 +352,13 @@ namespace Emby.Drawing
         }
 
         /// <inheritdoc />
-        public string GetImageCacheTag(User user)
+        public string? GetImageCacheTag(User user)
         {
+            if (user.ProfileImage == null)
+            {
+                return null;
+            }
+
             return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5()
                 .ToString("N", CultureInfo.InvariantCulture);
         }

+ 8 - 7
Emby.Naming/Video/CleanStringParser.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Text.RegularExpressions;
 
 namespace Emby.Naming.Video
@@ -16,8 +17,14 @@ namespace Emby.Naming.Video
         /// <param name="expressions">List of regex to parse name and year from.</param>
         /// <param name="newName">Parsing result string.</param>
         /// <returns>True if parsing was successful.</returns>
-        public static bool TryClean(string name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName)
+        public static bool TryClean([NotNullWhen(true)] string? name, IReadOnlyList<Regex> expressions, out ReadOnlySpan<char> newName)
         {
+            if (string.IsNullOrEmpty(name))
+            {
+                newName = ReadOnlySpan<char>.Empty;
+                return false;
+            }
+
             var len = expressions.Count;
             for (int i = 0; i < len; i++)
             {
@@ -33,12 +40,6 @@ namespace Emby.Naming.Video
 
         private static bool TryClean(string name, Regex expression, out ReadOnlySpan<char> newName)
         {
-            if (string.IsNullOrEmpty(name))
-            {
-                newName = ReadOnlySpan<char>.Empty;
-                return false;
-            }
-
             var match = expression.Match(name);
             int index = match.Index;
             if (match.Success && index != 0)

+ 8 - 7
Emby.Naming/Video/VideoListResolver.cs

@@ -221,20 +221,21 @@ namespace Emby.Naming.Video
             string testFilename = Path.GetFileNameWithoutExtension(testFilePath);
             if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
             {
-                if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
+                // Remove the folder name before cleaning as we don't care about cleaning that part
+                if (folderName.Length <= testFilename.Length)
                 {
-                    testFilename = cleanName.ToString();
+                    testFilename = testFilename.Substring(folderName.Length).Trim();
                 }
 
-                if (folderName.Length <= testFilename.Length)
+                if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
                 {
-                    testFilename = testFilename.Substring(folderName.Length).Trim();
+                    testFilename = cleanName.Trim().ToString();
                 }
 
+                // The CleanStringParser should have removed common keywords etc.
                 return string.IsNullOrEmpty(testFilename)
-                   || testFilename[0] == '-'
-                   || testFilename[0] == '_'
-                   || string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty));
+                       || testFilename[0] == '-'
+                       || Regex.IsMatch(testFilename, @"^\[([^]]*)\]");
             }
 
             return false;

+ 2 - 1
Emby.Naming/Video/VideoResolver.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Diagnostics.CodeAnalysis;
 using System.IO;
 using System.Linq;
 using Emby.Naming.Common;
@@ -146,7 +147,7 @@ namespace Emby.Naming.Video
         /// <param name="name">Raw name.</param>
         /// <param name="newName">Clean name.</param>
         /// <returns>True if cleaning of name was successful.</returns>
-        public bool TryCleanString(string name, out ReadOnlySpan<char> newName)
+        public bool TryCleanString([NotNullWhen(true)] string? name, out ReadOnlySpan<char> newName)
         {
             return CleanStringParser.TryClean(name, _options.CleanStringRegexes, out newName);
         }

+ 1 - 4
Emby.Server.Implementations/ApplicationHost.cs

@@ -10,8 +10,6 @@ using System.Net;
 using System.Reflection;
 using System.Runtime.InteropServices;
 using System.Security.Cryptography.X509Certificates;
-using System.Text;
-using System.Text.Json;
 using System.Threading;
 using System.Threading.Tasks;
 using Emby.Dlna;
@@ -51,7 +49,6 @@ using Jellyfin.Networking.Manager;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Events;
-using MediaBrowser.Common.Json;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Plugins;
 using MediaBrowser.Common.Updates;
@@ -470,7 +467,7 @@ namespace Emby.Server.Implementations
         }
 
         /// <inheritdoc />
-        public IReadOnlyCollection<T> GetExports<T>(CreationDelegate defaultFunc, bool manageLifetime = true)
+        public IReadOnlyCollection<T> GetExports<T>(CreationDelegateFactory defaultFunc, bool manageLifetime = true)
         {
             // Convert to list so this isn't executed for each iteration
             var parts = GetExportTypes<T>()

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

@@ -49,7 +49,7 @@ namespace Emby.Server.Implementations.Channels
         private readonly IProviderManager _providerManager;
         private readonly IMemoryCache _memoryCache;
         private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
-        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ChannelManager"/> class.

+ 14 - 1
Emby.Server.Implementations/Collections/CollectionManager.cs

@@ -344,7 +344,20 @@ namespace Emby.Server.Implementations.Collections
                     }
                     else
                     {
-                        results[item.Id] = item;
+                        var alreadyInResults = false;
+                        foreach (var child in item.GetMediaSources(true))
+                        {
+                            if (Guid.TryParse(child.Id, out var id) && results.ContainsKey(id))
+                            {
+                                alreadyInResults = true;
+                                break;
+                            }
+                        }
+
+                        if (!alreadyInResults)
+                        {
+                            results[item.Id] = item;
+                        }
                     }
                 }
             }

+ 1 - 1
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -88,7 +88,7 @@ namespace Emby.Server.Implementations.Data
             _imageProcessor = imageProcessor;
 
             _typeMapper = new TypeMapper();
-            _jsonOptions = JsonDefaults.GetOptions();
+            _jsonOptions = JsonDefaults.Options;
 
             DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db");
         }

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

@@ -27,6 +27,7 @@
     <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
     <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.3" />
     <PackageReference Include="Mono.Nat" Version="3.0.1" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.1" />
     <PackageReference Include="sharpcompress" Version="0.28.1" />

+ 1 - 1
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs

@@ -56,7 +56,7 @@ namespace Emby.Server.Implementations.HttpServer
             RemoteEndPoint = remoteEndPoint;
             QueryString = query;
 
-            _jsonOptions = JsonDefaults.GetOptions();
+            _jsonOptions = JsonDefaults.Options;
             LastActivityDate = DateTime.Now;
         }
 

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

@@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Library
         private readonly IMediaEncoder _mediaEncoder;
         private readonly ILogger _logger;
         private readonly IApplicationPaths _appPaths;
-        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
 
         public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IApplicationPaths appPaths)
         {

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

@@ -46,7 +46,7 @@ namespace Emby.Server.Implementations.Library
 
         private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
         private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
-        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
 
         private IMediaSourceProvider[] _providers;
 

+ 9 - 2
Emby.Server.Implementations/Library/PathExtensions.cs

@@ -59,11 +59,18 @@ namespace Emby.Server.Implementations.Library
         /// <param name="newPath">The result of the sub path replacement</param>
         /// <returns>The path after replacing the sub path.</returns>
         /// <exception cref="ArgumentNullException"><paramref name="path" />, <paramref name="newSubPath" /> or <paramref name="newSubPath" /> is empty.</exception>
-        public static bool TryReplaceSubPath(this string path, string subPath, string newSubPath, [NotNullWhen(true)] out string? newPath)
+        public static bool TryReplaceSubPath(
+            [NotNullWhen(true)] this string? path,
+            [NotNullWhen(true)] string? subPath,
+            [NotNullWhen(true)] string? newSubPath,
+            [NotNullWhen(true)] out string? newPath)
         {
             newPath = null;
 
-            if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(subPath) || string.IsNullOrEmpty(newSubPath) || subPath.Length > path.Length)
+            if (string.IsNullOrEmpty(path)
+                || string.IsNullOrEmpty(subPath)
+                || string.IsNullOrEmpty(newSubPath)
+                || subPath.Length > path.Length)
             {
                 return false;
             }

+ 2 - 2
Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs

@@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
         /// </summary>
         /// <param name="args">The args.</param>
         /// <returns>`0.</returns>
-        protected override T Resolve(ItemResolveArgs args)
+        public override T Resolve(ItemResolveArgs args)
         {
             return ResolveVideo<T>(args, false);
         }
@@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
         /// <param name="args">The args.</param>
         /// <param name="parseName">if set to <c>true</c> [parse name].</param>
         /// <returns>``0.</returns>
-        protected TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
+        protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
               where TVideoType : Video, new()
         {
             var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();

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

@@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
     {
         private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" };
 
-        protected override Book Resolve(ItemResolveArgs args)
+        public override Book Resolve(ItemResolveArgs args)
         {
             var collectionType = args.GetCollectionType();
 

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

@@ -69,6 +69,110 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
             return result;
         }
 
+        /// <summary>
+        /// Resolves the specified args.
+        /// </summary>
+        /// <param name="args">The args.</param>
+        /// <returns>Video.</returns>
+        public override Video Resolve(ItemResolveArgs args)
+        {
+            var collectionType = args.GetCollectionType();
+
+            // Find movies with their own folders
+            if (args.IsDirectory)
+            {
+                if (IsInvalid(args.Parent, collectionType))
+                {
+                    return null;
+                }
+
+                var files = args.FileSystemChildren
+                    .Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
+                    .ToList();
+
+                if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
+                {
+                    return FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
+                }
+
+                if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
+                {
+                    return FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
+                }
+
+                if (string.IsNullOrEmpty(collectionType))
+                {
+                    // Owned items will be caught by the plain video resolver
+                    if (args.Parent == null)
+                    {
+                        // return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
+                        return null;
+                    }
+
+                    if (args.HasParent<Series>())
+                    {
+                        return null;
+                    }
+
+                    {
+                        return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
+                    }
+                }
+
+                if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
+                {
+                    return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
+                }
+
+                return null;
+            }
+
+            // Handle owned items
+            if (args.Parent == null)
+            {
+                return base.Resolve(args);
+            }
+
+            if (IsInvalid(args.Parent, collectionType))
+            {
+                return null;
+            }
+
+            Video item = null;
+
+            if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
+            {
+                item = ResolveVideo<MusicVideo>(args, false);
+            }
+
+            // To find a movie file, the collection type must be movies or boxsets
+            else if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
+            {
+                item = ResolveVideo<Movie>(args, true);
+            }
+            else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
+                string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
+            {
+                item = ResolveVideo<Video>(args, false);
+            }
+            else if (string.IsNullOrEmpty(collectionType))
+            {
+                if (args.HasParent<Series>())
+                {
+                    return null;
+                }
+
+                item = ResolveVideo<Video>(args, false);
+            }
+
+            if (item != null)
+            {
+                item.IsInMixedFolder = true;
+            }
+
+            return item;
+        }
+
         private MultiItemResolverResult ResolveMultipleInternal(
             Folder parent,
             List<FileSystemMetadata> files,
@@ -216,110 +320,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
             return string.Equals(result.Path, file.FullName, StringComparison.OrdinalIgnoreCase);
         }
 
-        /// <summary>
-        /// Resolves the specified args.
-        /// </summary>
-        /// <param name="args">The args.</param>
-        /// <returns>Video.</returns>
-        protected override Video Resolve(ItemResolveArgs args)
-        {
-            var collectionType = args.GetCollectionType();
-
-            // Find movies with their own folders
-            if (args.IsDirectory)
-            {
-                if (IsInvalid(args.Parent, collectionType))
-                {
-                    return null;
-                }
-
-                var files = args.FileSystemChildren
-                    .Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
-                    .ToList();
-
-                if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
-                {
-                    return FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
-                }
-
-                if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
-                {
-                    return FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
-                }
-
-                if (string.IsNullOrEmpty(collectionType))
-                {
-                    // Owned items will be caught by the plain video resolver
-                    if (args.Parent == null)
-                    {
-                        // return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
-                        return null;
-                    }
-
-                    if (args.HasParent<Series>())
-                    {
-                        return null;
-                    }
-
-                    {
-                        return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
-                    }
-                }
-
-                if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
-                {
-                    return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
-                }
-
-                return null;
-            }
-
-            // Handle owned items
-            if (args.Parent == null)
-            {
-                return base.Resolve(args);
-            }
-
-            if (IsInvalid(args.Parent, collectionType))
-            {
-                return null;
-            }
-
-            Video item = null;
-
-            if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
-            {
-                item = ResolveVideo<MusicVideo>(args, false);
-            }
-
-            // To find a movie file, the collection type must be movies or boxsets
-            else if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
-            {
-                item = ResolveVideo<Movie>(args, true);
-            }
-            else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
-            {
-                item = ResolveVideo<Video>(args, false);
-            }
-            else if (string.IsNullOrEmpty(collectionType))
-            {
-                if (args.HasParent<Series>())
-                {
-                    return null;
-                }
-
-                item = ResolveVideo<Video>(args, false);
-            }
-
-            if (item != null)
-            {
-                item.IsInMixedFolder = true;
-            }
-
-            return item;
-        }
-
         /// <summary>
         /// Sets the initial item values.
         /// </summary>

+ 16 - 14
Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Linq;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Entities;
@@ -11,12 +12,21 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
     /// </summary>
     public class EpisodeResolver : BaseVideoResolver<Episode>
     {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
+        /// </summary>
+        /// <param name="libraryManager">The library manager.</param>
+        public EpisodeResolver(ILibraryManager libraryManager)
+            : base(libraryManager)
+        {
+        }
+
         /// <summary>
         /// Resolves the specified args.
         /// </summary>
         /// <param name="args">The args.</param>
         /// <returns>Episode.</returns>
-        protected override Episode Resolve(ItemResolveArgs args)
+        public override Episode Resolve(ItemResolveArgs args)
         {
             var parent = args.Parent;
 
@@ -34,11 +44,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
                 season = parent.GetParents().OfType<Season>().FirstOrDefault();
             }
 
-            // If the parent is a Season or Series, then this is an Episode if the VideoResolver returns something
+            // If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something
             // Also handle flat tv folders
-            if (season != null ||
-                string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
-                args.HasParent<Series>())
+            if ((season != null ||
+                 string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
+                 args.HasParent<Series>())
+                && (parent is Series || !BaseItem.AllExtrasTypesFolderNames.Contains(parent.Name, StringComparer.OrdinalIgnoreCase)))
             {
                 var episode = ResolveVideo<Episode>(args, false);
 
@@ -74,14 +85,5 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
 
             return null;
         }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
-        /// </summary>
-        /// <param name="libraryManager">The library manager.</param>
-        public EpisodeResolver(ILibraryManager libraryManager)
-            : base(libraryManager)
-        {
-        }
     }
 }

+ 1 - 1
Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs

@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         private readonly IServerApplicationPaths _appPaths;
         private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
         private readonly IServerConfigurationManager _serverConfigurationManager;
-        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
         private bool _hasExited;
         private Stream _logFileStream;
         private string _targetPath;

+ 1 - 1
Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs

@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
     {
         private readonly string _dataPath;
         private readonly object _fileDataLock = new object();
-        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
         private T[] _items;
 
         public ItemDataProvider(

+ 1 - 1
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
         private readonly ICryptoProvider _cryptoProvider;
 
         private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
-        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
         private DateTime _lastErrorResponse;
 
         public SchedulesDirect(

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

@@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             _networkManager = networkManager;
             _streamHelper = streamHelper;
 
-            _jsonOptions = JsonDefaults.GetOptions();
+            _jsonOptions = JsonDefaults.Options;
         }
 
         public string Name => "HD Homerun";

+ 70 - 186
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs

@@ -2,6 +2,7 @@
 
 using System;
 using System.Buffers;
+using System.Buffers.Binary;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Net;
@@ -10,6 +11,7 @@ using System.Text;
 using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Common;
 using MediaBrowser.Controller.LiveTv;
 
 namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
@@ -120,13 +122,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 
         private static async Task<bool> CheckTunerAvailability(NetworkStream stream, int tuner, CancellationToken cancellationToken)
         {
-            var lockkeyMsg = CreateGetMessage(tuner, "lockkey");
-            await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false);
-
             byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
             try
             {
-                int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+                var msgLen = WriteGetMessage(buffer, tuner, "lockkey");
+                await stream.WriteAsync(buffer.AsMemory(0, msgLen), cancellationToken).ConfigureAwait(false);
+
+                int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
 
                 ParseReturnMessage(buffer, receivedBytes, out string returnVal);
 
@@ -166,9 +168,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 
                     _activeTuner = i;
                     var lockKeyString = string.Format(CultureInfo.InvariantCulture, "{0:d}", lockKeyValue);
-                    var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null);
-                    await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false);
-                    int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+                    var lockkeyMsgLen = WriteSetMessage(buffer, i, "lockkey", lockKeyString, null);
+                    await stream.WriteAsync(buffer.AsMemory(0, lockkeyMsgLen), cancellationToken).ConfigureAwait(false);
+                    int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
 
                     // parse response to make sure it worked
                     if (!ParseReturnMessage(buffer, receivedBytes, out _))
@@ -178,9 +180,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 
                     foreach (var command in commands.GetCommands())
                     {
-                        var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue);
-                        await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
-                        receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+                        var channelMsgLen = WriteSetMessage(buffer, i, command.Item1, command.Item2, lockKeyValue);
+                        await stream.WriteAsync(buffer.AsMemory(0, channelMsgLen), cancellationToken).ConfigureAwait(false);
+                        receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
 
                         // parse response to make sure it worked
                         if (!ParseReturnMessage(buffer, receivedBytes, out _))
@@ -191,10 +193,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                     }
 
                     var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort);
-                    var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue);
+                    var targetMsgLen = WriteSetMessage(buffer, i, "target", targetValue, lockKeyValue);
 
-                    await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false);
-                    receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+                    await stream.WriteAsync(buffer.AsMemory(0, targetMsgLen), cancellationToken).ConfigureAwait(false);
+                    receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
 
                     // parse response to make sure it worked
                     if (!ParseReturnMessage(buffer, receivedBytes, out _))
@@ -232,9 +234,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             {
                 foreach (var command in commandList)
                 {
-                    var channelMsg = CreateSetMessage(_activeTuner, command.Item1, command.Item2, _lockkey);
-                    await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
-                    int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
+                    var channelMsgLen = WriteSetMessage(buffer, _activeTuner, command.Item1, command.Item2, _lockkey);
+                    await stream.WriteAsync(buffer.AsMemory(0, channelMsgLen), cancellationToken).ConfigureAwait(false);
+                    int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
 
                     // parse response to make sure it worked
                     if (!ParseReturnMessage(buffer, receivedBytes, out _))
@@ -265,17 +267,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
         {
             var stream = client.GetStream();
 
-            var releaseTarget = CreateSetMessage(_activeTuner, "target", "none", lockKeyValue);
-            await stream.WriteAsync(releaseTarget, 0, releaseTarget.Length).ConfigureAwait(false);
-
             var buffer = ArrayPool<byte>.Shared.Rent(8192);
             try
             {
-                await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
-                var releaseKeyMsg = CreateSetMessage(_activeTuner, "lockkey", "none", lockKeyValue);
+                var releaseTargetLen = WriteSetMessage(buffer, _activeTuner, "target", "none", lockKeyValue);
+                await stream.WriteAsync(buffer.AsMemory(0, releaseTargetLen)).ConfigureAwait(false);
+
+                await stream.ReadAsync(buffer).ConfigureAwait(false);
+                var releaseKeyMsgLen = WriteSetMessage(buffer, _activeTuner, "lockkey", "none", lockKeyValue);
                 _lockkey = null;
-                await stream.WriteAsync(releaseKeyMsg, 0, releaseKeyMsg.Length).ConfigureAwait(false);
-                await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
+                await stream.WriteAsync(buffer.AsMemory(0, releaseKeyMsgLen)).ConfigureAwait(false);
+                await stream.ReadAsync(buffer).ConfigureAwait(false);
             }
             finally
             {
@@ -283,107 +285,74 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             }
         }
 
-        private static byte[] CreateGetMessage(int tuner, string name)
+        internal static int WriteGetMessage(Span<byte> buffer, int tuner, string name)
         {
-            var byteName = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}\0", tuner, name));
-            int messageLength = byteName.Length + 10; // 4 bytes for header + 4 bytes for crc + 2 bytes for tag name and length
-
-            var message = new byte[messageLength];
+            var byteName = string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}", tuner, name);
+            int offset = WriteHeaderAndPayload(buffer, byteName);
+            return FinishPacket(buffer, offset);
+        }
 
-            int offset = InsertHeaderAndName(byteName, messageLength, message);
+        private static int WriteSetMessage(Span<byte> buffer, int tuner, string name, string value, uint? lockkey)
+        {
+            var byteName = string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}", tuner, name);
+            int offset = WriteHeaderAndPayload(buffer, byteName);
 
-            bool flipEndian = BitConverter.IsLittleEndian;
+            buffer[offset++] = GetSetValue;
+            offset += WriteNullTerminatedString(buffer.Slice(offset), value);
 
-            // calculate crc and insert at the end of the message
-            var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4));
-            if (flipEndian)
+            if (lockkey.HasValue)
             {
-                Array.Reverse(crcBytes);
+                buffer[offset++] = GetSetLockkey;
+                buffer[offset++] = 4;
+                BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(offset), lockkey.Value);
+                offset += 4;
             }
 
-            Buffer.BlockCopy(crcBytes, 0, message, offset, 4);
-
-            return message;
+            return FinishPacket(buffer, offset);
         }
 
-        private static byte[] CreateSetMessage(int tuner, string name, string value, uint? lockkey)
+        internal static int WriteNullTerminatedString(Span<byte> buffer, ReadOnlySpan<char> payload)
         {
-            var byteName = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}\0", tuner, name));
-            var byteValue = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "{0}\0", value));
+            int len = Encoding.UTF8.GetBytes(payload, buffer.Slice(1)) + 1;
 
-            int messageLength = byteName.Length + byteValue.Length + 12;
-            if (lockkey.HasValue)
-            {
-                messageLength += 6;
-            }
+            // TODO: variable length: this can be 2 bytes if len > 127
+            // Write length in front of value
+            buffer[0] = Convert.ToByte(len);
 
-            var message = new byte[messageLength];
+            // null-terminate
+            buffer[len++] = 0;
 
-            int offset = InsertHeaderAndName(byteName, messageLength, message);
+            return len;
+        }
 
-            bool flipEndian = BitConverter.IsLittleEndian;
+        private static int WriteHeaderAndPayload(Span<byte> buffer, ReadOnlySpan<char> payload)
+        {
+            // Packet type
+            BinaryPrimitives.WriteUInt16BigEndian(buffer, GetSetRequest);
 
-            message[offset++] = GetSetValue;
-            message[offset++] = Convert.ToByte(byteValue.Length);
-            Buffer.BlockCopy(byteValue, 0, message, offset, byteValue.Length);
-            offset += byteValue.Length;
-            if (lockkey.HasValue)
-            {
-                message[offset++] = GetSetLockkey;
-                message[offset++] = 4;
-                var lockKeyBytes = BitConverter.GetBytes(lockkey.Value);
-                if (flipEndian)
-                {
-                    Array.Reverse(lockKeyBytes);
-                }
+            // We write the payload length at the end
+            int offset = 4;
 
-                Buffer.BlockCopy(lockKeyBytes, 0, message, offset, 4);
-                offset += 4;
-            }
+            // Tag
+            buffer[offset++] = GetSetName;
 
-            // calculate crc and insert at the end of the message
-            var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4));
-            if (flipEndian)
-            {
-                Array.Reverse(crcBytes);
-            }
-
-            Buffer.BlockCopy(crcBytes, 0, message, offset, 4);
+            // Payload length + data
+            int strLen = WriteNullTerminatedString(buffer.Slice(offset), payload);
+            offset += strLen;
 
-            return message;
+            return offset;
         }
 
-        private static int InsertHeaderAndName(byte[] byteName, int messageLength, byte[] message)
+        private static int FinishPacket(Span<byte> buffer, int offset)
         {
-            // check to see if we need to flip endiannes
-            bool flipEndian = BitConverter.IsLittleEndian;
-            int offset = 0;
-
-            // create header bytes
-            var getSetBytes = BitConverter.GetBytes(GetSetRequest);
-            var msgLenBytes = BitConverter.GetBytes((ushort)(messageLength - 8)); // Subtrace 4 bytes for header and 4 bytes for crc
-
-            if (flipEndian)
-            {
-                Array.Reverse(getSetBytes);
-                Array.Reverse(msgLenBytes);
-            }
-
-            // insert header bytes into message
-            Buffer.BlockCopy(getSetBytes, 0, message, offset, 2);
-            offset += 2;
-            Buffer.BlockCopy(msgLenBytes, 0, message, offset, 2);
-            offset += 2;
-
-            // insert tag name and length
-            message[offset++] = GetSetName;
-            message[offset++] = Convert.ToByte(byteName.Length);
+            // Payload length
+            BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), (ushort)(offset - 4));
 
-            // insert name string
-            Buffer.BlockCopy(byteName, 0, message, offset, byteName.Length);
-            offset += byteName.Length;
+            // calculate crc and insert at the end of the message
+            var crc = Crc32.Compute(buffer.Slice(0, offset));
+            BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset), crc);
 
-            return offset;
+            return offset + 4;
         }
 
         private static bool ParseReturnMessage(byte[] buf, int numBytes, out string returnVal)
@@ -442,90 +411,5 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             returnVal = Encoding.UTF8.GetString(buf, offset, valueLength - 1); // remove null terminator
             return true;
         }
-
-        private static class HdHomerunCrc
-        {
-            private static uint[] crc_table = {
-            0x00000000, 0x77073096, 0xee0e612c, 0x990951ba,
-            0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
-            0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
-            0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
-            0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de,
-            0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
-            0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
-            0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
-            0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
-            0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
-            0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940,
-            0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
-            0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116,
-            0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
-            0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
-            0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
-            0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a,
-            0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
-            0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818,
-            0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
-            0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
-            0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
-            0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c,
-            0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
-            0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2,
-            0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
-            0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
-            0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
-            0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086,
-            0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
-            0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4,
-            0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
-            0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
-            0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
-            0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
-            0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
-            0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe,
-            0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
-            0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
-            0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
-            0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252,
-            0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
-            0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60,
-            0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
-            0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
-            0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
-            0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04,
-            0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
-            0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
-            0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
-            0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
-            0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
-            0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e,
-            0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
-            0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c,
-            0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
-            0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
-            0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
-            0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0,
-            0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
-            0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6,
-            0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
-            0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
-            0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d };
-
-            public static uint GetCrc32(byte[] bytes, int numBytes)
-            {
-                var hash = 0xffffffff;
-                for (var i = 0; i < numBytes; i++)
-                {
-                    hash = (hash >> 8) ^ crc_table[(hash ^ bytes[i]) & 0xff];
-                }
-
-                var tmp = ~hash & 0xffffffff;
-                var b0 = tmp & 0xff;
-                var b1 = (tmp >> 8) & 0xff;
-                var b2 = (tmp >> 16) & 0xff;
-                var b3 = (tmp >> 24) & 0xff;
-                return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
-            }
-        }
     }
 }

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

@@ -22,5 +22,26 @@
     "Artists": "Artistoj",
     "Application": "Aplikaĵo",
     "AppDeviceValues": "Aplikaĵo: {0}, Aparato: {1}",
-    "Albums": "Albumoj"
+    "Albums": "Albumoj",
+    "TasksLibraryCategory": "Libraro",
+    "VersionNumber": "Versio {0}",
+    "UserDownloadingItemWithValues": "{0} elŝutas {1}",
+    "UserCreatedWithName": "Uzanto {0} kreiĝis",
+    "User": "Uzanto",
+    "System": "Sistemo",
+    "Songs": "Kantoj",
+    "ScheduledTaskStartedWithName": "{0} komencis",
+    "ScheduledTaskFailedWithName": "{0} malsukcesis",
+    "PluginUninstalledWithName": "{0} malinstaliĝis",
+    "PluginInstalledWithName": "{0} instaliĝis",
+    "Plugin": "Kromprogramo",
+    "Playlists": "Ludlistoj",
+    "Photos": "Fotoj",
+    "NotificationOptionPluginUninstalled": "Kromprogramo malinstaliĝis",
+    "NotificationOptionNewLibraryContent": "Nova enhavo aldoniĝis",
+    "NotificationOptionPluginInstalled": "Kromprogramo instaliĝis",
+    "MusicVideos": "Muzikvideoj",
+    "LabelIpAddressValue": "IP-adreso: {0}",
+    "Genres": "Ĝenroj",
+    "DeviceOfflineWithName": "{0} malkonektis"
 }

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

@@ -116,7 +116,7 @@
     "TaskRefreshPeopleDescription": "Tasyğyşhanadağy aktörler men rejisörler metaderekterın jaŋartady.",
     "TaskCleanLogsDescription": "{0} künnen asqan jūrnal faildaryn joiady.",
     "TaskRefreshLibraryDescription": "Tasyğyşhanadağy jaŋa faildardy skanerleidі jäne metaderekterdı jaŋğyrtady.",
-    "TaskRefreshChapterImagesDescription": "Sahnalary bar beineler üşіn nobailar jasaidy.",
+    "TaskRefreshChapterImagesDescription": "Sahnalary bar beineler üşın nobailar jasaidy.",
     "TaskCleanCacheDescription": "Jüiede qajet emes keştelgen faildardy joiady.",
     "TaskCleanActivityLogDescription": "Äreket jūrnalyndağy teŋşelgen jasynan asqan jazbalary joiady."
 }

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

@@ -113,5 +113,9 @@
     "TasksChannelsCategory": "Internetiniai Kanalai",
     "TasksApplicationCategory": "Programa",
     "TasksLibraryCategory": "Mediateka",
-    "TasksMaintenanceCategory": "Priežiūra"
+    "TasksMaintenanceCategory": "Priežiūra",
+    "TaskCleanActivityLog": "Švarus veiklos žurnalas",
+    "Undefined": "Neapibrėžtas",
+    "Forced": "Priverstas",
+    "Default": "Numatytas"
 }

+ 1 - 1
Emby.Server.Implementations/Localization/Core/pt-BR.json

@@ -16,7 +16,7 @@
     "Folders": "Pastas",
     "Genres": "Gêneros",
     "HeaderAlbumArtists": "Artistas do Álbum",
-    "HeaderContinueWatching": "Continuar Assistindo",
+    "HeaderContinueWatching": "Continuar assistindo",
     "HeaderFavoriteAlbums": "Álbuns Favoritos",
     "HeaderFavoriteArtists": "Artistas favoritos",
     "HeaderFavoriteEpisodes": "Episódios favoritos",

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

@@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Localization
 
         private List<CultureDto> _cultures;
 
-        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="LocalizationManager" /> class.

+ 2 - 2
Emby.Server.Implementations/Plugins/PluginManager.cs

@@ -70,7 +70,7 @@ namespace Emby.Server.Implementations.Plugins
             _logger = logger ?? throw new ArgumentNullException(nameof(logger));
             _pluginsPath = pluginsPath;
             _appVersion = appVersion ?? throw new ArgumentNullException(nameof(appVersion));
-            _jsonOptions = new JsonSerializerOptions(JsonDefaults.GetOptions())
+            _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options)
             {
                 WriteIndented = true
             };
@@ -678,7 +678,7 @@ namespace Emby.Server.Implementations.Plugins
                 var entry = versions[x];
                 if (!string.Equals(lastName, entry.Name, StringComparison.OrdinalIgnoreCase))
                 {
-                    entry.DllFiles.AddRange(Directory.EnumerateFiles(entry.Path, "*.dll", SearchOption.AllDirectories));
+                    entry.DllFiles = Directory.GetFiles(entry.Path, "*.dll", SearchOption.AllDirectories);
                     if (entry.IsEnabledAndSupported)
                     {
                         lastName = entry.Name;

+ 1 - 1
Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs

@@ -69,7 +69,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
         /// <summary>
         /// The options for the json Serializer.
         /// </summary>
-        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
+        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class.

+ 1 - 1
Emby.Server.Implementations/Updates/InstallationManager.cs

@@ -92,7 +92,7 @@ namespace Emby.Server.Implementations.Updates
             _httpClientFactory = httpClientFactory;
             _config = config;
             _zipClient = zipClient;
-            _jsonSerializerOptions = JsonDefaults.GetOptions();
+            _jsonSerializerOptions = JsonDefaults.Options;
             _pluginManager = pluginManager;
         }
 

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

@@ -25,7 +25,7 @@ namespace Jellyfin.Api.Controllers
         private readonly IServerConfigurationManager _configurationManager;
         private readonly IMediaEncoder _mediaEncoder;
 
-        private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.GetOptions();
+        private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.Options;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ConfigurationController"/> class.

+ 20 - 3
Jellyfin.Api/Controllers/HlsSegmentController.cs

@@ -61,7 +61,13 @@ namespace Jellyfin.Api.Controllers
         {
             // TODO: Deprecate with new iOS app
             var file = segmentId + Path.GetExtension(Request.Path);
-            file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
+            var transcodePath = _serverConfigurationManager.GetTranscodePath();
+            file = Path.GetFullPath(Path.Combine(transcodePath, file));
+            var fileDir = Path.GetDirectoryName(file);
+            if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath))
+            {
+                return BadRequest("Invalid segment.");
+            }
 
             return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext);
         }
@@ -81,7 +87,13 @@ namespace Jellyfin.Api.Controllers
         public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
         {
             var file = playlistId + Path.GetExtension(Request.Path);
-            file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
+            var transcodePath = _serverConfigurationManager.GetTranscodePath();
+            file = Path.GetFullPath(Path.Combine(transcodePath, file));
+            var fileDir = Path.GetDirectoryName(file);
+            if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath) || Path.GetExtension(file) != ".m3u8")
+            {
+                return BadRequest("Invalid segment.");
+            }
 
             return GetFileResult(file, file);
         }
@@ -130,7 +142,12 @@ namespace Jellyfin.Api.Controllers
             var file = segmentId + Path.GetExtension(Request.Path);
             var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
 
-            file = Path.Combine(transcodeFolderPath, file);
+            file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
+            var fileDir = Path.GetDirectoryName(file);
+            if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath))
+            {
+                return BadRequest("Invalid segment.");
+            }
 
             var normalizedPlaylistId = playlistId;
 

+ 20 - 3
Jellyfin.Api/Controllers/ImageByNameController.cs

@@ -74,7 +74,7 @@ namespace Jellyfin.Api.Controllers
                 : type;
 
             var path = BaseItem.SupportedImageExtensions
-                .Select(i => Path.Combine(_applicationPaths.GeneralPath, name, filename + i))
+                .Select(i => Path.GetFullPath(Path.Combine(_applicationPaths.GeneralPath, name, filename + i)))
                 .FirstOrDefault(System.IO.File.Exists);
 
             if (path == null)
@@ -82,6 +82,11 @@ namespace Jellyfin.Api.Controllers
                 return NotFound();
             }
 
+            if (!path.StartsWith(_applicationPaths.GeneralPath))
+            {
+                return BadRequest("Invalid image path.");
+            }
+
             var contentType = MimeTypes.GetMimeType(path);
             return File(System.IO.File.OpenRead(path), contentType);
         }
@@ -163,7 +168,8 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
         private ActionResult GetImageFile(string basePath, string theme, string? name)
         {
-            var themeFolder = Path.Combine(basePath, theme);
+            var themeFolder = Path.GetFullPath(Path.Combine(basePath, theme));
+
             if (Directory.Exists(themeFolder))
             {
                 var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, name + i))
@@ -171,12 +177,18 @@ namespace Jellyfin.Api.Controllers
 
                 if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
                 {
+                    if (!path.StartsWith(basePath))
+                    {
+                        return BadRequest("Invalid image path.");
+                    }
+
                     var contentType = MimeTypes.GetMimeType(path);
+
                     return PhysicalFile(path, contentType);
                 }
             }
 
-            var allFolder = Path.Combine(basePath, "all");
+            var allFolder = Path.GetFullPath(Path.Combine(basePath, "all"));
             if (Directory.Exists(allFolder))
             {
                 var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, name + i))
@@ -184,6 +196,11 @@ namespace Jellyfin.Api.Controllers
 
                 if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
                 {
+                    if (!path.StartsWith(basePath))
+                    {
+                        return BadRequest("Invalid image path.");
+                    }
+
                     var contentType = MimeTypes.GetMimeType(path);
                     return PhysicalFile(path, contentType);
                 }

+ 11 - 1
Jellyfin.Api/Controllers/ImageController.cs

@@ -196,6 +196,11 @@ namespace Jellyfin.Api.Controllers
             }
 
             var user = _userManager.GetUserById(userId);
+            if (user?.ProfileImage == null)
+            {
+                return NoContent();
+            }
+
             try
             {
                 System.IO.File.Delete(user.ProfileImage.Path);
@@ -235,6 +240,11 @@ namespace Jellyfin.Api.Controllers
             }
 
             var user = _userManager.GetUserById(userId);
+            if (user?.ProfileImage == null)
+            {
+                return NoContent();
+            }
+
             try
             {
                 System.IO.File.Delete(user.ProfileImage.Path);
@@ -1469,7 +1479,7 @@ namespace Jellyfin.Api.Controllers
             [FromQuery] int? imageIndex)
         {
             var user = _userManager.GetUserById(userId);
-            if (user == null)
+            if (user?.ProfileImage == null)
             {
                 return NotFound();
             }

+ 82 - 8
Jellyfin.Api/Controllers/InstantMixController.cs

@@ -86,7 +86,7 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Creates an instant playlist based on a given song.
+        /// Creates an instant playlist based on a given album.
         /// </summary>
         /// <param name="id">The item id.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -122,7 +122,7 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Creates an instant playlist based on a given song.
+        /// Creates an instant playlist based on a given playlist.
         /// </summary>
         /// <param name="id">The item id.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -158,7 +158,7 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Creates an instant playlist based on a given song.
+        /// Creates an instant playlist based on a given genre.
         /// </summary>
         /// <param name="name">The genre name.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -172,7 +172,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
         [HttpGet("MusicGenres/{name}/InstantMix")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenre(
+        public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreByName(
             [FromRoute, Required] string name,
             [FromQuery] Guid? userId,
             [FromQuery] int? limit,
@@ -193,7 +193,7 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Creates an instant playlist based on a given song.
+        /// Creates an instant playlist based on a given artist.
         /// </summary>
         /// <param name="id">The item id.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -229,7 +229,7 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Creates an instant playlist based on a given song.
+        /// Creates an instant playlist based on a given genre.
         /// </summary>
         /// <param name="id">The item id.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -243,7 +243,7 @@ namespace Jellyfin.Api.Controllers
         /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
         [HttpGet("MusicGenres/{id}/InstantMix")]
         [ProducesResponseType(StatusCodes.Status200OK)]
-        public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenres(
+        public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById(
             [FromRoute, Required] Guid id,
             [FromQuery] Guid? userId,
             [FromQuery] int? limit,
@@ -265,7 +265,7 @@ namespace Jellyfin.Api.Controllers
         }
 
         /// <summary>
-        /// Creates an instant playlist based on a given song.
+        /// Creates an instant playlist based on a given item.
         /// </summary>
         /// <param name="id">The item id.</param>
         /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
@@ -300,6 +300,80 @@ namespace Jellyfin.Api.Controllers
             return GetResult(items, user, limit, dtoOptions);
         }
 
+        /// <summary>
+        /// Creates an instant playlist based on a given artist.
+        /// </summary>
+        /// <param name="id">The item id.</param>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+        /// <param name="enableImages">Optional. Include image information in output.</param>
+        /// <param name="enableUserData">Optional. Include user data.</param>
+        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <response code="200">Instant playlist returned.</response>
+        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
+        [HttpGet("Artists/InstantMix")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [Obsolete("Use GetInstantMixFromArtists")]
+        public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists2(
+            [FromQuery, Required] Guid id,
+            [FromQuery] Guid? userId,
+            [FromQuery] int? limit,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+            [FromQuery] bool? enableImages,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
+        {
+            return GetInstantMixFromArtists(
+                id,
+                userId,
+                limit,
+                fields,
+                enableImages,
+                enableUserData,
+                imageTypeLimit,
+                enableImageTypes);
+        }
+
+        /// <summary>
+        /// Creates an instant playlist based on a given genre.
+        /// </summary>
+        /// <param name="id">The item id.</param>
+        /// <param name="userId">Optional. Filter by user id, and attach user data.</param>
+        /// <param name="limit">Optional. The maximum number of records to return.</param>
+        /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
+        /// <param name="enableImages">Optional. Include image information in output.</param>
+        /// <param name="enableUserData">Optional. Include user data.</param>
+        /// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
+        /// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
+        /// <response code="200">Instant playlist returned.</response>
+        /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
+        [HttpGet("MusicGenres/InstantMix")]
+        [ProducesResponseType(StatusCodes.Status200OK)]
+        [Obsolete("Use GetInstantMixFromMusicGenres instead")]
+        public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById2(
+            [FromQuery, Required] Guid id,
+            [FromQuery] Guid? userId,
+            [FromQuery] int? limit,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+            [FromQuery] bool? enableImages,
+            [FromQuery] bool? enableUserData,
+            [FromQuery] int? imageTypeLimit,
+            [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
+        {
+            return GetInstantMixFromMusicGenreById(
+                id,
+                userId,
+                limit,
+                fields,
+                enableImages,
+                enableUserData,
+                imageTypeLimit,
+                enableImageTypes);
+        }
+
         private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
         {
             var list = items;

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

@@ -303,7 +303,7 @@ namespace Jellyfin.Api.Controllers
         /// </summary>
         /// <response code="204">Library scan started.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
-        [HttpGet("Library/Refresh")]
+        [HttpPost("Library/Refresh")]
         [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
         public async Task<ActionResult> RefreshLibrary()
@@ -590,15 +590,15 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Reports that new movies have been added by an external source.
         /// </summary>
-        /// <param name="updates">A list of updated media paths.</param>
+        /// <param name="dto">The update paths.</param>
         /// <response code="204">Report success.</response>
         /// <returns>A <see cref="NoContentResult"/>.</returns>
         [HttpPost("Library/Media/Updated")]
         [Authorize(Policy = Policies.DefaultAuthorization)]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto[] updates)
+        public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto dto)
         {
-            foreach (var item in updates)
+            foreach (var item in dto.Updates)
             {
                 _libraryMonitor.ReportFileSystemChanged(item.Path);
             }

+ 7 - 14
Jellyfin.Api/Controllers/NotificationsController.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
 using System.Linq;
 using System.Threading;
 using Jellyfin.Api.Constants;
@@ -86,26 +87,19 @@ namespace Jellyfin.Api.Controllers
         /// <summary>
         /// Sends a notification to all admins.
         /// </summary>
-        /// <param name="url">The URL of the notification.</param>
-        /// <param name="level">The level of the notification.</param>
-        /// <param name="name">The name of the notification.</param>
-        /// <param name="description">The description of the notification.</param>
+        /// <param name="notificationDto">The notification request.</param>
         /// <response code="204">Notification sent.</response>
         /// <returns>A <cref see="NoContentResult"/>.</returns>
         [HttpPost("Admin")]
         [ProducesResponseType(StatusCodes.Status204NoContent)]
-        public ActionResult CreateAdminNotification(
-            [FromQuery] string? url,
-            [FromQuery] NotificationLevel? level,
-            [FromQuery] string name = "",
-            [FromQuery] string description = "")
+        public ActionResult CreateAdminNotification([FromBody, Required] AdminNotificationDto notificationDto)
         {
             var notification = new NotificationRequest
             {
-                Name = name,
-                Description = description,
-                Url = url,
-                Level = level ?? NotificationLevel.Normal,
+                Name = notificationDto.Name,
+                Description = notificationDto.Description,
+                Url = notificationDto.Url,
+                Level = notificationDto.NotificationLevel ?? NotificationLevel.Normal,
                 UserIds = _userManager.Users
                     .Where(user => user.HasPermission(PermissionKind.IsAdministrator))
                     .Select(user => user.Id)
@@ -114,7 +108,6 @@ namespace Jellyfin.Api.Controllers
             };
 
             _notificationManager.SendNotification(notification, CancellationToken.None);
-
             return NoContent();
         }
 

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

@@ -45,7 +45,7 @@ namespace Jellyfin.Api.Controllers
         {
             _installationManager = installationManager;
             _pluginManager = pluginManager;
-            _serializerOptions = JsonDefaults.GetOptions();
+            _serializerOptions = JsonDefaults.Options;
             _config = config;
         }
 

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

@@ -132,7 +132,10 @@ namespace Jellyfin.Api.Controllers
         {
             var user = _userManager.Users.First();
 
-            user.Username = startupUserDto.Name;
+            if (startupUserDto.Name != null)
+            {
+                user.Username = startupUserDto.Name;
+            }
 
             await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
 

+ 6 - 9
Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoDto.cs

@@ -1,4 +1,7 @@
-namespace Jellyfin.Api.Models.LibraryDtos
+using System;
+using System.Collections.Generic;
+
+namespace Jellyfin.Api.Models.LibraryDtos
 {
     /// <summary>
     /// Media Update Info Dto.
@@ -6,14 +9,8 @@
     public class MediaUpdateInfoDto
     {
         /// <summary>
-        /// Gets or sets media path.
-        /// </summary>
-        public string? Path { get; set; }
-
-        /// <summary>
-        /// Gets or sets media update type.
-        /// Created, Modified, Deleted.
+        /// Gets or sets the list of updates.
         /// </summary>
-        public string? UpdateType { get; set; }
+        public IReadOnlyList<MediaUpdateInfoPathDto> Updates { get; set; } = Array.Empty<MediaUpdateInfoPathDto>();
     }
 }

+ 19 - 0
Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoPathDto.cs

@@ -0,0 +1,19 @@
+namespace Jellyfin.Api.Models.LibraryDtos
+{
+    /// <summary>
+    /// The media update info path.
+    /// </summary>
+    public class MediaUpdateInfoPathDto
+    {
+        /// <summary>
+        /// Gets or sets media path.
+        /// </summary>
+        public string? Path { get; set; }
+
+        /// <summary>
+        /// Gets or sets media update type.
+        /// Created, Modified, Deleted.
+        /// </summary>
+        public string? UpdateType { get; set; }
+    }
+}

+ 30 - 0
Jellyfin.Api/Models/NotificationDtos/AdminNotificationDto.cs

@@ -0,0 +1,30 @@
+using MediaBrowser.Model.Notifications;
+
+namespace Jellyfin.Api.Models.NotificationDtos
+{
+    /// <summary>
+    /// The admin notification dto.
+    /// </summary>
+    public class AdminNotificationDto
+    {
+        /// <summary>
+        /// Gets or sets the notification name.
+        /// </summary>
+        public string? Name { get; set; }
+
+        /// <summary>
+        /// Gets or sets the notification description.
+        /// </summary>
+        public string? Description { get; set; }
+
+        /// <summary>
+        /// Gets or sets the notification level.
+        /// </summary>
+        public NotificationLevel? NotificationLevel { get; set; }
+
+        /// <summary>
+        /// Gets or sets the notification url.
+        /// </summary>
+        public string? Url { get; set; }
+    }
+}

+ 7 - 53
Jellyfin.Data/DayOfWeekHelper.cs

@@ -1,67 +1,21 @@
 #pragma warning disable CS1591
 
 using System;
-using System.Collections.Generic;
 using Jellyfin.Data.Enums;
 
 namespace Jellyfin.Data
 {
     public static class DayOfWeekHelper
     {
-        public static List<DayOfWeek> GetDaysOfWeek(DynamicDayOfWeek day)
+        public static DayOfWeek[] GetDaysOfWeek(DynamicDayOfWeek day)
         {
-            var days = new List<DayOfWeek>(7);
-
-            if (day == DynamicDayOfWeek.Sunday
-                || day == DynamicDayOfWeek.Weekend
-                || day == DynamicDayOfWeek.Everyday)
-            {
-                days.Add(DayOfWeek.Sunday);
-            }
-
-            if (day == DynamicDayOfWeek.Monday
-                || day == DynamicDayOfWeek.Weekday
-                || day == DynamicDayOfWeek.Everyday)
-            {
-                days.Add(DayOfWeek.Monday);
-            }
-
-            if (day == DynamicDayOfWeek.Tuesday
-                || day == DynamicDayOfWeek.Weekday
-                || day == DynamicDayOfWeek.Everyday)
-            {
-                days.Add(DayOfWeek.Tuesday);
-            }
-
-            if (day == DynamicDayOfWeek.Wednesday
-                || day == DynamicDayOfWeek.Weekday
-                || day == DynamicDayOfWeek.Everyday)
+            return day switch
             {
-                days.Add(DayOfWeek.Wednesday);
-            }
-
-            if (day == DynamicDayOfWeek.Thursday
-                || day == DynamicDayOfWeek.Weekday
-                || day == DynamicDayOfWeek.Everyday)
-            {
-                days.Add(DayOfWeek.Thursday);
-            }
-
-            if (day == DynamicDayOfWeek.Friday
-                || day == DynamicDayOfWeek.Weekday
-                || day == DynamicDayOfWeek.Everyday)
-            {
-                days.Add(DayOfWeek.Friday);
-            }
-
-            if (day == DynamicDayOfWeek.Saturday
-                || day == DynamicDayOfWeek.Weekend
-                || day == DynamicDayOfWeek.Everyday)
-            {
-                days.Add(DayOfWeek.Saturday);
-            }
-
-            return days;
+                DynamicDayOfWeek.Everyday => new[] { DayOfWeek.Sunday, DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday },
+                DynamicDayOfWeek.Weekday => new[] { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday },
+                DynamicDayOfWeek.Weekend => new[] { DayOfWeek.Sunday, DayOfWeek.Saturday },
+                _ => new[] { (DayOfWeek)day }
+            };
         }
     }
 }

+ 0 - 29
Jellyfin.Data/Entities/AccessSchedule.cs

@@ -1,7 +1,5 @@
 using System;
-using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
-using System.Text.Json.Serialization;
 using System.Xml.Serialization;
 using Jellyfin.Data.Enums;
 
@@ -27,14 +25,6 @@ namespace Jellyfin.Data.Entities
             EndHour = endHour;
         }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="AccessSchedule"/> class.
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </summary>
-        protected AccessSchedule()
-        {
-        }
-
         /// <summary>
         /// Gets or sets the id of this instance.
         /// </summary>
@@ -42,8 +32,6 @@ namespace Jellyfin.Data.Entities
         /// Identity, Indexed, Required.
         /// </remarks>
         [XmlIgnore]
-        [Key]
-        [Required]
         [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
         public int Id { get; protected set; }
 
@@ -51,41 +39,24 @@ namespace Jellyfin.Data.Entities
         /// Gets or sets the id of the associated user.
         /// </summary>
         [XmlIgnore]
-        [Required]
         public Guid UserId { get; protected set; }
 
         /// <summary>
         /// Gets or sets the day of week.
         /// </summary>
         /// <value>The day of week.</value>
-        [Required]
         public DynamicDayOfWeek DayOfWeek { get; set; }
 
         /// <summary>
         /// Gets or sets the start hour.
         /// </summary>
         /// <value>The start hour.</value>
-        [Required]
         public double StartHour { get; set; }
 
         /// <summary>
         /// Gets or sets the end hour.
         /// </summary>
         /// <value>The end hour.</value>
-        [Required]
         public double EndHour { get; set; }
-
-        /// <summary>
-        /// Static create function (for use in LINQ queries, etc.)
-        /// </summary>
-        /// <param name="dayOfWeek">The day of the week.</param>
-        /// <param name="startHour">The start hour.</param>
-        /// <param name="endHour">The end hour.</param>
-        /// <param name="userId">The associated user's id.</param>
-        /// <returns>The newly created instance.</returns>
-        public static AccessSchedule Create(DynamicDayOfWeek dayOfWeek, double startHour, double endHour, Guid userId)
-        {
-            return new AccessSchedule(dayOfWeek, startHour, endHour, userId);
-        }
     }
 }

+ 5 - 16
Jellyfin.Data/Entities/ActivityLog.cs

@@ -18,8 +18,7 @@ namespace Jellyfin.Data.Entities
         /// <param name="name">The name.</param>
         /// <param name="type">The type.</param>
         /// <param name="userId">The user id.</param>
-        /// <param name="logLevel">The log level.</param>
-        public ActivityLog(string name, string type, Guid userId, LogLevel logLevel = LogLevel.Information)
+        public ActivityLog(string name, string type, Guid userId)
         {
             if (string.IsNullOrEmpty(name))
             {
@@ -35,15 +34,7 @@ namespace Jellyfin.Data.Entities
             Type = type;
             UserId = userId;
             DateCreated = DateTime.UtcNow;
-            LogSeverity = logLevel;
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ActivityLog"/> class.
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </summary>
-        protected ActivityLog()
-        {
+            LogSeverity = LogLevel.Information;
         }
 
         /// <summary>
@@ -59,7 +50,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required, Max length = 512.
         /// </remarks>
-        [Required]
         [MaxLength(512)]
         [StringLength(512)]
         public string Name { get; set; }
@@ -72,7 +62,7 @@ namespace Jellyfin.Data.Entities
         /// </remarks>
         [MaxLength(512)]
         [StringLength(512)]
-        public string Overview { get; set; }
+        public string? Overview { get; set; }
 
         /// <summary>
         /// Gets or sets the short overview.
@@ -82,7 +72,7 @@ namespace Jellyfin.Data.Entities
         /// </remarks>
         [MaxLength(512)]
         [StringLength(512)]
-        public string ShortOverview { get; set; }
+        public string? ShortOverview { get; set; }
 
         /// <summary>
         /// Gets or sets the type.
@@ -90,7 +80,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required, Max length = 256.
         /// </remarks>
-        [Required]
         [MaxLength(256)]
         [StringLength(256)]
         public string Type { get; set; }
@@ -111,7 +100,7 @@ namespace Jellyfin.Data.Entities
         /// </remarks>
         [MaxLength(256)]
         [StringLength(256)]
-        public string ItemId { get; set; }
+        public string? ItemId { get; set; }
 
         /// <summary>
         /// Gets or sets the date created. This should be in UTC.

+ 5 - 15
Jellyfin.Data/Entities/CustomItemDisplayPreferences.cs

@@ -15,22 +15,15 @@ namespace Jellyfin.Data.Entities
         /// <param name="userId">The user id.</param>
         /// <param name="itemId">The item id.</param>
         /// <param name="client">The client.</param>
-        /// <param name="preferenceKey">The preference key.</param>
-        /// <param name="preferenceValue">The preference value.</param>
-        public CustomItemDisplayPreferences(Guid userId, Guid itemId, string client, string preferenceKey, string preferenceValue)
+        /// <param name="key">The preference key.</param>
+        /// <param name="value">The preference value.</param>
+        public CustomItemDisplayPreferences(Guid userId, Guid itemId, string client, string key, string value)
         {
             UserId = userId;
             ItemId = itemId;
             Client = client;
-            Key = preferenceKey;
-            Value = preferenceValue;
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="CustomItemDisplayPreferences"/> class.
-        /// </summary>
-        protected CustomItemDisplayPreferences()
-        {
+            Key = key;
+            Value = value;
         }
 
         /// <summary>
@@ -64,7 +57,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required. Max Length = 32.
         /// </remarks>
-        [Required]
         [MaxLength(32)]
         [StringLength(32)]
         public string Client { get; set; }
@@ -75,7 +67,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required.
         /// </remarks>
-        [Required]
         public string Key { get; set; }
 
         /// <summary>
@@ -84,7 +75,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required.
         /// </remarks>
-        [Required]
         public string Value { get; set; }
     }
 }

+ 2 - 12
Jellyfin.Data/Entities/DisplayPreferences.cs

@@ -30,19 +30,10 @@ namespace Jellyfin.Data.Entities
             SkipBackwardLength = 10000;
             ScrollDirection = ScrollDirection.Horizontal;
             ChromecastVersion = ChromecastVersion.Stable;
-            DashboardTheme = string.Empty;
-            TvHome = string.Empty;
 
             HomeSections = new HashSet<HomeSection>();
         }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="DisplayPreferences"/> class.
-        /// </summary>
-        protected DisplayPreferences()
-        {
-        }
-
         /// <summary>
         /// Gets or sets the Id.
         /// </summary>
@@ -74,7 +65,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required. Max Length = 32.
         /// </remarks>
-        [Required]
         [MaxLength(32)]
         [StringLength(32)]
         public string Client { get; set; }
@@ -145,14 +135,14 @@ namespace Jellyfin.Data.Entities
         /// </summary>
         [MaxLength(32)]
         [StringLength(32)]
-        public string DashboardTheme { get; set; }
+        public string? DashboardTheme { get; set; }
 
         /// <summary>
         /// Gets or sets the tv home screen.
         /// </summary>
         [MaxLength(32)]
         [StringLength(32)]
-        public string TvHome { get; set; }
+        public string? TvHome { get; set; }
 
         /// <summary>
         /// Gets or sets the home sections.

+ 0 - 11
Jellyfin.Data/Entities/Group.cs

@@ -32,16 +32,6 @@ namespace Jellyfin.Data.Entities
             Preferences = new HashSet<Preference>();
         }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="Group"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected Group()
-        {
-        }
-
         /// <summary>
         /// Gets or sets the id of this group.
         /// </summary>
@@ -56,7 +46,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required, Max length = 255.
         /// </remarks>
-        [Required]
         [MaxLength(255)]
         [StringLength(255)]
         public string Name { get; set; }

+ 0 - 1
Jellyfin.Data/Entities/HomeSection.cs

@@ -15,7 +15,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Identity. Required.
         /// </remarks>
-        [Key]
         [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
         public int Id { get; protected set; }
 

+ 0 - 11
Jellyfin.Data/Entities/ImageInfo.cs

@@ -19,16 +19,6 @@ namespace Jellyfin.Data.Entities
             LastModified = DateTime.UtcNow;
         }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ImageInfo"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected ImageInfo()
-        {
-        }
-
         /// <summary>
         /// Gets or sets the id.
         /// </summary>
@@ -49,7 +39,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required.
         /// </remarks>
-        [Required]
         [MaxLength(512)]
         [StringLength(512)]
         public string Path { get; set; }

+ 0 - 9
Jellyfin.Data/Entities/ItemDisplayPreferences.cs

@@ -28,13 +28,6 @@ namespace Jellyfin.Data.Entities
             RememberIndexing = false;
         }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ItemDisplayPreferences"/> class.
-        /// </summary>
-        protected ItemDisplayPreferences()
-        {
-        }
-
         /// <summary>
         /// Gets or sets the Id.
         /// </summary>
@@ -66,7 +59,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required. Max Length = 32.
         /// </remarks>
-        [Required]
         [MaxLength(32)]
         [StringLength(32)]
         public string Client { get; set; }
@@ -106,7 +98,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required.
         /// </remarks>
-        [Required]
         [MaxLength(64)]
         [StringLength(64)]
         public string SortBy { get; set; }

+ 1 - 15
Jellyfin.Data/Entities/Libraries/Artwork.cs

@@ -18,8 +18,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </summary>
         /// <param name="path">The path.</param>
         /// <param name="kind">The kind of art.</param>
-        /// <param name="owner">The owner.</param>
-        public Artwork(string path, ArtKind kind, IHasArtwork owner)
+        public Artwork(string path, ArtKind kind)
         {
             if (string.IsNullOrEmpty(path))
             {
@@ -28,18 +27,6 @@ namespace Jellyfin.Data.Entities.Libraries
 
             Path = path;
             Kind = kind;
-
-            owner?.Artwork.Add(this);
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="Artwork"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected Artwork()
-        {
         }
 
         /// <summary>
@@ -57,7 +44,6 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <remarks>
         /// Required, Max length = 65535.
         /// </remarks>
-        [Required]
         [MaxLength(65535)]
         [StringLength(65535)]
         public string Path { get; set; }

+ 2 - 1
Jellyfin.Data/Entities/Libraries/Book.cs

@@ -13,7 +13,8 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <summary>
         /// Initializes a new instance of the <see cref="Book"/> class.
         /// </summary>
-        public Book()
+        /// <param name="library">The library.</param>
+        public Book(Library library) : base(library)
         {
             BookMetadata = new HashSet<BookMetadata>();
             Releases = new HashSet<Release>();

+ 1 - 22
Jellyfin.Data/Entities/Libraries/BookMetadata.cs

@@ -1,8 +1,6 @@
 #pragma warning disable CA2227
 
-using System;
 using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations.Schema;
 using Jellyfin.Data.Interfaces;
 
 namespace Jellyfin.Data.Entities.Libraries
@@ -17,29 +15,11 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </summary>
         /// <param name="title">The title or name of the object.</param>
         /// <param name="language">ISO-639-3 3-character language codes.</param>
-        /// <param name="book">The book.</param>
-        public BookMetadata(string title, string language, Book book) : base(title, language)
+        public BookMetadata(string title, string language) : base(title, language)
         {
-            if (book == null)
-            {
-                throw new ArgumentNullException(nameof(book));
-            }
-
-            book.BookMetadata.Add(this);
-
             Publishers = new HashSet<Company>();
         }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="BookMetadata"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected BookMetadata()
-        {
-        }
-
         /// <summary>
         /// Gets or sets the ISBN.
         /// </summary>
@@ -51,7 +31,6 @@ namespace Jellyfin.Data.Entities.Libraries
         public virtual ICollection<Company> Publishers { get; protected set; }
 
         /// <inheritdoc />
-        [NotMapped]
         public ICollection<Company> Companies => Publishers;
     }
 }

+ 2 - 21
Jellyfin.Data/Entities/Libraries/Chapter.cs

@@ -17,8 +17,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </summary>
         /// <param name="language">ISO-639-3 3-character language codes.</param>
         /// <param name="startTime">The start time for this chapter.</param>
-        /// <param name="release">The release.</param>
-        public Chapter(string language, long startTime, Release release)
+        public Chapter(string language, long startTime)
         {
             if (string.IsNullOrEmpty(language))
             {
@@ -27,23 +26,6 @@ namespace Jellyfin.Data.Entities.Libraries
 
             Language = language;
             StartTime = startTime;
-
-            if (release == null)
-            {
-                throw new ArgumentNullException(nameof(release));
-            }
-
-            release.Chapters.Add(this);
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="Chapter"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected Chapter()
-        {
         }
 
         /// <summary>
@@ -63,7 +45,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(1024)]
         [StringLength(1024)]
-        public string Name { get; set; }
+        public string? Name { get; set; }
 
         /// <summary>
         /// Gets or sets the language.
@@ -72,7 +54,6 @@ namespace Jellyfin.Data.Entities.Libraries
         /// Required, Min length = 3, Max length = 3
         /// ISO-639-3 3-character language codes.
         /// </remarks>
-        [Required]
         [MinLength(3)]
         [MaxLength(3)]
         [StringLength(3)]

+ 2 - 1
Jellyfin.Data/Entities/Libraries/Collection.cs

@@ -1,3 +1,4 @@
+#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
 #pragma warning disable CA2227
 
 using System.Collections.Generic;
@@ -37,7 +38,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(1024)]
         [StringLength(1024)]
-        public string Name { get; set; }
+        public string? Name { get; set; }
 
         /// <inheritdoc />
         [ConcurrencyCheck]

+ 5 - 35
Jellyfin.Data/Entities/Libraries/CollectionItem.cs

@@ -1,4 +1,3 @@
-using System;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
 using Jellyfin.Data.Interfaces;
@@ -13,39 +12,10 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <summary>
         /// Initializes a new instance of the <see cref="CollectionItem"/> class.
         /// </summary>
-        /// <param name="collection">The collection.</param>
-        /// <param name="previous">The previous item.</param>
-        /// <param name="next">The next item.</param>
-        public CollectionItem(Collection collection, CollectionItem previous, CollectionItem next)
-        {
-            if (collection == null)
-            {
-                throw new ArgumentNullException(nameof(collection));
-            }
-
-            collection.Items.Add(this);
-
-            if (next != null)
-            {
-                Next = next;
-                next.Previous = this;
-            }
-
-            if (previous != null)
-            {
-                Previous = previous;
-                previous.Next = this;
-            }
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="CollectionItem"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected CollectionItem()
+        /// <param name="libraryItem">The library item.</param>
+        public CollectionItem(LibraryItem libraryItem)
         {
+            LibraryItem = libraryItem;
         }
 
         /// <summary>
@@ -75,7 +45,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <remarks>
         /// TODO check if this properly updated Dependant and has the proper principal relationship.
         /// </remarks>
-        public virtual CollectionItem Next { get; set; }
+        public virtual CollectionItem? Next { get; set; }
 
         /// <summary>
         /// Gets or sets the previous item in the collection.
@@ -83,7 +53,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <remarks>
         /// TODO check if this properly updated Dependant and has the proper principal relationship.
         /// </remarks>
-        public virtual CollectionItem Previous { get; set; }
+        public virtual CollectionItem? Previous { get; set; }
 
         /// <inheritdoc />
         public void OnSavingChanges()

+ 2 - 15
Jellyfin.Data/Entities/Libraries/Company.cs

@@ -15,22 +15,10 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <summary>
         /// Initializes a new instance of the <see cref="Company"/> class.
         /// </summary>
-        /// <param name="owner">The owner of this company.</param>
-        public Company(IHasCompanies owner)
+        public Company()
         {
-            owner?.Companies.Add(this);
-
             CompanyMetadata = new HashSet<CompanyMetadata>();
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="Company"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected Company()
-        {
+            ChildCompanies = new HashSet<Company>();
         }
 
         /// <summary>
@@ -57,7 +45,6 @@ namespace Jellyfin.Data.Entities.Libraries
         public virtual ICollection<Company> ChildCompanies { get; protected set; }
 
         /// <inheritdoc />
-        [NotMapped]
         public ICollection<Company> Companies => ChildCompanies;
 
         /// <inheritdoc />

+ 5 - 20
Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs

@@ -1,4 +1,3 @@
-using System;
 using System.ComponentModel.DataAnnotations;
 
 namespace Jellyfin.Data.Entities.Libraries
@@ -13,21 +12,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </summary>
         /// <param name="title">The title or name of the object.</param>
         /// <param name="language">ISO-639-3 3-character language codes.</param>
-        /// <param name="company">The company.</param>
-        public CompanyMetadata(string title, string language, Company company) : base(title, language)
-        {
-            if (company == null)
-            {
-                throw new ArgumentNullException(nameof(company));
-            }
-
-            company.CompanyMetadata.Add(this);
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="CompanyMetadata"/> class.
-        /// </summary>
-        protected CompanyMetadata()
+        public CompanyMetadata(string title, string language) : base(title, language)
         {
         }
 
@@ -39,7 +24,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(65535)]
         [StringLength(65535)]
-        public string Description { get; set; }
+        public string? Description { get; set; }
 
         /// <summary>
         /// Gets or sets the headquarters.
@@ -49,7 +34,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(255)]
         [StringLength(255)]
-        public string Headquarters { get; set; }
+        public string? Headquarters { get; set; }
 
         /// <summary>
         /// Gets or sets the country code.
@@ -59,7 +44,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(2)]
         [StringLength(2)]
-        public string Country { get; set; }
+        public string? Country { get; set; }
 
         /// <summary>
         /// Gets or sets the homepage.
@@ -69,6 +54,6 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(1024)]
         [StringLength(1024)]
-        public string Homepage { get; set; }
+        public string? Homepage { get; set; }
     }
 }

+ 2 - 1
Jellyfin.Data/Entities/Libraries/CustomItem.cs

@@ -13,7 +13,8 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <summary>
         /// Initializes a new instance of the <see cref="CustomItem"/> class.
         /// </summary>
-        public CustomItem()
+        /// <param name="library">The library.</param>
+        public CustomItem(Library library) : base(library)
         {
             CustomItemMetadata = new HashSet<CustomItemMetadata>();
             Releases = new HashSet<Release>();

+ 1 - 20
Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs

@@ -1,5 +1,3 @@
-using System;
-
 namespace Jellyfin.Data.Entities.Libraries
 {
     /// <summary>
@@ -12,24 +10,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </summary>
         /// <param name="title">The title or name of the object.</param>
         /// <param name="language">ISO-639-3 3-character language codes.</param>
-        /// <param name="item">The item.</param>
-        public CustomItemMetadata(string title, string language, CustomItem item) : base(title, language)
-        {
-            if (item == null)
-            {
-                throw new ArgumentNullException(nameof(item));
-            }
-
-            item.CustomItemMetadata.Add(this);
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="CustomItemMetadata"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected CustomItemMetadata()
+        public CustomItemMetadata(string title, string language) : base(title, language)
         {
         }
     }

+ 2 - 20
Jellyfin.Data/Entities/Libraries/Episode.cs

@@ -1,6 +1,5 @@
 #pragma warning disable CA2227
 
-using System;
 using System.Collections.Generic;
 using Jellyfin.Data.Interfaces;
 
@@ -14,30 +13,13 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <summary>
         /// Initializes a new instance of the <see cref="Episode"/> class.
         /// </summary>
-        /// <param name="season">The season.</param>
-        public Episode(Season season)
+        /// <param name="library">The library.</param>
+        public Episode(Library library) : base(library)
         {
-            if (season == null)
-            {
-                throw new ArgumentNullException(nameof(season));
-            }
-
-            season.Episodes.Add(this);
-
             Releases = new HashSet<Release>();
             EpisodeMetadata = new HashSet<EpisodeMetadata>();
         }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="Episode"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected Episode()
-        {
-        }
-
         /// <summary>
         /// Gets or sets the episode number.
         /// </summary>

+ 4 - 22
Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs

@@ -1,4 +1,3 @@
-using System;
 using System.ComponentModel.DataAnnotations;
 
 namespace Jellyfin.Data.Entities.Libraries
@@ -13,24 +12,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </summary>
         /// <param name="title">The title or name of the object.</param>
         /// <param name="language">ISO-639-3 3-character language codes.</param>
-        /// <param name="episode">The episode.</param>
-        public EpisodeMetadata(string title, string language, Episode episode) : base(title, language)
-        {
-            if (episode == null)
-            {
-                throw new ArgumentNullException(nameof(episode));
-            }
-
-            episode.EpisodeMetadata.Add(this);
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="EpisodeMetadata"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected EpisodeMetadata()
+        public EpisodeMetadata(string title, string language) : base(title, language)
         {
         }
 
@@ -42,7 +24,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(1024)]
         [StringLength(1024)]
-        public string Outline { get; set; }
+        public string? Outline { get; set; }
 
         /// <summary>
         /// Gets or sets the plot.
@@ -52,7 +34,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(65535)]
         [StringLength(65535)]
-        public string Plot { get; set; }
+        public string? Plot { get; set; }
 
         /// <summary>
         /// Gets or sets the tagline.
@@ -62,6 +44,6 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(1024)]
         [StringLength(1024)]
-        public string Tagline { get; set; }
+        public string? Tagline { get; set; }
     }
 }

+ 1 - 26
Jellyfin.Data/Entities/Libraries/Genre.cs

@@ -1,4 +1,3 @@
-using System;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
 using Jellyfin.Data.Interfaces;
@@ -14,32 +13,9 @@ namespace Jellyfin.Data.Entities.Libraries
         /// Initializes a new instance of the <see cref="Genre"/> class.
         /// </summary>
         /// <param name="name">The name.</param>
-        /// <param name="itemMetadata">The metadata.</param>
-        public Genre(string name, ItemMetadata itemMetadata)
+        public Genre(string name)
         {
-            if (string.IsNullOrEmpty(name))
-            {
-                throw new ArgumentNullException(nameof(name));
-            }
-
             Name = name;
-
-            if (itemMetadata == null)
-            {
-                throw new ArgumentNullException(nameof(itemMetadata));
-            }
-
-            itemMetadata.Genres.Add(this);
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="Genre"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected Genre()
-        {
         }
 
         /// <summary>
@@ -57,7 +33,6 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <remarks>
         /// Indexed, Required, Max length = 255.
         /// </remarks>
-        [Required]
         [MaxLength(255)]
         [StringLength(255)]
         public string Name { get; set; }

+ 2 - 14
Jellyfin.Data/Entities/Libraries/ItemMetadata.cs

@@ -42,16 +42,6 @@ namespace Jellyfin.Data.Entities.Libraries
             Sources = new HashSet<MetadataProviderId>();
         }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ItemMetadata"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to being abstract.
-        /// </remarks>
-        protected ItemMetadata()
-        {
-        }
-
         /// <summary>
         /// Gets or sets the id.
         /// </summary>
@@ -67,7 +57,6 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <remarks>
         /// Required, Max length = 1024.
         /// </remarks>
-        [Required]
         [MaxLength(1024)]
         [StringLength(1024)]
         public string Title { get; set; }
@@ -80,7 +69,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(1024)]
         [StringLength(1024)]
-        public string OriginalTitle { get; set; }
+        public string? OriginalTitle { get; set; }
 
         /// <summary>
         /// Gets or sets the sort title.
@@ -90,7 +79,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(1024)]
         [StringLength(1024)]
-        public string SortTitle { get; set; }
+        public string? SortTitle { get; set; }
 
         /// <summary>
         /// Gets or sets the language.
@@ -99,7 +88,6 @@ namespace Jellyfin.Data.Entities.Libraries
         /// Required, Min length = 3, Max length = 3.
         /// ISO-639-3 3-character language codes.
         /// </remarks>
-        [Required]
         [MinLength(3)]
         [MaxLength(3)]
         [StringLength(3)]

+ 3 - 19
Jellyfin.Data/Entities/Libraries/Library.cs

@@ -1,4 +1,3 @@
-using System;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
 using Jellyfin.Data.Interfaces;
@@ -14,24 +13,11 @@ namespace Jellyfin.Data.Entities.Libraries
         /// Initializes a new instance of the <see cref="Library"/> class.
         /// </summary>
         /// <param name="name">The name of the library.</param>
-        public Library(string name)
+        /// <param name="path">The path of the library.</param>
+        public Library(string name, string path)
         {
-            if (string.IsNullOrWhiteSpace(name))
-            {
-                throw new ArgumentNullException(nameof(name));
-            }
-
             Name = name;
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="Library"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected Library()
-        {
+            Path = path;
         }
 
         /// <summary>
@@ -49,7 +35,6 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <remarks>
         /// Required, Max length = 128.
         /// </remarks>
-        [Required]
         [MaxLength(128)]
         [StringLength(128)]
         public string Name { get; set; }
@@ -60,7 +45,6 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <remarks>
         /// Required.
         /// </remarks>
-        [Required]
         public string Path { get; set; }
 
         /// <inheritdoc />

+ 0 - 8
Jellyfin.Data/Entities/Libraries/LibraryItem.cs

@@ -20,13 +20,6 @@ namespace Jellyfin.Data.Entities.Libraries
             Library = library;
         }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="LibraryItem"/> class.
-        /// </summary>
-        protected LibraryItem()
-        {
-        }
-
         /// <summary>
         /// Gets or sets the id.
         /// </summary>
@@ -51,7 +44,6 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <remarks>
         /// Required.
         /// </remarks>
-        [Required]
         public virtual Library Library { get; set; }
 
         /// <inheritdoc />

+ 1 - 20
Jellyfin.Data/Entities/Libraries/MediaFile.cs

@@ -19,8 +19,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </summary>
         /// <param name="path">The path relative to the LibraryRoot.</param>
         /// <param name="kind">The file kind.</param>
-        /// <param name="release">The release.</param>
-        public MediaFile(string path, MediaFileKind kind, Release release)
+        public MediaFile(string path, MediaFileKind kind)
         {
             if (string.IsNullOrEmpty(path))
             {
@@ -30,26 +29,9 @@ namespace Jellyfin.Data.Entities.Libraries
             Path = path;
             Kind = kind;
 
-            if (release == null)
-            {
-                throw new ArgumentNullException(nameof(release));
-            }
-
-            release.MediaFiles.Add(this);
-
             MediaFileStreams = new HashSet<MediaFileStream>();
         }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="MediaFile"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected MediaFile()
-        {
-        }
-
         /// <summary>
         /// Gets or sets the id.
         /// </summary>
@@ -65,7 +47,6 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <remarks>
         /// Required, Max length = 65535.
         /// </remarks>
-        [Required]
         [MaxLength(65535)]
         [StringLength(65535)]
         public string Path { get; set; }

+ 3 - 20
Jellyfin.Data/Entities/Libraries/MediaFileStream.cs

@@ -1,4 +1,5 @@
-using System;
+#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
+
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
 using Jellyfin.Data.Interfaces;
@@ -14,27 +15,9 @@ namespace Jellyfin.Data.Entities.Libraries
         /// Initializes a new instance of the <see cref="MediaFileStream"/> class.
         /// </summary>
         /// <param name="streamNumber">The number of this stream.</param>
-        /// <param name="mediaFile">The media file.</param>
-        public MediaFileStream(int streamNumber, MediaFile mediaFile)
+        public MediaFileStream(int streamNumber)
         {
             StreamNumber = streamNumber;
-
-            if (mediaFile == null)
-            {
-                throw new ArgumentNullException(nameof(mediaFile));
-            }
-
-            mediaFile.MediaFileStreams.Add(this);
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="MediaFileStream"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected MediaFileStream()
-        {
         }
 
         /// <summary>

+ 0 - 11
Jellyfin.Data/Entities/Libraries/MetadataProvider.cs

@@ -24,16 +24,6 @@ namespace Jellyfin.Data.Entities.Libraries
             Name = name;
         }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="MetadataProvider"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected MetadataProvider()
-        {
-        }
-
         /// <summary>
         /// Gets or sets the id.
         /// </summary>
@@ -49,7 +39,6 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <remarks>
         /// Required, Max length = 1024.
         /// </remarks>
-        [Required]
         [MaxLength(1024)]
         [StringLength(1024)]
         public string Name { get; set; }

+ 3 - 20
Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs

@@ -14,8 +14,8 @@ namespace Jellyfin.Data.Entities.Libraries
         /// Initializes a new instance of the <see cref="MetadataProviderId"/> class.
         /// </summary>
         /// <param name="providerId">The provider id.</param>
-        /// <param name="itemMetadata">The metadata entity.</param>
-        public MetadataProviderId(string providerId, ItemMetadata itemMetadata)
+        /// <param name="metadataProvider">The metadata provider.</param>
+        public MetadataProviderId(string providerId, MetadataProvider metadataProvider)
         {
             if (string.IsNullOrEmpty(providerId))
             {
@@ -23,23 +23,7 @@ namespace Jellyfin.Data.Entities.Libraries
             }
 
             ProviderId = providerId;
-
-            if (itemMetadata == null)
-            {
-                throw new ArgumentNullException(nameof(itemMetadata));
-            }
-
-            itemMetadata.Sources.Add(this);
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="MetadataProviderId"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected MetadataProviderId()
-        {
+            MetadataProvider = metadataProvider;
         }
 
         /// <summary>
@@ -57,7 +41,6 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <remarks>
         /// Required, Max length = 255.
         /// </remarks>
-        [Required]
         [MaxLength(255)]
         [StringLength(255)]
         public string ProviderId { get; set; }

+ 2 - 1
Jellyfin.Data/Entities/Libraries/Movie.cs

@@ -13,7 +13,8 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <summary>
         /// Initializes a new instance of the <see cref="Movie"/> class.
         /// </summary>
-        public Movie()
+        /// <param name="library">The library.</param>
+        public Movie(Library library) : base(library)
         {
             Releases = new HashSet<Release>();
             MovieMetadata = new HashSet<MovieMetadata>();

+ 5 - 20
Jellyfin.Data/Entities/Libraries/MovieMetadata.cs

@@ -2,7 +2,6 @@
 
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
 using Jellyfin.Data.Interfaces;
 
 namespace Jellyfin.Data.Entities.Libraries
@@ -17,22 +16,9 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </summary>
         /// <param name="title">The title or name of the movie.</param>
         /// <param name="language">ISO-639-3 3-character language codes.</param>
-        /// <param name="movie">The movie.</param>
-        public MovieMetadata(string title, string language, Movie movie) : base(title, language)
+        public MovieMetadata(string title, string language) : base(title, language)
         {
             Studios = new HashSet<Company>();
-
-            movie.MovieMetadata.Add(this);
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="MovieMetadata"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected MovieMetadata()
-        {
         }
 
         /// <summary>
@@ -43,7 +29,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(1024)]
         [StringLength(1024)]
-        public string Outline { get; set; }
+        public string? Outline { get; set; }
 
         /// <summary>
         /// Gets or sets the tagline.
@@ -53,7 +39,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(1024)]
         [StringLength(1024)]
-        public string Tagline { get; set; }
+        public string? Tagline { get; set; }
 
         /// <summary>
         /// Gets or sets the plot.
@@ -63,7 +49,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(65535)]
         [StringLength(65535)]
-        public string Plot { get; set; }
+        public string? Plot { get; set; }
 
         /// <summary>
         /// Gets or sets the country code.
@@ -73,7 +59,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(2)]
         [StringLength(2)]
-        public string Country { get; set; }
+        public string? Country { get; set; }
 
         /// <summary>
         /// Gets or sets the studios that produced this movie.
@@ -81,7 +67,6 @@ namespace Jellyfin.Data.Entities.Libraries
         public virtual ICollection<Company> Studios { get; protected set; }
 
         /// <inheritdoc />
-        [NotMapped]
         public ICollection<Company> Companies => Studios;
     }
 }

+ 2 - 1
Jellyfin.Data/Entities/Libraries/MusicAlbum.cs

@@ -12,7 +12,8 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <summary>
         /// Initializes a new instance of the <see cref="MusicAlbum"/> class.
         /// </summary>
-        public MusicAlbum()
+        /// <param name="library">The library.</param>
+        public MusicAlbum(Library library) : base(library)
         {
             MusicAlbumMetadata = new HashSet<MusicAlbumMetadata>();
             Tracks = new HashSet<Track>();

+ 4 - 17
Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs

@@ -15,22 +15,9 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </summary>
         /// <param name="title">The title or name of the album.</param>
         /// <param name="language">ISO-639-3 3-character language codes.</param>
-        /// <param name="album">The music album.</param>
-        public MusicAlbumMetadata(string title, string language, MusicAlbum album) : base(title, language)
+        public MusicAlbumMetadata(string title, string language) : base(title, language)
         {
             Labels = new HashSet<Company>();
-
-            album.MusicAlbumMetadata.Add(this);
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="MusicAlbumMetadata"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected MusicAlbumMetadata()
-        {
         }
 
         /// <summary>
@@ -41,7 +28,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(255)]
         [StringLength(255)]
-        public string Barcode { get; set; }
+        public string? Barcode { get; set; }
 
         /// <summary>
         /// Gets or sets the label number.
@@ -51,7 +38,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(255)]
         [StringLength(255)]
-        public string LabelNumber { get; set; }
+        public string? LabelNumber { get; set; }
 
         /// <summary>
         /// Gets or sets the country code.
@@ -61,7 +48,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(2)]
         [StringLength(2)]
-        public string Country { get; set; }
+        public string? Country { get; set; }
 
         /// <summary>
         /// Gets or sets a collection containing the labels.

+ 1 - 12
Jellyfin.Data/Entities/Libraries/Person.cs

@@ -31,16 +31,6 @@ namespace Jellyfin.Data.Entities.Libraries
             Sources = new HashSet<MetadataProviderId>();
         }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="Person"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected Person()
-        {
-        }
-
         /// <summary>
         /// Gets or sets the id.
         /// </summary>
@@ -56,7 +46,6 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <remarks>
         /// Required, Max length = 1024.
         /// </remarks>
-        [Required]
         [MaxLength(1024)]
         [StringLength(1024)]
         public string Name { get; set; }
@@ -69,7 +58,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(256)]
         [StringLength(256)]
-        public string SourceId { get; set; }
+        public string? SourceId { get; set; }
 
         /// <summary>
         /// Gets or sets the date added.

+ 5 - 23
Jellyfin.Data/Entities/Libraries/PersonRole.cs

@@ -1,6 +1,5 @@
 #pragma warning disable CA2227
 
-using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
@@ -18,31 +17,15 @@ namespace Jellyfin.Data.Entities.Libraries
         /// Initializes a new instance of the <see cref="PersonRole"/> class.
         /// </summary>
         /// <param name="type">The role type.</param>
-        /// <param name="itemMetadata">The metadata.</param>
-        public PersonRole(PersonRoleType type, ItemMetadata itemMetadata)
+        /// <param name="person">The person.</param>
+        public PersonRole(PersonRoleType type, Person person)
         {
             Type = type;
-
-            if (itemMetadata == null)
-            {
-                throw new ArgumentNullException(nameof(itemMetadata));
-            }
-
-            itemMetadata.PersonRoles.Add(this);
-
+            Person = person;
+            Artwork = new HashSet<Artwork>();
             Sources = new HashSet<MetadataProviderId>();
         }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="PersonRole"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected PersonRole()
-        {
-        }
-
         /// <summary>
         /// Gets or sets the id.
         /// </summary>
@@ -60,7 +43,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(1024)]
         [StringLength(1024)]
-        public string Role { get; set; }
+        public string? Role { get; set; }
 
         /// <summary>
         /// Gets or sets the person's role type.
@@ -80,7 +63,6 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <remarks>
         /// Required.
         /// </remarks>
-        [Required]
         public virtual Person Person { get; set; }
 
         /// <inheritdoc />

+ 2 - 1
Jellyfin.Data/Entities/Libraries/Photo.cs

@@ -13,7 +13,8 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <summary>
         /// Initializes a new instance of the <see cref="Photo"/> class.
         /// </summary>
-        public Photo()
+        /// <param name="library">The library.</param>
+        public Photo(Library library) : base(library)
         {
             PhotoMetadata = new HashSet<PhotoMetadata>();
             Releases = new HashSet<Release>();

+ 1 - 20
Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs

@@ -1,5 +1,3 @@
-using System;
-
 namespace Jellyfin.Data.Entities.Libraries
 {
     /// <summary>
@@ -12,24 +10,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </summary>
         /// <param name="title">The title or name of the photo.</param>
         /// <param name="language">ISO-639-3 3-character language codes.</param>
-        /// <param name="photo">The photo.</param>
-        public PhotoMetadata(string title, string language, Photo photo) : base(title, language)
-        {
-            if (photo == null)
-            {
-                throw new ArgumentNullException(nameof(photo));
-            }
-
-            photo.PhotoMetadata.Add(this);
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="PhotoMetadata"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected PhotoMetadata()
+        public PhotoMetadata(string title, string language) : base(title, language)
         {
         }
     }

+ 2 - 21
Jellyfin.Data/Entities/Libraries/Rating.cs

@@ -1,4 +1,3 @@
-using System;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
 using Jellyfin.Data.Interfaces;
@@ -14,27 +13,9 @@ namespace Jellyfin.Data.Entities.Libraries
         /// Initializes a new instance of the <see cref="Rating"/> class.
         /// </summary>
         /// <param name="value">The value.</param>
-        /// <param name="itemMetadata">The metadata.</param>
-        public Rating(double value, ItemMetadata itemMetadata)
+        public Rating(double value)
         {
             Value = value;
-
-            if (itemMetadata == null)
-            {
-                throw new ArgumentNullException(nameof(itemMetadata));
-            }
-
-            itemMetadata.Ratings.Add(this);
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="Rating"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected Rating()
-        {
         }
 
         /// <summary>
@@ -67,7 +48,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// Gets or sets the rating type.
         /// If this is <c>null</c> it's the internal user rating.
         /// </summary>
-        public virtual RatingSource RatingType { get; set; }
+        public virtual RatingSource? RatingType { get; set; }
 
         /// <inheritdoc />
         public void OnSavingChanges()

+ 3 - 22
Jellyfin.Data/Entities/Libraries/RatingSource.cs

@@ -1,4 +1,3 @@
-using System;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
 using Jellyfin.Data.Interfaces;
@@ -15,28 +14,10 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </summary>
         /// <param name="minimumValue">The minimum value.</param>
         /// <param name="maximumValue">The maximum value.</param>
-        /// <param name="rating">The rating.</param>
-        public RatingSource(double minimumValue, double maximumValue, Rating rating)
+        public RatingSource(double minimumValue, double maximumValue)
         {
             MinimumValue = minimumValue;
             MaximumValue = maximumValue;
-
-            if (rating == null)
-            {
-                throw new ArgumentNullException(nameof(rating));
-            }
-
-            rating.RatingType = this;
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="RatingSource"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected RatingSource()
-        {
         }
 
         /// <summary>
@@ -56,7 +37,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(1024)]
         [StringLength(1024)]
-        public string Name { get; set; }
+        public string? Name { get; set; }
 
         /// <summary>
         /// Gets or sets the minimum value.
@@ -81,7 +62,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <summary>
         /// Gets or sets the metadata source.
         /// </summary>
-        public virtual MetadataProviderId Source { get; set; }
+        public virtual MetadataProviderId? Source { get; set; }
 
         /// <inheritdoc />
         public void OnSavingChanges()

+ 1 - 15
Jellyfin.Data/Entities/Libraries/Release.cs

@@ -17,8 +17,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// Initializes a new instance of the <see cref="Release"/> class.
         /// </summary>
         /// <param name="name">The name of this release.</param>
-        /// <param name="owner">The owner of this release.</param>
-        public Release(string name, IHasReleases owner)
+        public Release(string name)
         {
             if (string.IsNullOrEmpty(name))
             {
@@ -27,22 +26,10 @@ namespace Jellyfin.Data.Entities.Libraries
 
             Name = name;
 
-            owner?.Releases.Add(this);
-
             MediaFiles = new HashSet<MediaFile>();
             Chapters = new HashSet<Chapter>();
         }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="Release"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected Release()
-        {
-        }
-
         /// <summary>
         /// Gets or sets the id.
         /// </summary>
@@ -58,7 +45,6 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <remarks>
         /// Required, Max length = 1024.
         /// </remarks>
-        [Required]
         [MaxLength(1024)]
         [StringLength(1024)]
         public string Name { get; set; }

+ 2 - 20
Jellyfin.Data/Entities/Libraries/Season.cs

@@ -1,6 +1,5 @@
 #pragma warning disable CA2227
 
-using System;
 using System.Collections.Generic;
 
 namespace Jellyfin.Data.Entities.Libraries
@@ -13,30 +12,13 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <summary>
         /// Initializes a new instance of the <see cref="Season"/> class.
         /// </summary>
-        /// <param name="series">The series.</param>
-        public Season(Series series)
+        /// <param name="library">The library.</param>
+        public Season(Library library) : base(library)
         {
-            if (series == null)
-            {
-                throw new ArgumentNullException(nameof(series));
-            }
-
-            series.Seasons.Add(this);
-
             Episodes = new HashSet<Episode>();
             SeasonMetadata = new HashSet<SeasonMetadata>();
         }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="Season"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected Season()
-        {
-        }
-
         /// <summary>
         /// Gets or sets the season number.
         /// </summary>

+ 2 - 20
Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs

@@ -1,4 +1,3 @@
-using System;
 using System.ComponentModel.DataAnnotations;
 
 namespace Jellyfin.Data.Entities.Libraries
@@ -13,24 +12,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </summary>
         /// <param name="title">The title or name of the object.</param>
         /// <param name="language">ISO-639-3 3-character language codes.</param>
-        /// <param name="season">The season.</param>
-        public SeasonMetadata(string title, string language, Season season) : base(title, language)
-        {
-            if (season == null)
-            {
-                throw new ArgumentNullException(nameof(season));
-            }
-
-            season.SeasonMetadata.Add(this);
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="SeasonMetadata"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected SeasonMetadata()
+        public SeasonMetadata(string title, string language) : base(title, language)
         {
         }
 
@@ -42,6 +24,6 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(1024)]
         [StringLength(1024)]
-        public string Outline { get; set; }
+        public string? Outline { get; set; }
     }
 }

+ 2 - 1
Jellyfin.Data/Entities/Libraries/Series.cs

@@ -13,7 +13,8 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <summary>
         /// Initializes a new instance of the <see cref="Series"/> class.
         /// </summary>
-        public Series()
+        /// <param name="library">The library.</param>
+        public Series(Library library) : base(library)
         {
             DateAdded = DateTime.UtcNow;
             Seasons = new HashSet<Season>();

+ 5 - 25
Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs

@@ -1,6 +1,5 @@
 #pragma warning disable CA2227
 
-using System;
 using System.Collections.Generic;
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
@@ -18,29 +17,11 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </summary>
         /// <param name="title">The title or name of the object.</param>
         /// <param name="language">ISO-639-3 3-character language codes.</param>
-        /// <param name="series">The series.</param>
-        public SeriesMetadata(string title, string language, Series series) : base(title, language)
+        public SeriesMetadata(string title, string language) : base(title, language)
         {
-            if (series == null)
-            {
-                throw new ArgumentNullException(nameof(series));
-            }
-
-            series.SeriesMetadata.Add(this);
-
             Networks = new HashSet<Company>();
         }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="SeriesMetadata"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected SeriesMetadata()
-        {
-        }
-
         /// <summary>
         /// Gets or sets the outline.
         /// </summary>
@@ -49,7 +30,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(1024)]
         [StringLength(1024)]
-        public string Outline { get; set; }
+        public string? Outline { get; set; }
 
         /// <summary>
         /// Gets or sets the plot.
@@ -59,7 +40,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(65535)]
         [StringLength(65535)]
-        public string Plot { get; set; }
+        public string? Plot { get; set; }
 
         /// <summary>
         /// Gets or sets the tagline.
@@ -69,7 +50,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(1024)]
         [StringLength(1024)]
-        public string Tagline { get; set; }
+        public string? Tagline { get; set; }
 
         /// <summary>
         /// Gets or sets the country code.
@@ -79,7 +60,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </remarks>
         [MaxLength(2)]
         [StringLength(2)]
-        public string Country { get; set; }
+        public string? Country { get; set; }
 
         /// <summary>
         /// Gets or sets a collection containing the networks.
@@ -87,7 +68,6 @@ namespace Jellyfin.Data.Entities.Libraries
         public virtual ICollection<Company> Networks { get; protected set; }
 
         /// <inheritdoc />
-        [NotMapped]
         public ICollection<Company> Companies => Networks;
     }
 }

+ 2 - 20
Jellyfin.Data/Entities/Libraries/Track.cs

@@ -1,6 +1,5 @@
 #pragma warning disable CA2227
 
-using System;
 using System.Collections.Generic;
 using Jellyfin.Data.Interfaces;
 
@@ -14,30 +13,13 @@ namespace Jellyfin.Data.Entities.Libraries
         /// <summary>
         /// Initializes a new instance of the <see cref="Track"/> class.
         /// </summary>
-        /// <param name="album">The album.</param>
-        public Track(MusicAlbum album)
+        /// <param name="library">The library.</param>
+        public Track(Library library) : base(library)
         {
-            if (album == null)
-            {
-                throw new ArgumentNullException(nameof(album));
-            }
-
-            album.Tracks.Add(this);
-
             Releases = new HashSet<Release>();
             TrackMetadata = new HashSet<TrackMetadata>();
         }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="Track"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected Track()
-        {
-        }
-
         /// <summary>
         /// Gets or sets the track number.
         /// </summary>

+ 1 - 20
Jellyfin.Data/Entities/Libraries/TrackMetadata.cs

@@ -1,5 +1,3 @@
-using System;
-
 namespace Jellyfin.Data.Entities.Libraries
 {
     /// <summary>
@@ -12,24 +10,7 @@ namespace Jellyfin.Data.Entities.Libraries
         /// </summary>
         /// <param name="title">The title or name of the object.</param>
         /// <param name="language">ISO-639-3 3-character language codes.</param>
-        /// <param name="track">The track.</param>
-        public TrackMetadata(string title, string language, Track track) : base(title, language)
-        {
-            if (track == null)
-            {
-                throw new ArgumentNullException(nameof(track));
-            }
-
-            track.TrackMetadata.Add(this);
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="TrackMetadata"/> class.
-        /// </summary>
-        /// <remarks>
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </remarks>
-        protected TrackMetadata()
+        public TrackMetadata(string title, string language) : base(title, language)
         {
         }
     }

+ 2 - 8
Jellyfin.Data/Entities/Permission.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA1711 // Identifiers should not have incorrect suffix
+
 using System.ComponentModel.DataAnnotations;
 using System.ComponentModel.DataAnnotations.Schema;
 using Jellyfin.Data.Enums;
@@ -22,14 +24,6 @@ namespace Jellyfin.Data.Entities
             Value = value;
         }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="Permission"/> class.
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </summary>
-        protected Permission()
-        {
-        }
-
         /// <summary>
         /// Gets or sets the id of this permission.
         /// </summary>

+ 0 - 9
Jellyfin.Data/Entities/Preference.cs

@@ -23,14 +23,6 @@ namespace Jellyfin.Data.Entities
             Value = value ?? throw new ArgumentNullException(nameof(value));
         }
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="Preference"/> class.
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </summary>
-        protected Preference()
-        {
-        }
-
         /// <summary>
         /// Gets or sets the id of this preference.
         /// </summary>
@@ -54,7 +46,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required, Max length = 65535.
         /// </remarks>
-        [Required]
         [MaxLength(65535)]
         [StringLength(65535)]
         public string Value { get; set; }

+ 25 - 33
Jellyfin.Data/Entities/User.cs

@@ -51,6 +51,7 @@ namespace Jellyfin.Data.Entities
             PasswordResetProviderId = passwordResetProviderId;
 
             AccessSchedules = new HashSet<AccessSchedule>();
+            DisplayPreferences = new HashSet<DisplayPreferences>();
             ItemDisplayPreferences = new HashSet<ItemDisplayPreferences>();
             // Groups = new HashSet<Group>();
             Permissions = new HashSet<Permission>();
@@ -72,17 +73,6 @@ namespace Jellyfin.Data.Entities
             PlayDefaultAudioTrack = true;
             SubtitleMode = SubtitlePlaybackMode.Default;
             SyncPlayAccess = SyncPlayUserAccessType.CreateAndJoinGroups;
-
-            AddDefaultPermissions();
-            AddDefaultPreferences();
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="User"/> class.
-        /// Default constructor. Protected due to required properties, but present because EF needs it.
-        /// </summary>
-        protected User()
-        {
         }
 
         /// <summary>
@@ -100,7 +90,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required, Max length = 255.
         /// </remarks>
-        [Required]
         [MaxLength(255)]
         [StringLength(255)]
         public string Username { get; set; }
@@ -113,7 +102,7 @@ namespace Jellyfin.Data.Entities
         /// </remarks>
         [MaxLength(65535)]
         [StringLength(65535)]
-        public string Password { get; set; }
+        public string? Password { get; set; }
 
         /// <summary>
         /// Gets or sets the user's easy password, or <c>null</c> if none is set.
@@ -123,7 +112,7 @@ namespace Jellyfin.Data.Entities
         /// </remarks>
         [MaxLength(65535)]
         [StringLength(65535)]
-        public string EasyPassword { get; set; }
+        public string? EasyPassword { get; set; }
 
         /// <summary>
         /// Gets or sets a value indicating whether the user must update their password.
@@ -141,7 +130,7 @@ namespace Jellyfin.Data.Entities
         /// </remarks>
         [MaxLength(255)]
         [StringLength(255)]
-        public string AudioLanguagePreference { get; set; }
+        public string? AudioLanguagePreference { get; set; }
 
         /// <summary>
         /// Gets or sets the authentication provider id.
@@ -149,7 +138,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required, Max length = 255.
         /// </remarks>
-        [Required]
         [MaxLength(255)]
         [StringLength(255)]
         public string AuthenticationProviderId { get; set; }
@@ -160,7 +148,6 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required, Max length = 255.
         /// </remarks>
-        [Required]
         [MaxLength(255)]
         [StringLength(255)]
         public string PasswordResetProviderId { get; set; }
@@ -217,7 +204,7 @@ namespace Jellyfin.Data.Entities
         /// </remarks>
         [MaxLength(255)]
         [StringLength(255)]
-        public string SubtitleLanguagePreference { get; set; }
+        public string? SubtitleLanguagePreference { get; set; }
 
         /// <summary>
         /// Gets or sets a value indicating whether missing episodes should be displayed.
@@ -312,7 +299,7 @@ namespace Jellyfin.Data.Entities
         /// Gets or sets the user's profile image. Can be <c>null</c>.
         /// </summary>
         // [ForeignKey("UserId")]
-        public virtual ImageInfo ProfileImage { get; set; }
+        public virtual ImageInfo? ProfileImage { get; set; }
 
         /// <summary>
         /// Gets or sets the user's display preferences.
@@ -320,8 +307,7 @@ namespace Jellyfin.Data.Entities
         /// <remarks>
         /// Required.
         /// </remarks>
-        [Required]
-        public virtual DisplayPreferences DisplayPreferences { get; set; }
+        public virtual ICollection<DisplayPreferences> DisplayPreferences { get; set; }
 
         /// <summary>
         /// Gets or sets the level of sync play permissions this user has.
@@ -494,18 +480,11 @@ namespace Jellyfin.Data.Entities
             return Array.IndexOf(GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders), id) != -1;
         }
 
-        private static bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date)
-        {
-            var localTime = date.ToLocalTime();
-            var hour = localTime.TimeOfDay.TotalHours;
-
-            return DayOfWeekHelper.GetDaysOfWeek(schedule.DayOfWeek).Contains(localTime.DayOfWeek)
-                   && hour >= schedule.StartHour
-                   && hour <= schedule.EndHour;
-        }
-
+        /// <summary>
+        /// Initializes the default permissions for a user. Should only be called on user creation.
+        /// </summary>
         // TODO: make these user configurable?
-        private void AddDefaultPermissions()
+        public void AddDefaultPermissions()
         {
             Permissions.Add(new Permission(PermissionKind.IsAdministrator, false));
             Permissions.Add(new Permission(PermissionKind.IsDisabled, false));
@@ -530,12 +509,25 @@ namespace Jellyfin.Data.Entities
             Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false));
         }
 
-        private void AddDefaultPreferences()
+        /// <summary>
+        /// Initializes the default preferences. Should only be called on user creation.
+        /// </summary>
+        public void AddDefaultPreferences()
         {
             foreach (var val in Enum.GetValues(typeof(PreferenceKind)).Cast<PreferenceKind>())
             {
                 Preferences.Add(new Preference(val, string.Empty));
             }
         }
+
+        private static bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date)
+        {
+            var localTime = date.ToLocalTime();
+            var hour = localTime.TimeOfDay.TotalHours;
+
+            return DayOfWeekHelper.GetDaysOfWeek(schedule.DayOfWeek).Contains(localTime.DayOfWeek)
+                   && hour >= schedule.StartHour
+                   && hour <= schedule.EndHour;
+        }
     }
 }

+ 1 - 1
Jellyfin.Data/Interfaces/IHasPermissions.cs

@@ -2,7 +2,7 @@
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 
-namespace Jellyfin.Data
+namespace Jellyfin.Data.Interfaces
 {
     /// <summary>
     /// An abstraction representing an entity that has permissions.

+ 4 - 6
Jellyfin.Data/Jellyfin.Data.csproj

@@ -5,6 +5,9 @@
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+    <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
+    <Nullable>enable</Nullable>
     <PublishRepositoryUrl>true</PublishRepositoryUrl>
     <EmbedUntrackedSources>true</EmbedUntrackedSources>
     <IncludeSymbols>true</IncludeSymbols>
@@ -24,10 +27,6 @@
     <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
   </PropertyGroup>
 
-  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
-
   <ItemGroup>
     <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
   </ItemGroup>
@@ -40,8 +39,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.3" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.3" />
+    <PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" />
   </ItemGroup>
 
   <ItemGroup>

+ 7 - 4
Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj

@@ -11,6 +11,8 @@
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <Nullable>enable</Nullable>
+    <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
   </PropertyGroup>
 
   <ItemGroup>
@@ -30,6 +32,11 @@
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
   </ItemGroup>
 
+  <ItemGroup>
+    <!-- Needed for https://github.com/dotnet/roslyn-analyzers/issues/4382 which is in the SDK yet -->
+    <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3" PrivateAssets="All" />
+  </ItemGroup>
+
   <!-- Code analysers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
     <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
@@ -37,8 +44,4 @@
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
   </ItemGroup>
 
-  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
-
 </Project>

+ 4 - 9
Jellyfin.Drawing.Skia/SkiaEncoder.cs

@@ -274,8 +274,8 @@ namespace Jellyfin.Drawing.Skia
 
             if (requiresTransparencyHack || forceCleanBitmap)
             {
-                using var codec = SKCodec.Create(NormalizePath(path));
-                if (codec == null)
+                using SKCodec codec = SKCodec.Create(NormalizePath(path), out SKCodecResult res);
+                if (res != SKCodecResult.Success)
                 {
                     origin = GetSKEncodedOrigin(orientation);
                     return null;
@@ -345,11 +345,6 @@ namespace Jellyfin.Drawing.Skia
 
         private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin)
         {
-            if (origin == SKEncodedOrigin.Default)
-            {
-                return bitmap;
-            }
-
             var needsFlip = origin == SKEncodedOrigin.LeftBottom
                             || origin == SKEncodedOrigin.LeftTop
                             || origin == SKEncodedOrigin.RightBottom
@@ -447,7 +442,7 @@ namespace Jellyfin.Drawing.Skia
         }
 
         /// <inheritdoc/>
-        public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat)
+        public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat)
         {
             if (inputPath.Length == 0)
             {
@@ -459,7 +454,7 @@ namespace Jellyfin.Drawing.Skia
                 throw new ArgumentException("String can't be empty.", nameof(outputPath));
             }
 
-            var skiaOutputFormat = GetImageFormat(selectedOutputFormat);
+            var skiaOutputFormat = GetImageFormat(outputFormat);
 
             var hasBackgroundColor = !string.IsNullOrWhiteSpace(options.BackgroundColor);
             var hasForegroundColor = !string.IsNullOrWhiteSpace(options.ForegroundLayer);

+ 2 - 4
Jellyfin.Networking/Jellyfin.Networking.csproj

@@ -5,6 +5,8 @@
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <Nullable>enable</Nullable>
+    <AnalysisMode>AllEnabledByDefault</AnalysisMode>
+    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
   </PropertyGroup>
 
   <ItemGroup>
@@ -18,10 +20,6 @@
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
   </ItemGroup>
 
-  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
-  </PropertyGroup>
-
   <ItemGroup>
     <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
     <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.