Browse Source

Merge remote-tracking branch 'upstream/master' into syncplay

gion 5 years ago
parent
commit
029bb80910
100 changed files with 4514 additions and 1322 deletions
  1. 0 59
      .copr/Makefile
  2. 1 0
      .copr/Makefile
  3. 7 10
      .editorconfig
  4. 3 0
      .github/CODEOWNERS
  5. 12 7
      .gitignore
  6. 2 0
      DvdLib/BigEndianBinaryReader.cs
  7. 1 0
      DvdLib/DvdLib.csproj
  8. 2 0
      DvdLib/Ifo/Cell.cs
  9. 2 0
      DvdLib/Ifo/CellPlaybackInfo.cs
  10. 2 0
      DvdLib/Ifo/CellPositionInfo.cs
  11. 2 0
      DvdLib/Ifo/Chapter.cs
  12. 2 0
      DvdLib/Ifo/Dvd.cs
  13. 2 0
      DvdLib/Ifo/DvdTime.cs
  14. 2 0
      DvdLib/Ifo/Program.cs
  15. 2 0
      DvdLib/Ifo/ProgramChain.cs
  16. 2 0
      DvdLib/Ifo/Title.cs
  17. 2 0
      DvdLib/Ifo/UserOperation.cs
  18. 4 1
      Emby.Dlna/PlayTo/PlayToController.cs
  19. 10 7
      Emby.Naming/Audio/AlbumParser.cs
  20. 2 1
      Emby.Naming/Audio/AudioFileParser.cs
  21. 1 6
      Emby.Naming/Common/EpisodeExpression.cs
  22. 4 8
      Emby.Naming/Subtitles/SubtitleParser.cs
  23. 3 3
      Emby.Naming/Video/VideoResolver.cs
  24. 1 1
      Emby.Notifications/NotificationEntryPoint.cs
  25. 38 70
      Emby.Server.Implementations/ApplicationHost.cs
  26. 4 4
      Emby.Server.Implementations/Browser/BrowserLauncher.cs
  27. 0 1
      Emby.Server.Implementations/ConfigurationOptions.cs
  28. 10 0
      Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
  29. 1 0
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  30. 2 2
      Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
  31. 109 113
      Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
  32. 1 1
      Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
  33. 0 39
      Emby.Server.Implementations/HttpServer/IHttpListener.cs
  34. 19 4
      Emby.Server.Implementations/HttpServer/ResponseFilter.cs
  35. 125 132
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  36. 4 2
      Emby.Server.Implementations/Library/MediaStreamSelector.cs
  37. 12 10
      Emby.Server.Implementations/Library/PathExtensions.cs
  38. 2 0
      Emby.Server.Implementations/Library/ResolverHelper.cs
  39. 2 2
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
  40. 3 3
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
  41. 25 0
      Emby.Server.Implementations/Library/UserManager.cs
  42. 1 1
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  43. 1 1
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
  44. 2 2
      Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
  45. 1 1
      Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
  46. 1 1
      Emby.Server.Implementations/Localization/Core/af.json
  47. 3 1
      Emby.Server.Implementations/Localization/Core/bn.json
  48. 1 1
      Emby.Server.Implementations/Localization/Core/es-MX.json
  49. 1 1
      Emby.Server.Implementations/Localization/Core/es.json
  50. 13 13
      Emby.Server.Implementations/Localization/Core/fi.json
  51. 19 1
      Emby.Server.Implementations/Localization/Core/fr-CA.json
  52. 57 35
      Emby.Server.Implementations/Localization/Core/gsw.json
  53. 10 2
      Emby.Server.Implementations/Localization/Core/he.json
  54. 10 2
      Emby.Server.Implementations/Localization/Core/hr.json
  55. 1 1
      Emby.Server.Implementations/Localization/Core/it.json
  56. 8 1
      Emby.Server.Implementations/Localization/Core/mk.json
  57. 5 1
      Emby.Server.Implementations/Localization/Core/nb.json
  58. 3 3
      Emby.Server.Implementations/Localization/Core/nl.json
  59. 22 1
      Emby.Server.Implementations/Localization/Core/sl-SI.json
  60. 4 3
      Emby.Server.Implementations/Localization/Core/sv.json
  61. 36 0
      Emby.Server.Implementations/Localization/Core/uk.json
  62. 5 2
      Emby.Server.Implementations/Localization/Core/zh-HK.json
  63. 0 39
      Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs
  64. 0 48
      Emby.Server.Implementations/Net/IWebSocket.cs
  65. 0 29
      Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs
  66. 2 14
      Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs
  67. 4 16
      Emby.Server.Implementations/Services/UrlExtensions.cs
  68. 0 191
      Emby.Server.Implementations/Session/HttpSessionController.cs
  69. 7 8
      Emby.Server.Implementations/Session/SessionManager.cs
  70. 17 16
      Emby.Server.Implementations/Session/SessionWebSocketListener.cs
  71. 51 35
      Emby.Server.Implementations/Session/WebSocketController.cs
  72. 0 105
      Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs
  73. 0 135
      Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs
  74. 10 14
      Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs
  75. 0 10
      Emby.Server.Implementations/WebSockets/WebSocketHandler.cs
  76. 0 102
      Emby.Server.Implementations/WebSockets/WebSocketManager.cs
  77. 195 0
      Jellyfin.Data/Entities/Artwork.cs
  78. 69 0
      Jellyfin.Data/Entities/Book.cs
  79. 107 0
      Jellyfin.Data/Entities/BookMetadata.cs
  80. 263 0
      Jellyfin.Data/Entities/Chapter.cs
  81. 120 0
      Jellyfin.Data/Entities/Collection.cs
  82. 143 0
      Jellyfin.Data/Entities/CollectionItem.cs
  83. 137 0
      Jellyfin.Data/Entities/Company.cs
  84. 216 0
      Jellyfin.Data/Entities/CompanyMetadata.cs
  85. 68 0
      Jellyfin.Data/Entities/CustomItem.cs
  86. 67 0
      Jellyfin.Data/Entities/CustomItemMetadata.cs
  87. 110 0
      Jellyfin.Data/Entities/Episode.cs
  88. 179 0
      Jellyfin.Data/Entities/EpisodeMetadata.cs
  89. 152 0
      Jellyfin.Data/Entities/Genre.cs
  90. 109 0
      Jellyfin.Data/Entities/Group.cs
  91. 147 0
      Jellyfin.Data/Entities/Library.cs
  92. 170 0
      Jellyfin.Data/Entities/LibraryItem.cs
  93. 192 0
      Jellyfin.Data/Entities/LibraryRoot.cs
  94. 200 0
      Jellyfin.Data/Entities/MediaFile.cs
  95. 149 0
      Jellyfin.Data/Entities/MediaFileStream.cs
  96. 380 0
      Jellyfin.Data/Entities/Metadata.cs
  97. 147 0
      Jellyfin.Data/Entities/MetadataProvider.cs
  98. 179 0
      Jellyfin.Data/Entities/MetadataProviderId.cs
  99. 69 0
      Jellyfin.Data/Entities/Movie.cs
  100. 223 0
      Jellyfin.Data/Entities/MovieMetadata.cs

+ 0 - 59
.copr/Makefile

@@ -1,59 +0,0 @@
-VERSION := $(shell sed -ne '/^Version:/s/.*  *//p'                      \
-                   deployment/fedora-package-x64/pkg-src/jellyfin.spec)
-
-deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz:
-	curl -f -L -o deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz \
-         https://github.com/jellyfin/jellyfin-web/archive/v$(VERSION).tar.gz \
-	|| curl -f -L -o deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz \
-         https://github.com/jellyfin/jellyfin-web/archive/master.tar.gz \
-
-srpm: deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz
-	cd deployment/fedora-package-x64;                                             \
-    SOURCE_DIR=../..                                                              \
-    WORKDIR="$${PWD}";                                                            \
-    package_temporary_dir="$${WORKDIR}/pkg-dist-tmp";                             \
-    pkg_src_dir="$${WORKDIR}/pkg-src";                                            \
-    GNU_TAR=1;                                                                    \
-    tar                                                                           \
-    --transform "s,^\.,jellyfin-$(VERSION),"                                      \
-    --exclude='.git*'                                                             \
-    --exclude='**/.git'                                                           \
-    --exclude='**/.hg'                                                            \
-    --exclude='**/.vs'                                                            \
-    --exclude='**/.vscode'                                                        \
-    --exclude='deployment'                                                        \
-    --exclude='**/bin'                                                            \
-    --exclude='**/obj'                                                            \
-    --exclude='**/.nuget'                                                         \
-    --exclude='*.deb'                                                             \
-    --exclude='*.rpm'                                                             \
-    -czf "pkg-src/jellyfin-$(VERSION).tar.gz"                                     \
-    -C $${SOURCE_DIR} ./ || GNU_TAR=0;                                            \
-    if [ $${GNU_TAR} -eq 0 ]; then                                                \
-        package_temporary_dir="$$(mktemp -d)";                                    \
-        mkdir -p "$${package_temporary_dir}/jellyfin";                            \
-        tar                                                                       \
-        --exclude='.git*'                                                         \
-        --exclude='**/.git'                                                       \
-        --exclude='**/.hg'                                                        \
-        --exclude='**/.vs'                                                        \
-        --exclude='**/.vscode'                                                    \
-        --exclude='deployment'                                                    \
-        --exclude='**/bin'                                                        \
-        --exclude='**/obj'                                                        \
-        --exclude='**/.nuget'                                                     \
-        --exclude='*.deb'                                                         \
-        --exclude='*.rpm'                                                         \
-        -czf "$${package_temporary_dir}/jellyfin/jellyfin-$(VERSION).tar.gz"      \
-        -C $${SOURCE_DIR} ./;                                                     \
-        mkdir -p "$${package_temporary_dir}/jellyfin-$(VERSION)";                 \
-        tar -xzf "$${package_temporary_dir}/jellyfin/jellyfin-$(VERSION).tar.gz"  \
-            -C "$${package_temporary_dir}/jellyfin-$(VERSION);                    \
-        rm -f "$${package_temporary_dir}/jellyfin/jellyfin-$(VERSION).tar.gz";    \
-        tar -czf "$${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-$(VERSION).tar.gz"      \
-            -C "$${package_temporary_dir}" "jellyfin-$(VERSION);                  \
-        rm -rf $${package_temporary_dir};                                         \
-	fi;                                                                           \
-	rpmbuild -bs pkg-src/jellyfin.spec                                            \
-	         --define "_sourcedir $$PWD/pkg-src/"                                 \
-	         --define "_srcrpmdir $(outdir)"

+ 1 - 0
.copr/Makefile

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

+ 7 - 10
.editorconfig

@@ -13,7 +13,7 @@ charset = utf-8
 trim_trailing_whitespace = true
 insert_final_newline = true
 end_of_line = lf
-max_line_length = null
+max_line_length = off
 
 # YAML indentation
 [*.{yml,yaml}]
@@ -22,6 +22,7 @@ indent_size = 2
 # XML indentation
 [*.{csproj,xml}]
 indent_size = 2
+
 ###############################
 # .NET Coding Conventions     #
 ###############################
@@ -51,11 +52,12 @@ dotnet_style_explicit_tuple_names = true:suggestion
 dotnet_style_null_propagation = true:suggestion
 dotnet_style_coalesce_expression = true:suggestion
 dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
-dotnet_prefer_inferred_tuple_names = true:suggestion
-dotnet_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
 dotnet_style_prefer_auto_properties = true:silent
 dotnet_style_prefer_conditional_expression_over_assignment = true:silent
 dotnet_style_prefer_conditional_expression_over_return = true:silent
+
 ###############################
 # Naming Conventions          #
 ###############################
@@ -67,7 +69,7 @@ dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non
 dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style
 
 dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field
-dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected internal, private protected
+dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
 dotnet_naming_symbols.non_private_static_fields.required_modifiers = static
 
 dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
@@ -159,6 +161,7 @@ csharp_style_deconstructed_variable_declaration = true:suggestion
 csharp_prefer_simple_default_expression = true:suggestion
 csharp_style_pattern_local_over_anonymous_function = true:suggestion
 csharp_style_inlined_variable_declaration = true:suggestion
+
 ###############################
 # C# Formatting Rules         #
 ###############################
@@ -189,9 +192,3 @@ csharp_space_between_method_call_empty_parameter_list_parentheses = false
 # Wrapping preferences
 csharp_preserve_single_line_statements = true
 csharp_preserve_single_line_blocks = true
-###############################
-# VB Coding Conventions       #
-###############################
-[*.vb]
-# Modifier preferences
-visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion

+ 3 - 0
.github/CODEOWNERS

@@ -0,0 +1,3 @@
+# Joshua must review all changes to deployment and build.sh
+deployment/*    @joshuaboniface
+build.sh        @joshuaboniface

+ 12 - 7
.gitignore

@@ -245,14 +245,14 @@ pip-log.txt
 #########################
 
 # Artifacts for debian-x64
-deployment/debian-package-x64/pkg-src/.debhelper/
-deployment/debian-package-x64/pkg-src/*.debhelper
-deployment/debian-package-x64/pkg-src/debhelper-build-stamp
-deployment/debian-package-x64/pkg-src/files
-deployment/debian-package-x64/pkg-src/jellyfin.substvars
-deployment/debian-package-x64/pkg-src/jellyfin/
+debian/.debhelper/
+debian/*.debhelper
+debian/debhelper-build-stamp
+debian/files
+debian/jellyfin.substvars
+debian/jellyfin/
 # Don't ignore the debian/bin folder
-!deployment/debian-package-x64/pkg-src/bin/
+!debian/bin/
 
 deployment/**/dist/
 deployment/**/pkg-dist/
@@ -272,3 +272,8 @@ dist
 
 # BenchmarkDotNet artifacts
 BenchmarkDotNet.Artifacts
+
+# Ignore web artifacts from native builds
+web/
+web-src.*
+MediaBrowser.WebDashboard/jellyfin-web/

+ 2 - 0
DvdLib/BigEndianBinaryReader.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Buffers.Binary;
 using System.IO;
 

+ 1 - 0
DvdLib/DvdLib.csproj

@@ -13,6 +13,7 @@
     <TargetFramework>netstandard2.1</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
+    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
   </PropertyGroup>
 
 </Project>

+ 2 - 0
DvdLib/Ifo/Cell.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.IO;
 
 namespace DvdLib.Ifo

+ 2 - 0
DvdLib/Ifo/CellPlaybackInfo.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.IO;
 
 namespace DvdLib.Ifo

+ 2 - 0
DvdLib/Ifo/CellPositionInfo.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.IO;
 
 namespace DvdLib.Ifo

+ 2 - 0
DvdLib/Ifo/Chapter.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 namespace DvdLib.Ifo
 {
     public class Chapter

+ 2 - 0
DvdLib/Ifo/Dvd.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 using System.Collections.Generic;
 using System.IO;

+ 2 - 0
DvdLib/Ifo/DvdTime.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 
 namespace DvdLib.Ifo

+ 2 - 0
DvdLib/Ifo/Program.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 
 namespace DvdLib.Ifo

+ 2 - 0
DvdLib/Ifo/ProgramChain.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;

+ 2 - 0
DvdLib/Ifo/Title.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System.Collections.Generic;
 using System.IO;
 

+ 2 - 0
DvdLib/Ifo/UserOperation.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CS1591
+
 using System;
 
 namespace DvdLib.Ifo

+ 4 - 1
Emby.Dlna/PlayTo/PlayToController.cs

@@ -908,7 +908,8 @@ namespace Emby.Dlna.PlayTo
             return 0;
         }
 
-        public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken)
+        /// <inheritdoc />
+        public Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken)
         {
             if (_disposed)
             {
@@ -924,10 +925,12 @@ namespace Emby.Dlna.PlayTo
             {
                 return SendPlayCommand(data as PlayRequest, cancellationToken);
             }
+
             if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
             {
                 return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
             }
+
             if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
             {
                 return SendGeneralCommand(data as GeneralCommand, cancellationToken);

+ 10 - 7
Emby.Naming/Audio/AlbumParser.cs

@@ -1,9 +1,9 @@
+#nullable enable
 #pragma warning disable CS1591
 
 using System;
 using System.Globalization;
 using System.IO;
-using System.Linq;
 using System.Text.RegularExpressions;
 using Emby.Naming.Common;
 
@@ -21,8 +21,7 @@ namespace Emby.Naming.Audio
         public bool IsMultiPart(string path)
         {
             var filename = Path.GetFileName(path);
-
-            if (string.IsNullOrEmpty(filename))
+            if (filename.Length == 0)
             {
                 return false;
             }
@@ -39,18 +38,22 @@ namespace Emby.Naming.Audio
             filename = filename.Replace(')', ' ');
             filename = Regex.Replace(filename, @"\s+", " ");
 
-            filename = filename.TrimStart();
+            ReadOnlySpan<char> trimmedFilename = filename.TrimStart();
 
             foreach (var prefix in _options.AlbumStackingPrefixes)
             {
-                if (filename.IndexOf(prefix, StringComparison.OrdinalIgnoreCase) != 0)
+                if (!trimmedFilename.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
                 {
                     continue;
                 }
 
-                var tmp = filename.Substring(prefix.Length);
+                var tmp = trimmedFilename.Slice(prefix.Length).Trim();
 
-                tmp = tmp.Trim().Split(' ').FirstOrDefault() ?? string.Empty;
+                int index = tmp.IndexOf(' ');
+                if (index != -1)
+                {
+                    tmp = tmp.Slice(0, index);
+                }
 
                 if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
                 {

+ 2 - 1
Emby.Naming/Audio/AudioFileParser.cs

@@ -1,3 +1,4 @@
+#nullable enable
 #pragma warning disable CS1591
 
 using System;
@@ -11,7 +12,7 @@ namespace Emby.Naming.Audio
     {
         public static bool IsAudioFile(string path, NamingOptions options)
         {
-            var extension = Path.GetExtension(path) ?? string.Empty;
+            var extension = Path.GetExtension(path);
             return options.AudioFileExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase);
         }
     }

+ 1 - 6
Emby.Naming/Common/EpisodeExpression.cs

@@ -23,11 +23,6 @@ namespace Emby.Naming.Common
         {
         }
 
-        public EpisodeExpression()
-            : this(null)
-        {
-        }
-
         public string Expression
         {
             get => _expression;
@@ -48,6 +43,6 @@ namespace Emby.Naming.Common
 
         public string[] DateTimeFormats { get; set; }
 
-        public Regex Regex => _regex ?? (_regex = new Regex(Expression, RegexOptions.IgnoreCase | RegexOptions.Compiled));
+        public Regex Regex => _regex ??= new Regex(Expression, RegexOptions.IgnoreCase | RegexOptions.Compiled);
     }
 }

+ 4 - 8
Emby.Naming/Subtitles/SubtitleParser.cs

@@ -1,3 +1,4 @@
+#nullable enable
 #pragma warning disable CS1591
 
 using System;
@@ -16,11 +17,11 @@ namespace Emby.Naming.Subtitles
             _options = options;
         }
 
-        public SubtitleInfo ParseFile(string path)
+        public SubtitleInfo? ParseFile(string path)
         {
-            if (string.IsNullOrEmpty(path))
+            if (path.Length == 0)
             {
-                throw new ArgumentNullException(nameof(path));
+                throw new ArgumentException("File path can't be empty.", nameof(path));
             }
 
             var extension = Path.GetExtension(path);
@@ -52,11 +53,6 @@ namespace Emby.Naming.Subtitles
 
         private string[] GetFlags(string path)
         {
-            if (string.IsNullOrEmpty(path))
-            {
-                throw new ArgumentNullException(nameof(path));
-            }
-
             // Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _.
 
             var file = Path.GetFileName(path);

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

@@ -89,14 +89,14 @@ namespace Emby.Naming.Video
             if (parseName)
             {
                 var cleanDateTimeResult = CleanDateTime(name);
+                name = cleanDateTimeResult.Name;
+                year = cleanDateTimeResult.Year;
 
                 if (extraResult.ExtraType == null
-                    && TryCleanString(cleanDateTimeResult.Name, out ReadOnlySpan<char> newName))
+                    && TryCleanString(name, out ReadOnlySpan<char> newName))
                 {
                     name = newName.ToString();
                 }
-
-                year = cleanDateTimeResult.Year;
             }
 
             return new VideoFileInfo

+ 1 - 1
Emby.Notifications/NotificationEntryPoint.cs

@@ -143,7 +143,7 @@ namespace Emby.Notifications
 
             var notification = new NotificationRequest
             {
-                Description = "Please see jellyfin.media for details.",
+                Description = "Please see jellyfin.org for details.",
                 NotificationType = type,
                 Name = _localization.GetLocalizedString("NewVersionIsAvailable")
             };

+ 38 - 70
Emby.Server.Implementations/ApplicationHost.cs

@@ -44,7 +44,6 @@ using Emby.Server.Implementations.Security;
 using Emby.Server.Implementations.Serialization;
 using Emby.Server.Implementations.Services;
 using Emby.Server.Implementations.Session;
-using Emby.Server.Implementations.SocketSharp;
 using Emby.Server.Implementations.TV;
 using Emby.Server.Implementations.Updates;
 using Emby.Server.Implementations.SyncPlay;
@@ -96,7 +95,6 @@ using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Services;
 using MediaBrowser.Model.System;
 using MediaBrowser.Model.Tasks;
-using MediaBrowser.Model.Updates;
 using MediaBrowser.Providers.Chapters;
 using MediaBrowser.Providers.Manager;
 using MediaBrowser.Providers.Plugins.TheTvdb;
@@ -104,9 +102,9 @@ using MediaBrowser.Providers.Subtitles;
 using MediaBrowser.WebDashboard.Api;
 using MediaBrowser.XbmcMetadata.Providers;
 using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
+using Prometheus.DotNetRuntime;
 using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
 
 namespace Emby.Server.Implementations
@@ -261,6 +259,12 @@ namespace Emby.Server.Implementations
 
             _startupOptions = options;
 
+            // Initialize runtime stat collection
+            if (ServerConfigurationManager.Configuration.EnableMetrics)
+            {
+                DotNetRuntimeStatsBuilder.Default().StartCollecting();
+            }
+
             fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
 
             _networkManager.NetworkChanged += OnNetworkChanged;
@@ -498,32 +502,8 @@ namespace Emby.Server.Implementations
             RegisterServices(serviceCollection);
         }
 
-        public async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next)
-        {
-            if (!context.WebSockets.IsWebSocketRequest)
-            {
-                await next().ConfigureAwait(false);
-                return;
-            }
-
-            await _httpServer.ProcessWebSocketRequest(context).ConfigureAwait(false);
-        }
-
-        public async Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
-        {
-            if (context.WebSockets.IsWebSocketRequest)
-            {
-                await next().ConfigureAwait(false);
-                return;
-            }
-
-            var request = context.Request;
-            var response = context.Response;
-            var localPath = context.Request.Path.ToString();
-
-            var req = new WebSocketSharpRequest(request, response, request.Path, LoggerFactory.CreateLogger<WebSocketSharpRequest>());
-            await _httpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted).ConfigureAwait(false);
-        }
+        public Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
+            => _httpServer.RequestHandler(context);
 
         /// <summary>
         /// Registers services/resources with the service collection that will be available via DI.
@@ -541,13 +521,6 @@ namespace Emby.Server.Implementations
 
             serviceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
 
-            // TODO: Remove support for injecting ILogger completely
-            serviceCollection.AddSingleton((provider) =>
-            {
-                Logger.LogWarning("Injecting ILogger directly is deprecated and should be replaced with ILogger<T>");
-                return Logger;
-            });
-
             serviceCollection.AddSingleton(_fileSystemManager);
             serviceCollection.AddSingleton<TvdbClientManager>();
 
@@ -616,7 +589,6 @@ namespace Emby.Server.Implementations
             serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
 
             serviceCollection.AddSingleton<ServiceController>();
-            serviceCollection.AddSingleton<IHttpListener, WebSocketSharpListener>();
             serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
 
             serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
@@ -1147,9 +1119,6 @@ namespace Emby.Server.Implementations
                 ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
                 InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
                 CachePath = ApplicationPaths.CachePath,
-                HttpServerPortNumber = HttpPort,
-                SupportsHttps = SupportsHttps,
-                HttpsPortNumber = HttpsPort,
                 OperatingSystem = OperatingSystem.Id.ToString(),
                 OperatingSystemDisplayName = OperatingSystem.Name,
                 CanSelfRestart = CanSelfRestart,
@@ -1185,23 +1154,22 @@ namespace Emby.Server.Implementations
             };
         }
 
-        public bool EnableHttps => SupportsHttps && ServerConfigurationManager.Configuration.EnableHttps;
-
-        public bool SupportsHttps => Certificate != null || ServerConfigurationManager.Configuration.IsBehindProxy;
+        /// <inheritdoc/>
+        public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.Configuration.EnableHttps;
 
-        public async Task<string> GetLocalApiUrl(CancellationToken cancellationToken, bool forceHttp = false)
+        /// <inheritdoc/>
+        public async Task<string> GetLocalApiUrl(CancellationToken cancellationToken)
         {
             try
             {
                 // Return the first matched address, if found, or the first known local address
                 var addresses = await GetLocalIpAddressesInternal(false, 1, cancellationToken).ConfigureAwait(false);
-
-                foreach (var address in addresses)
+                if (addresses.Count == 0)
                 {
-                    return GetLocalApiUrl(address, forceHttp);
+                    return null;
                 }
 
-                return null;
+                return GetLocalApiUrl(addresses.First());
             }
             catch (Exception ex)
             {
@@ -1228,7 +1196,7 @@ namespace Emby.Server.Implementations
         }
 
         /// <inheritdoc />
-        public string GetLocalApiUrl(IPAddress ipAddress, bool forceHttp = false)
+        public string GetLocalApiUrl(IPAddress ipAddress)
         {
             if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
             {
@@ -1238,29 +1206,30 @@ namespace Emby.Server.Implementations
                 str.CopyTo(span.Slice(1));
                 span[^1] = ']';
 
-                return GetLocalApiUrl(span, forceHttp);
+                return GetLocalApiUrl(span);
             }
 
-            return GetLocalApiUrl(ipAddress.ToString(), forceHttp);
+            return GetLocalApiUrl(ipAddress.ToString());
         }
 
-        /// <inheritdoc />
-        public string GetLocalApiUrl(ReadOnlySpan<char> host, bool forceHttp = false)
+        /// <inheritdoc/>
+        public string GetLoopbackHttpApiUrl()
         {
-            var url = new StringBuilder(64);
-            bool useHttps = EnableHttps && !forceHttp;
-            url.Append(useHttps ? "https://" : "http://")
-                .Append(host)
-                .Append(':')
-                .Append(useHttps ? HttpsPort : HttpPort);
-
-            string baseUrl = ServerConfigurationManager.Configuration.BaseUrl;
-            if (baseUrl.Length != 0)
-            {
-                url.Append(baseUrl);
-            }
+            return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort);
+        }
 
-            return url.ToString();
+        /// <inheritdoc/>
+        public string GetLocalApiUrl(ReadOnlySpan<char> host, string scheme = null, int? port = null)
+        {
+            // NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
+            // not. For consistency, always trim the trailing slash.
+            return new UriBuilder
+            {
+                Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
+                Host = host.ToString(),
+                Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
+                Path = ServerConfigurationManager.Configuration.BaseUrl
+            }.ToString().TrimEnd('/');
         }
 
         public Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken)
@@ -1294,7 +1263,7 @@ namespace Emby.Server.Implementations
                     }
                 }
 
-                var valid = await IsIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false);
+                var valid = await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false);
                 if (valid)
                 {
                     resultList.Add(address);
@@ -1328,7 +1297,7 @@ namespace Emby.Server.Implementations
 
         private readonly ConcurrentDictionary<string, bool> _validAddressResults = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
 
-        private async Task<bool> IsIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken)
+        private async Task<bool> IsLocalIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken)
         {
             if (address.Equals(IPAddress.Loopback)
                 || address.Equals(IPAddress.IPv6Loopback))
@@ -1336,8 +1305,7 @@ namespace Emby.Server.Implementations
                 return true;
             }
 
-            var apiUrl = GetLocalApiUrl(address);
-            apiUrl += "/system/ping";
+            var apiUrl = GetLocalApiUrl(address) + "/system/ping";
 
             if (_validAddressResults.TryGetValue(apiUrl, out var cachedResult))
             {

+ 4 - 4
Emby.Server.Implementations/Browser/BrowserLauncher.cs

@@ -31,18 +31,18 @@ namespace Emby.Server.Implementations.Browser
         /// Opens the specified URL in an external browser window. Any exceptions will be logged, but ignored.
         /// </summary>
         /// <param name="appHost">The application host.</param>
-        /// <param name="url">The URL.</param>
-        private static void TryOpenUrl(IServerApplicationHost appHost, string url)
+        /// <param name="relativeUrl">The URL to open, relative to the server base URL.</param>
+        private static void TryOpenUrl(IServerApplicationHost appHost, string relativeUrl)
         {
             try
             {
                 string baseUrl = appHost.GetLocalApiUrl("localhost");
-                appHost.LaunchUrl(baseUrl + url);
+                appHost.LaunchUrl(baseUrl + relativeUrl);
             }
             catch (Exception ex)
             {
                 var logger = appHost.Resolve<ILogger>();
-                logger?.LogError(ex, "Failed to open browser window with URL {URL}", url);
+                logger?.LogError(ex, "Failed to open browser window with URL {URL}", relativeUrl);
             }
         }
     }

+ 0 - 1
Emby.Server.Implementations/ConfigurationOptions.cs

@@ -1,7 +1,6 @@
 using System.Collections.Generic;
 using Emby.Server.Implementations.HttpServer;
 using Emby.Server.Implementations.Updates;
-using MediaBrowser.Providers.Music;
 using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
 
 namespace Emby.Server.Implementations

+ 10 - 0
Emby.Server.Implementations/Data/SqliteUserDataRepository.cs

@@ -375,5 +375,15 @@ namespace Emby.Server.Implementations.Data
 
             return userData;
         }
+
+        /// <inheritdoc/>
+        /// <remarks>
+        /// There is nothing to dispose here since <see cref="BaseSqliteRepository.WriteLock"/> and
+        /// <see cref="BaseSqliteRepository.WriteConnection"/> are managed by <see cref="SqliteItemRepository"/>.
+        /// See <see cref="Initialize(IUserManager, SemaphoreSlim, SQLiteDatabaseConnection)"/>.
+        /// </remarks>
+        protected override void Dispose(bool dispose)
+        {
+        }
     }
 }

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

@@ -39,6 +39,7 @@
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.3" />
     <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.3" />
     <PackageReference Include="Mono.Nat" Version="2.0.1" />
+    <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
     <PackageReference Include="ServiceStack.Text.Core" Version="5.8.0" />
     <PackageReference Include="sharpcompress" Version="0.25.0" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />

+ 2 - 2
Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs

@@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.EntryPoints
                 .Append(config.PublicHttpsPort).Append(Separator)
                 .Append(_appHost.HttpPort).Append(Separator)
                 .Append(_appHost.HttpsPort).Append(Separator)
-                .Append(_appHost.EnableHttps).Append(Separator)
+                .Append(_appHost.ListenWithHttps).Append(Separator)
                 .Append(config.EnableRemoteAccess).Append(Separator)
                 .ToString();
         }
@@ -158,7 +158,7 @@ namespace Emby.Server.Implementations.EntryPoints
         {
             yield return CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort);
 
-            if (_appHost.EnableHttps)
+            if (_appHost.ListenWithHttps)
             {
                 yield return CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort);
             }

+ 109 - 113
Emby.Server.Implementations/HttpServer/HttpListenerHost.cs

@@ -6,11 +6,12 @@ using System.Diagnostics;
 using System.IO;
 using System.Linq;
 using System.Net.Sockets;
+using System.Net.WebSockets;
 using System.Reflection;
 using System.Threading;
 using System.Threading.Tasks;
-using Emby.Server.Implementations.Net;
 using Emby.Server.Implementations.Services;
+using Emby.Server.Implementations.SocketSharp;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
@@ -22,15 +23,17 @@ using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Services;
 using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
 using Microsoft.AspNetCore.WebUtilities;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Primitives;
 using ServiceStack.Text.Jsv;
 
 namespace Emby.Server.Implementations.HttpServer
 {
-    public class HttpListenerHost : IHttpServer, IDisposable
+    public class HttpListenerHost : IHttpServer
     {
         /// <summary>
         /// The key for a setting that specifies the default redirect path
@@ -39,17 +42,17 @@ namespace Emby.Server.Implementations.HttpServer
         public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath";
 
         private readonly ILogger _logger;
+        private readonly ILoggerFactory _loggerFactory;
         private readonly IServerConfigurationManager _config;
         private readonly INetworkManager _networkManager;
         private readonly IServerApplicationHost _appHost;
         private readonly IJsonSerializer _jsonSerializer;
         private readonly IXmlSerializer _xmlSerializer;
-        private readonly IHttpListener _socketListener;
         private readonly Func<Type, Func<string, object>> _funcParseFn;
         private readonly string _defaultRedirectPath;
         private readonly string _baseUrlPrefix;
+
         private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>();
-        private readonly List<IWebSocketConnection> _webSocketConnections = new List<IWebSocketConnection>();
         private readonly IHostEnvironment _hostEnvironment;
 
         private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
@@ -63,10 +66,10 @@ namespace Emby.Server.Implementations.HttpServer
             INetworkManager networkManager,
             IJsonSerializer jsonSerializer,
             IXmlSerializer xmlSerializer,
-            IHttpListener socketListener,
             ILocalizationManager localizationManager,
             ServiceController serviceController,
-            IHostEnvironment hostEnvironment)
+            IHostEnvironment hostEnvironment,
+            ILoggerFactory loggerFactory)
         {
             _appHost = applicationHost;
             _logger = logger;
@@ -76,11 +79,9 @@ namespace Emby.Server.Implementations.HttpServer
             _networkManager = networkManager;
             _jsonSerializer = jsonSerializer;
             _xmlSerializer = xmlSerializer;
-            _socketListener = socketListener;
             ServiceController = serviceController;
-
-            _socketListener.WebSocketConnected = OnWebSocketConnected;
             _hostEnvironment = hostEnvironment;
+            _loggerFactory = loggerFactory;
 
             _funcParseFn = t => s => JsvReader.GetParseFn(t)(s);
 
@@ -172,38 +173,6 @@ namespace Emby.Server.Implementations.HttpServer
             return attributes;
         }
 
-        private void OnWebSocketConnected(WebSocketConnectEventArgs e)
-        {
-            if (_disposed)
-            {
-                return;
-            }
-
-            var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger)
-            {
-                OnReceive = ProcessWebSocketMessageReceived,
-                Url = e.Url,
-                QueryString = e.QueryString
-            };
-
-            connection.Closed += OnConnectionClosed;
-
-            lock (_webSocketConnections)
-            {
-                _webSocketConnections.Add(connection);
-            }
-
-            WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
-        }
-
-        private void OnConnectionClosed(object sender, EventArgs e)
-        {
-            lock (_webSocketConnections)
-            {
-                _webSocketConnections.Remove((IWebSocketConnection)sender);
-            }
-        }
-
         private static Exception GetActualException(Exception ex)
         {
             if (ex is AggregateException agg)
@@ -289,32 +258,6 @@ namespace Emby.Server.Implementations.HttpServer
                 .Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase);
         }
 
-        /// <summary>
-        /// Shut down the Web Service
-        /// </summary>
-        public void Stop()
-        {
-            List<IWebSocketConnection> connections;
-
-            lock (_webSocketConnections)
-            {
-                connections = _webSocketConnections.ToList();
-                _webSocketConnections.Clear();
-            }
-
-            foreach (var connection in connections)
-            {
-                try
-                {
-                    connection.Dispose();
-                }
-                catch (Exception ex)
-                {
-                    _logger.LogError(ex, "Error disposing connection");
-                }
-            }
-        }
-
         public static string RemoveQueryStringByKey(string url, string key)
         {
             var uri = new Uri(url);
@@ -424,33 +367,52 @@ namespace Emby.Server.Implementations.HttpServer
             return true;
         }
 
+        /// <summary>
+        /// Validate a connection from a remote IP address to a URL to see if a redirection to HTTPS is required.
+        /// </summary>
+        /// <returns>True if the request is valid, or false if the request is not valid and an HTTPS redirect is required.</returns>
         private bool ValidateSsl(string remoteIp, string urlString)
         {
-            if (_config.Configuration.RequireHttps && _appHost.EnableHttps && !_config.Configuration.IsBehindProxy)
+            if (_config.Configuration.RequireHttps
+                && _appHost.ListenWithHttps
+                && !urlString.Contains("https://", StringComparison.OrdinalIgnoreCase))
             {
-                if (urlString.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == -1)
+                // These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
+                if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
+                    || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
                 {
-                    // These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
-                    if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
-                        || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
-                    {
-                        return true;
-                    }
+                    return true;
+                }
 
-                    if (!_networkManager.IsInLocalNetwork(remoteIp))
-                    {
-                        return false;
-                    }
+                if (!_networkManager.IsInLocalNetwork(remoteIp))
+                {
+                    return false;
                 }
             }
 
             return true;
         }
 
+        /// <inheritdoc />
+        public Task RequestHandler(HttpContext context)
+        {
+            if (context.WebSockets.IsWebSocketRequest)
+            {
+                return WebSocketRequestHandler(context);
+            }
+
+            var request = context.Request;
+            var response = context.Response;
+            var localPath = context.Request.Path.ToString();
+
+            var req = new WebSocketSharpRequest(request, response, request.Path, _logger);
+            return RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted);
+        }
+
         /// <summary>
         /// Overridable method that can be used to implement a custom handler.
         /// </summary>
-        public async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
+        private async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
         {
             var stopWatch = new Stopwatch();
             stopWatch.Start();
@@ -493,9 +455,10 @@ namespace Emby.Server.Implementations.HttpServer
                 if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
                 {
                     httpRes.StatusCode = 200;
-                    httpRes.Headers.Add("Access-Control-Allow-Origin", "*");
-                    httpRes.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
-                    httpRes.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization");
+                    foreach(var (key, value) in GetDefaultCorsHeaders(httpReq))
+                    {
+                        httpRes.Headers.Add(key, value);
+                    }
                     httpRes.ContentType = "text/plain";
                     await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false);
                     return;
@@ -578,6 +541,68 @@ namespace Emby.Server.Implementations.HttpServer
             }
         }
 
+        private async Task WebSocketRequestHandler(HttpContext context)
+        {
+            if (_disposed)
+            {
+                return;
+            }
+
+            try
+            {
+                _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
+
+                WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
+
+                var connection = new WebSocketConnection(
+                    _loggerFactory.CreateLogger<WebSocketConnection>(),
+                    webSocket,
+                    context.Connection.RemoteIpAddress,
+                    context.Request.Query)
+                {
+                    OnReceive = ProcessWebSocketMessageReceived
+                };
+
+                WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
+
+                await connection.ProcessAsync().ConfigureAwait(false);
+                _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
+            }
+            catch (Exception ex) // Otherwise ASP.Net will ignore the exception
+            {
+                _logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress);
+                if (!context.Response.HasStarted)
+                {
+                    context.Response.StatusCode = 500;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Get the default CORS headers
+        /// </summary>
+        /// <param name="req"></param>
+        /// <returns></returns>
+        public IDictionary<string, string> GetDefaultCorsHeaders(IRequest req)
+        {
+            var origin = req.Headers["Origin"];
+            if (origin == StringValues.Empty)
+            {
+                origin = req.Headers["Host"];
+                if (origin == StringValues.Empty)
+                {
+                    origin = "*";
+                }
+            }
+
+            var headers = new Dictionary<string, string>();
+            headers.Add("Access-Control-Allow-Origin", origin);
+            headers.Add("Access-Control-Allow-Credentials", "true");
+            headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
+            headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization, Cookie");
+            return headers;
+        }
+
         // Entry point for HttpListener
         public ServiceHandler GetServiceHandler(IHttpRequest httpReq)
         {
@@ -624,7 +649,7 @@ namespace Emby.Server.Implementations.HttpServer
 
             ResponseFilters = new Action<IRequest, HttpResponse, object>[]
             {
-                new ResponseFilter(_logger).FilterResponse
+                new ResponseFilter(this, _logger).FilterResponse
             };
         }
 
@@ -685,11 +710,6 @@ namespace Emby.Server.Implementations.HttpServer
             return _jsonSerializer.DeserializeFromStreamAsync(stream, type);
         }
 
-        public Task ProcessWebSocketRequest(HttpContext context)
-        {
-            return _socketListener.ProcessWebSocketRequest(context);
-        }
-
         private string NormalizeEmbyRoutePath(string path)
         {
             _logger.LogDebug("Normalizing /emby route");
@@ -708,28 +728,6 @@ namespace Emby.Server.Implementations.HttpServer
             return _baseUrlPrefix + NormalizeUrlPath(path);
         }
 
-        /// <inheritdoc />
-        public void Dispose()
-        {
-            Dispose(true);
-            GC.SuppressFinalize(this);
-        }
-
-        protected virtual void Dispose(bool disposing)
-        {
-            if (_disposed)
-            {
-                return;
-            }
-
-            if (disposing)
-            {
-                Stop();
-            }
-
-            _disposed = true;
-        }
-
         /// <summary>
         /// Processes the web socket message received.
         /// </summary>
@@ -741,8 +739,6 @@ namespace Emby.Server.Implementations.HttpServer
                 return Task.CompletedTask;
             }
 
-            _logger.LogDebug("Websocket message received: {0}", result.MessageType);
-
             IEnumerable<Task> GetTasks()
             {
                 foreach (var x in _webSocketListeners)

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

@@ -426,7 +426,7 @@ namespace Emby.Server.Implementations.HttpServer
 
             if (!noCache)
             {
-                if (!DateTime.TryParseExact(requestContext.Headers[HeaderNames.IfModifiedSince], HttpDateFormat, _enUSculture, DateTimeStyles.AssumeUniversal, out var ifModifiedSinceHeader))
+                if (!DateTime.TryParseExact(requestContext.Headers[HeaderNames.IfModifiedSince], HttpDateFormat, _enUSculture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var ifModifiedSinceHeader))
                 {
                     _logger.LogDebug("Failed to parse If-Modified-Since header date: {0}", requestContext.Headers[HeaderNames.IfModifiedSince]);
                     return null;

+ 0 - 39
Emby.Server.Implementations/HttpServer/IHttpListener.cs

@@ -1,39 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.Net;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-
-namespace Emby.Server.Implementations.HttpServer
-{
-    public interface IHttpListener : IDisposable
-    {
-        /// <summary>
-        /// Gets or sets the error handler.
-        /// </summary>
-        /// <value>The error handler.</value>
-        Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; }
-
-        /// <summary>
-        /// Gets or sets the request handler.
-        /// </summary>
-        /// <value>The request handler.</value>
-        Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; }
-
-        /// <summary>
-        /// Gets or sets the web socket handler.
-        /// </summary>
-        /// <value>The web socket handler.</value>
-        Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }
-
-        /// <summary>
-        /// Stops this instance.
-        /// </summary>
-        Task Stop();
-
-        Task ProcessWebSocketRequest(HttpContext ctx);
-    }
-}

+ 19 - 4
Emby.Server.Implementations/HttpServer/ResponseFilter.cs

@@ -1,6 +1,8 @@
 using System;
+using System.Collections.Generic;
 using System.Globalization;
 using System.Text;
+using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Services;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
@@ -13,14 +15,17 @@ namespace Emby.Server.Implementations.HttpServer
     /// </summary>
     public class ResponseFilter
     {
+        private readonly IHttpServer _server;
         private readonly ILogger _logger;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ResponseFilter"/> class.
         /// </summary>
+        /// <param name="server">The HTTP server.</param>
         /// <param name="logger">The logger.</param>
-        public ResponseFilter(ILogger logger)
+        public ResponseFilter(IHttpServer server, ILogger logger)
         {
+            _server = server;
             _logger = logger;
         }
 
@@ -32,10 +37,16 @@ namespace Emby.Server.Implementations.HttpServer
         /// <param name="dto">The dto.</param>
         public void FilterResponse(IRequest req, HttpResponse res, object dto)
         {
+            foreach(var (key, value) in _server.GetDefaultCorsHeaders(req))
+            {
+                res.Headers.Add(key, value);
+            }
             // Try to prevent compatibility view
-            res.Headers.Add("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Authorization");
-            res.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
-            res.Headers.Add("Access-Control-Allow-Origin", "*");
+            res.Headers["Access-Control-Allow-Headers"] = ("Accept, Accept-Language, Authorization, Cache-Control, " +
+                "Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, " +
+                "Content-Type, Cookie, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, " +
+                "Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, " +
+                "X-Emby-Authorization");
 
             if (dto is Exception exception)
             {
@@ -82,6 +93,10 @@ namespace Emby.Server.Implementations.HttpServer
             {
                 return null;
             }
+            else if (inString.Length == 0)
+            {
+                return inString;
+            }
 
             var newString = new StringBuilder(inString.Length);
 

+ 125 - 132
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs

@@ -1,15 +1,18 @@
-using System;
+#nullable enable
+
+using System;
+using System.Buffers;
+using System.IO.Pipelines;
+using System.Net;
 using System.Net.WebSockets;
-using System.Text;
+using System.Text.Json;
 using System.Threading;
 using System.Threading.Tasks;
-using Emby.Server.Implementations.Net;
+using MediaBrowser.Common.Json;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
-using UtfUnknown;
 
 namespace Emby.Server.Implementations.HttpServer
 {
@@ -24,69 +27,50 @@ namespace Emby.Server.Implementations.HttpServer
         private readonly ILogger _logger;
 
         /// <summary>
-        /// The json serializer.
+        /// The json serializer options.
         /// </summary>
-        private readonly IJsonSerializer _jsonSerializer;
+        private readonly JsonSerializerOptions _jsonOptions;
 
         /// <summary>
         /// The socket.
         /// </summary>
-        private readonly IWebSocket _socket;
+        private readonly WebSocket _socket;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="WebSocketConnection" /> class.
         /// </summary>
+        /// <param name="logger">The logger.</param>
         /// <param name="socket">The socket.</param>
         /// <param name="remoteEndPoint">The remote end point.</param>
-        /// <param name="jsonSerializer">The json serializer.</param>
-        /// <param name="logger">The logger.</param>
-        /// <exception cref="ArgumentNullException">socket</exception>
-        public WebSocketConnection(IWebSocket socket, string remoteEndPoint, IJsonSerializer jsonSerializer, ILogger logger)
+        /// <param name="query">The query.</param>
+        public WebSocketConnection(
+            ILogger<WebSocketConnection> logger,
+            WebSocket socket,
+            IPAddress? remoteEndPoint,
+            IQueryCollection query)
         {
-            if (socket == null)
-            {
-                throw new ArgumentNullException(nameof(socket));
-            }
-
-            if (string.IsNullOrEmpty(remoteEndPoint))
-            {
-                throw new ArgumentNullException(nameof(remoteEndPoint));
-            }
-
-            if (jsonSerializer == null)
-            {
-                throw new ArgumentNullException(nameof(jsonSerializer));
-            }
-
-            if (logger == null)
-            {
-                throw new ArgumentNullException(nameof(logger));
-            }
-
-            Id = Guid.NewGuid();
-            _jsonSerializer = jsonSerializer;
+            _logger = logger;
             _socket = socket;
-            _socket.OnReceiveBytes = OnReceiveInternal;
-
             RemoteEndPoint = remoteEndPoint;
-            _logger = logger;
+            QueryString = query;
 
-            socket.Closed += OnSocketClosed;
+            _jsonOptions = JsonDefaults.GetOptions();
+            LastActivityDate = DateTime.Now;
         }
 
         /// <inheritdoc />
-        public event EventHandler<EventArgs> Closed;
+        public event EventHandler<EventArgs>? Closed;
 
         /// <summary>
         /// Gets or sets the remote end point.
         /// </summary>
-        public string RemoteEndPoint { get; private set; }
+        public IPAddress? RemoteEndPoint { get; }
 
         /// <summary>
         /// Gets or sets the receive action.
         /// </summary>
         /// <value>The receive action.</value>
-        public Func<WebSocketMessageInfo, Task> OnReceive { get; set; }
+        public Func<WebSocketMessageInfo, Task>? OnReceive { get; set; }
 
         /// <summary>
         /// Gets the last activity date.
@@ -97,23 +81,11 @@ namespace Emby.Server.Implementations.HttpServer
         /// <inheritdoc />
         public DateTime LastKeepAliveDate { get; set; }
 
-        /// <summary>
-        /// Gets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        public Guid Id { get; private set; }
-
-        /// <summary>
-        /// Gets or sets the URL.
-        /// </summary>
-        /// <value>The URL.</value>
-        public string Url { get; set; }
-
         /// <summary>
         /// Gets or sets the query string.
         /// </summary>
         /// <value>The query string.</value>
-        public IQueryCollection QueryString { get; set; }
+        public IQueryCollection QueryString { get; }
 
         /// <summary>
         /// Gets the state.
@@ -121,121 +93,142 @@ namespace Emby.Server.Implementations.HttpServer
         /// <value>The state.</value>
         public WebSocketState State => _socket.State;
 
-        void OnSocketClosed(object sender, EventArgs e)
+        /// <summary>
+        /// Sends a message asynchronously.
+        /// </summary>
+        /// <typeparam name="T"></typeparam>
+        /// <param name="message">The message.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
         {
-            Closed?.Invoke(this, EventArgs.Empty);
+            var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
+            return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
         }
 
-        /// <summary>
-        /// Called when [receive].
-        /// </summary>
-        /// <param name="bytes">The bytes.</param>
-        private void OnReceiveInternal(byte[] bytes)
+        /// <inheritdoc />
+        public async Task ProcessAsync(CancellationToken cancellationToken = default)
         {
-            LastActivityDate = DateTime.UtcNow;
+            var pipe = new Pipe();
+            var writer = pipe.Writer;
 
-            if (OnReceive == null)
+            ValueWebSocketReceiveResult receiveresult;
+            do
             {
-                return;
-            }
-            var charset = CharsetDetector.DetectFromBytes(bytes).Detected?.EncodingName;
+                // Allocate at least 512 bytes from the PipeWriter
+                Memory<byte> memory = writer.GetMemory(512);
+                try
+                {
+                    receiveresult = await _socket.ReceiveAsync(memory, cancellationToken);
+                }
+                catch (WebSocketException ex)
+                {
+                    _logger.LogWarning("WS {IP} error receiving data: {Message}", RemoteEndPoint, ex.Message);
+                    break;
+                }
 
-            if (string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase))
-            {
-                OnReceiveInternal(Encoding.UTF8.GetString(bytes, 0, bytes.Length));
-            }
-            else
+                int bytesRead = receiveresult.Count;
+                if (bytesRead == 0)
+                {
+                    break;
+                }
+
+                // Tell the PipeWriter how much was read from the Socket
+                writer.Advance(bytesRead);
+
+                // Make the data available to the PipeReader
+                FlushResult flushResult = await writer.FlushAsync();
+                if (flushResult.IsCompleted)
+                {
+                    // The PipeReader stopped reading
+                    break;
+                }
+
+                LastActivityDate = DateTime.UtcNow;
+
+                if (receiveresult.EndOfMessage)
+                {
+                    await ProcessInternal(pipe.Reader).ConfigureAwait(false);
+                }
+            } while (
+                (_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
+                && receiveresult.MessageType != WebSocketMessageType.Close);
+
+            Closed?.Invoke(this, EventArgs.Empty);
+
+            if (_socket.State == WebSocketState.Open
+                || _socket.State == WebSocketState.CloseReceived
+                || _socket.State == WebSocketState.CloseSent)
             {
-                OnReceiveInternal(Encoding.ASCII.GetString(bytes, 0, bytes.Length));
+                await _socket.CloseAsync(
+                    WebSocketCloseStatus.NormalClosure,
+                    string.Empty,
+                    cancellationToken).ConfigureAwait(false);
             }
         }
 
-        private void OnReceiveInternal(string message)
+        private async Task ProcessInternal(PipeReader reader)
         {
-            LastActivityDate = DateTime.UtcNow;
+            ReadResult result = await reader.ReadAsync().ConfigureAwait(false);
+            ReadOnlySequence<byte> buffer = result.Buffer;
 
-            if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase))
+            if (OnReceive == null)
             {
-                // This info is useful sometimes but also clogs up the log
-                _logger.LogDebug("Received web socket message that is not a json structure: {message}", message);
+                // Tell the PipeReader how much of the buffer we have consumed
+                reader.AdvanceTo(buffer.End);
                 return;
             }
 
+            WebSocketMessage<object> stub;
             try
             {
-                var stub = (WebSocketMessage<object>)_jsonSerializer.DeserializeFromString(message, typeof(WebSocketMessage<object>));
-
-                var info = new WebSocketMessageInfo
-                {
-                    MessageType = stub.MessageType,
-                    Data = stub.Data?.ToString(),
-                    Connection = this
-                };
 
-                if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal))
+                if (buffer.IsSingleSegment)
                 {
-                    SendKeepAliveResponse();
+                    stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buffer.FirstSpan, _jsonOptions);
                 }
                 else
                 {
-                    OnReceive?.Invoke(info);
+                    var buf = ArrayPool<byte>.Shared.Rent(Convert.ToInt32(buffer.Length));
+                    try
+                    {
+                        buffer.CopyTo(buf);
+                        stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buf, _jsonOptions);
+                    }
+                    finally
+                    {
+                        ArrayPool<byte>.Shared.Return(buf);
+                    }
                 }
             }
-            catch (Exception ex)
+            catch (JsonException ex)
             {
+                // Tell the PipeReader how much of the buffer we have consumed
+                reader.AdvanceTo(buffer.End);
                 _logger.LogError(ex, "Error processing web socket message");
+                return;
             }
-        }
 
-        /// <summary>
-        /// Sends a message asynchronously.
-        /// </summary>
-        /// <typeparam name="T"></typeparam>
-        /// <param name="message">The message.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        /// <exception cref="ArgumentNullException">message</exception>
-        public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
-        {
-            if (message == null)
-            {
-                throw new ArgumentNullException(nameof(message));
-            }
+            // Tell the PipeReader how much of the buffer we have consumed
+            reader.AdvanceTo(buffer.End);
 
-            var json = _jsonSerializer.SerializeToString(message);
+            _logger.LogDebug("WS {IP} received message: {@Message}", RemoteEndPoint, stub);
 
-            return SendAsync(json, cancellationToken);
-        }
+            var info = new WebSocketMessageInfo
+            {
+                MessageType = stub.MessageType,
+                Data = stub.Data?.ToString(), // Data can be null
+                Connection = this
+            };
 
-        /// <summary>
-        /// Sends a message asynchronously.
-        /// </summary>
-        /// <param name="buffer">The buffer.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public Task SendAsync(byte[] buffer, CancellationToken cancellationToken)
-        {
-            if (buffer == null)
+            if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal))
             {
-                throw new ArgumentNullException(nameof(buffer));
+                SendKeepAliveResponse();
             }
-
-            cancellationToken.ThrowIfCancellationRequested();
-
-            return _socket.SendAsync(buffer, true, cancellationToken);
-        }
-
-        /// <inheritdoc />
-        public Task SendAsync(string text, CancellationToken cancellationToken)
-        {
-            if (string.IsNullOrEmpty(text))
+            else
             {
-                throw new ArgumentNullException(nameof(text));
+                await OnReceive(info).ConfigureAwait(false);
             }
-
-            cancellationToken.ThrowIfCancellationRequested();
-
-            return _socket.SendAsync(text, true, cancellationToken);
         }
 
         private void SendKeepAliveResponse()

+ 4 - 2
Emby.Server.Implementations/Library/MediaStreamSelector.cs

@@ -35,7 +35,8 @@ namespace Emby.Server.Implementations.Library
             return null;
         }
 
-        public static int? GetDefaultSubtitleStreamIndex(List<MediaStream> streams,
+        public static int? GetDefaultSubtitleStreamIndex(
+            List<MediaStream> streams,
             string[] preferredLanguages,
             SubtitlePlaybackMode mode,
             string audioTrackLanguage)
@@ -115,7 +116,8 @@ namespace Emby.Server.Implementations.Library
                  .ThenBy(i => i.Index);
         }
 
-        public static void SetSubtitleStreamScores(List<MediaStream> streams,
+        public static void SetSubtitleStreamScores(
+            List<MediaStream> streams,
             string[] preferredLanguages,
             SubtitlePlaybackMode mode,
             string audioTrackLanguage)

+ 12 - 10
Emby.Server.Implementations/Library/PathExtensions.cs

@@ -1,3 +1,5 @@
+#nullable enable
+
 using System;
 using System.Text.RegularExpressions;
 
@@ -12,24 +14,24 @@ namespace Emby.Server.Implementations.Library
         /// Gets the attribute value.
         /// </summary>
         /// <param name="str">The STR.</param>
-        /// <param name="attrib">The attrib.</param>
+        /// <param name="attribute">The attrib.</param>
         /// <returns>System.String.</returns>
-        /// <exception cref="ArgumentNullException">attrib</exception>
-        public static string GetAttributeValue(this string str, string attrib)
+        /// <exception cref="ArgumentException"><paramref name="str" /> or <paramref name="attribute" /> is empty.</exception>
+        public static string? GetAttributeValue(this string str, string attribute)
         {
-            if (string.IsNullOrEmpty(str))
+            if (str.Length == 0)
             {
-                throw new ArgumentNullException(nameof(str));
+                throw new ArgumentException("String can't be empty.", nameof(str));
             }
 
-            if (string.IsNullOrEmpty(attrib))
+            if (attribute.Length == 0)
             {
-                throw new ArgumentNullException(nameof(attrib));
+                throw new ArgumentException("String can't be empty.", nameof(attribute));
             }
 
-            string srch = "[" + attrib + "=";
+            string srch = "[" + attribute + "=";
             int start = str.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
-            if (start > -1)
+            if (start != -1)
             {
                 start += srch.Length;
                 int end = str.IndexOf(']', start);
@@ -37,7 +39,7 @@ namespace Emby.Server.Implementations.Library
             }
 
             // for imdbid we also accept pattern matching
-            if (string.Equals(attrib, "imdbid", StringComparison.OrdinalIgnoreCase))
+            if (string.Equals(attribute, "imdbid", StringComparison.OrdinalIgnoreCase))
             {
                 var m = Regex.Match(str, "tt([0-9]{7,8})", RegexOptions.IgnoreCase);
                 return m.Success ? m.Value : null;

+ 2 - 0
Emby.Server.Implementations/Library/ResolverHelper.cs

@@ -118,10 +118,12 @@ namespace Emby.Server.Implementations.Library
             {
                 throw new ArgumentNullException(nameof(fileSystem));
             }
+
             if (item == null)
             {
                 throw new ArgumentNullException(nameof(item));
             }
+
             if (args == null)
             {
                 throw new ArgumentNullException(nameof(args));

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

@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
     /// </summary>
     public class MusicAlbumResolver : ItemResolver<MusicAlbum>
     {
-        private readonly ILogger _logger;
+        private readonly ILogger<MusicAlbumResolver> _logger;
         private readonly IFileSystem _fileSystem;
         private readonly ILibraryManager _libraryManager;
 
@@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
         /// <param name="logger">The logger.</param>
         /// <param name="fileSystem">The file system.</param>
         /// <param name="libraryManager">The library manager.</param>
-        public MusicAlbumResolver(ILogger logger, IFileSystem fileSystem, ILibraryManager libraryManager)
+        public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, IFileSystem fileSystem, ILibraryManager libraryManager)
         {
             _logger = logger;
             _fileSystem = fileSystem;

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

@@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
     /// </summary>
     public class MusicArtistResolver : ItemResolver<MusicArtist>
     {
-        private readonly ILogger _logger;
+        private readonly ILogger<MusicAlbumResolver> _logger;
         private readonly IFileSystem _fileSystem;
         private readonly ILibraryManager _libraryManager;
         private readonly IServerConfigurationManager _config;
@@ -23,12 +23,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
         /// <summary>
         /// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
         /// </summary>
-        /// <param name="logger">The logger.</param>
+        /// <param name="logger">The logger for the created <see cref="MusicAlbumResolver"/> instances.</param>
         /// <param name="fileSystem">The file system.</param>
         /// <param name="libraryManager">The library manager.</param>
         /// <param name="config">The configuration manager.</param>
         public MusicArtistResolver(
-            ILogger<MusicArtistResolver> logger,
+            ILogger<MusicAlbumResolver> logger,
             IFileSystem fileSystem,
             ILibraryManager libraryManager,
             IServerConfigurationManager config)

+ 25 - 0
Emby.Server.Implementations/Library/UserManager.cs

@@ -608,6 +608,31 @@ namespace Emby.Server.Implementations.Library
             return dto;
         }
 
+        public PublicUserDto GetPublicUserDto(User user, string remoteEndPoint = null)
+        {
+            if (user == null)
+            {
+                throw new ArgumentNullException(nameof(user));
+            }
+
+            IAuthenticationProvider authenticationProvider = GetAuthenticationProvider(user);
+            bool hasConfiguredPassword = authenticationProvider.HasPassword(user);
+            bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(authenticationProvider.GetEasyPasswordHash(user));
+
+            bool hasPassword = user.Configuration.EnableLocalPassword &&
+                !string.IsNullOrEmpty(remoteEndPoint) &&
+                _networkManager.IsInLocalNetwork(remoteEndPoint) ? hasConfiguredEasyPassword : hasConfiguredPassword;
+
+            PublicUserDto dto = new PublicUserDto
+            {
+                Name = user.Name,
+                HasPassword = hasPassword,
+                HasConfiguredPassword = hasConfiguredPassword,
+            };
+
+            return dto;
+        }
+
         public UserDto GetOfflineUserDto(User user)
         {
             var dto = GetUserDto(user);

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

@@ -1059,7 +1059,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         {
             var stream = new MediaSourceInfo
             {
-                EncoderPath = _appHost.GetLocalApiUrl("127.0.0.1", true) + "/LiveTv/LiveRecordings/" + info.Id + "/stream",
+                EncoderPath = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveRecordings/" + info.Id + "/stream",
                 EncoderProtocol = MediaProtocol.Http,
                 Path = info.Path,
                 Protocol = MediaProtocol.File,

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

@@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             //OpenedMediaSource.Path = tempFile;
             //OpenedMediaSource.ReadAtNativeFramerate = true;
 
-            MediaSource.Path = _appHost.GetLocalApiUrl("127.0.0.1", true) + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
+            MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
             MediaSource.Protocol = MediaProtocol.Http;
             //OpenedMediaSource.SupportsDirectPlay = false;
             //OpenedMediaSource.SupportsDirectStream = true;

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

@@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
         public M3UTunerHost(
             IServerConfigurationManager config,
             IMediaSourceManager mediaSourceManager,
-            ILogger logger,
+            ILogger<M3UTunerHost> logger,
             IJsonSerializer jsonSerializer,
             IFileSystem fileSystem,
             IHttpClient httpClient,
@@ -83,7 +83,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             return Task.FromResult(list);
         }
 
-        private static readonly string[] _disallowedSharedStreamExtensions = new string[]
+        private static readonly string[] _disallowedSharedStreamExtensions =
         {
             ".mkv",
             ".mp4",

+ 1 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs

@@ -106,7 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             //OpenedMediaSource.Path = tempFile;
             //OpenedMediaSource.ReadAtNativeFramerate = true;
 
-            MediaSource.Path = _appHost.GetLocalApiUrl("127.0.0.1", true) + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
+            MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
             MediaSource.Protocol = MediaProtocol.Http;
 
             //OpenedMediaSource.Path = TempFilePath;

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

@@ -4,7 +4,7 @@
     "Folders": "Fouers",
     "Favorites": "Gunstelinge",
     "HeaderFavoriteShows": "Gunsteling Vertonings",
-    "ValueSpecialEpisodeName": "Spesiaal - {0}",
+    "ValueSpecialEpisodeName": "Spesiale - {0}",
     "HeaderAlbumArtists": "Album Kunstenaars",
     "Books": "Boeke",
     "HeaderNextUp": "Volgende",

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

@@ -91,5 +91,7 @@
     "HeaderNextUp": "এরপরে আসছে",
     "HeaderLiveTV": "লাইভ টিভি",
     "HeaderFavoriteSongs": "প্রিয় গানগুলো",
-    "HeaderFavoriteShows": "প্রিয় শোগুলো"
+    "HeaderFavoriteShows": "প্রিয় শোগুলো",
+    "TasksLibraryCategory": "গ্রন্থাগার",
+    "TasksMaintenanceCategory": "রক্ষণাবেক্ষণ"
 }

+ 1 - 1
Emby.Server.Implementations/Localization/Core/es-MX.json

@@ -11,7 +11,7 @@
     "Collections": "Colecciones",
     "DeviceOfflineWithName": "{0} se ha desconectado",
     "DeviceOnlineWithName": "{0} está conectado",
-    "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión de {0}",
+    "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}",
     "Favorites": "Favoritos",
     "Folders": "Carpetas",
     "Genres": "Géneros",

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

@@ -71,7 +71,7 @@
     "ScheduledTaskFailedWithName": "{0} falló",
     "ScheduledTaskStartedWithName": "{0} iniciada",
     "ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
-    "Shows": "Series",
+    "Shows": "Mostrar",
     "Songs": "Canciones",
     "StartupEmbyServerIsLoading": "Jellyfin Server se está cargando. Vuelve a intentarlo en breve.",
     "SubtitleDownloadFailureForItem": "Error al descargar subtítulos para {0}",

+ 13 - 13
Emby.Server.Implementations/Localization/Core/fi.json

@@ -1,5 +1,5 @@
 {
-    "HeaderLiveTV": "Suorat lähetykset",
+    "HeaderLiveTV": "Live-TV",
     "NewVersionIsAvailable": "Uusi versio Jellyfin palvelimesta on ladattavissa.",
     "NameSeasonUnknown": "Tuntematon Kausi",
     "NameSeasonNumber": "Kausi {0}",
@@ -12,7 +12,7 @@
     "MessageNamedServerConfigurationUpdatedWithValue": "Palvelimen asetusryhmä {0} on päivitetty",
     "MessageApplicationUpdatedTo": "Jellyfin palvelin on päivitetty versioon {0}",
     "MessageApplicationUpdated": "Jellyfin palvelin on päivitetty",
-    "Latest": "Viimeisin",
+    "Latest": "Uusimmat",
     "LabelRunningTimeValue": "Toiston kesto: {0}",
     "LabelIpAddressValue": "IP-osoite: {0}",
     "ItemRemovedWithName": "{0} poistettiin kirjastosta",
@@ -41,7 +41,7 @@
     "CameraImageUploadedFrom": "Uusi kamerakuva on ladattu {0}",
     "Books": "Kirjat",
     "AuthenticationSucceededWithUserName": "{0} todennus onnistui",
-    "Artists": "Esiintyjät",
+    "Artists": "Artistit",
     "Application": "Sovellus",
     "AppDeviceValues": "Sovellus: {0}, Laite: {1}",
     "Albums": "Albumit",
@@ -67,21 +67,21 @@
     "UserDownloadingItemWithValues": "{0} lataa {1}",
     "UserDeletedWithName": "Käyttäjä {0} poistettu",
     "UserCreatedWithName": "Käyttäjä {0} luotu",
-    "TvShows": "TV-Ohjelmat",
+    "TvShows": "TV-sarjat",
     "Sync": "Synkronoi",
-    "SubtitleDownloadFailureFromForItem": "Tekstityksen lataaminen epäonnistui {0} - {1}",
+    "SubtitleDownloadFailureFromForItem": "Tekstitysten lataus ({0} -> {1}) epäonnistui //this string would have to be generated for each provider and movie because of finnish cases, sorry",
     "StartupEmbyServerIsLoading": "Jellyfin palvelin latautuu. Kokeile hetken kuluttua uudelleen.",
     "Songs": "Kappaleet",
-    "Shows": "Ohjelmat",
-    "ServerNameNeedsToBeRestarted": "{0} vaatii uudelleenkäynnistyksen",
+    "Shows": "Sarjat",
+    "ServerNameNeedsToBeRestarted": "{0} täytyy käynnistää uudelleen",
     "ProviderValue": "Tarjoaja: {0}",
     "Plugin": "Liitännäinen",
     "NotificationOptionVideoPlaybackStopped": "Videon toisto pysäytetty",
-    "NotificationOptionVideoPlayback": "Videon toisto aloitettu",
-    "NotificationOptionUserLockedOut": "Käyttäjä lukittu",
+    "NotificationOptionVideoPlayback": "Videota toistetaan",
+    "NotificationOptionUserLockedOut": "Käyttäjä kirjautui ulos",
     "NotificationOptionTaskFailed": "Ajastettu tehtävä epäonnistui",
-    "NotificationOptionServerRestartRequired": "Palvelimen uudelleenkäynnistys vaaditaan",
-    "NotificationOptionPluginUpdateInstalled": "Lisäosan päivitys asennettu",
+    "NotificationOptionServerRestartRequired": "Palvelin pitää käynnistää uudelleen",
+    "NotificationOptionPluginUpdateInstalled": "Liitännäinen päivitetty",
     "NotificationOptionPluginUninstalled": "Liitännäinen poistettu",
     "NotificationOptionPluginInstalled": "Liitännäinen asennettu",
     "NotificationOptionPluginError": "Ongelma liitännäisessä",
@@ -90,8 +90,8 @@
     "NotificationOptionCameraImageUploaded": "Kameran kuva ladattu",
     "NotificationOptionAudioPlaybackStopped": "Äänen toisto lopetettu",
     "NotificationOptionAudioPlayback": "Toistetaan ääntä",
-    "NotificationOptionApplicationUpdateInstalled": "Uusi sovellusversio asennettu",
-    "NotificationOptionApplicationUpdateAvailable": "Sovelluksesta on uusi versio saatavilla",
+    "NotificationOptionApplicationUpdateInstalled": "Sovelluspäivitys asennettu",
+    "NotificationOptionApplicationUpdateAvailable": "Ohjelmistopäivitys saatavilla",
     "TasksMaintenanceCategory": "Ylläpito",
     "TaskDownloadMissingSubtitlesDescription": "Etsii puuttuvia tekstityksiä videon metadatatietojen pohjalta.",
     "TaskDownloadMissingSubtitles": "Lataa puuttuvat tekstitykset",

+ 19 - 1
Emby.Server.Implementations/Localization/Core/fr-CA.json

@@ -94,5 +94,23 @@
     "ValueSpecialEpisodeName": "Spécial - {0}",
     "VersionNumber": "Version {0}",
     "TasksLibraryCategory": "Bibliothèque",
-    "TasksMaintenanceCategory": "Entretien"
+    "TasksMaintenanceCategory": "Entretien",
+    "TaskDownloadMissingSubtitlesDescription": "Recherche l'internet pour des sous-titres manquants à base de métadonnées configurées.",
+    "TaskDownloadMissingSubtitles": "Télécharger des sous-titres manquants",
+    "TaskRefreshChannelsDescription": "Rafraîchit des informations des chaines d'internet.",
+    "TaskRefreshChannels": "Rafraîchir des chaines",
+    "TaskCleanTranscodeDescription": "Retirer des fichiers de transcodage de plus qu'un jour.",
+    "TaskCleanTranscode": "Nettoyer le directoire de transcodage",
+    "TaskUpdatePluginsDescription": "Télécharger et installer des mises à jours des plugins qui sont configurés m.à.j. automisés.",
+    "TaskUpdatePlugins": "Mise à jour des plugins",
+    "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre bibliothèque.",
+    "TaskRefreshPeople": "Rafraîchir les acteurs",
+    "TaskCleanLogsDescription": "Retire les données qui ont plus que {0} jours.",
+    "TaskCleanLogs": "Nettoyer les données de directoire",
+    "TaskRefreshLibraryDescription": "Analyse votre bibliothèque média pour des nouveaux fichiers et rafraîchit les métadonnées.",
+    "TaskRefreshChapterImages": "Extraire des images du chapitre",
+    "TaskRefreshChapterImagesDescription": "Créer des vignettes pour des vidéos qui ont des chapitres",
+    "TaskRefreshLibrary": "Analyser la bibliothèque de média",
+    "TaskCleanCache": "Nettoyer le cache de directoire",
+    "TasksApplicationCategory": "Application"
 }

+ 57 - 35
Emby.Server.Implementations/Localization/Core/gsw.json

@@ -1,41 +1,41 @@
 {
-    "Albums": "Albom",
-    "AppDeviceValues": "App: {0}, Grät: {1}",
-    "Application": "Aawändig",
-    "Artists": "Könstler",
-    "AuthenticationSucceededWithUserName": "{0} het sech aagmäudet",
-    "Books": "Büecher",
-    "CameraImageUploadedFrom": "Es nöis Foti esch ufeglade worde vo {0}",
-    "Channels": "Kanäu",
-    "ChapterNameValue": "Kapitu {0}",
-    "Collections": "Sammlige",
-    "DeviceOfflineWithName": "{0} esch offline gange",
-    "DeviceOnlineWithName": "{0} esch online cho",
-    "FailedLoginAttemptWithUserName": "Fäugschlagne Aamäudeversuech vo {0}",
-    "Favorites": "Favorite",
+    "Albums": "Alben",
+    "AppDeviceValues": "App: {0}, Gerät: {1}",
+    "Application": "Anwendung",
+    "Artists": "Künstler",
+    "AuthenticationSucceededWithUserName": "{0} hat sich angemeldet",
+    "Books": "Bücher",
+    "CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen",
+    "Channels": "Kanäle",
+    "ChapterNameValue": "Kapitel {0}",
+    "Collections": "Sammlungen",
+    "DeviceOfflineWithName": "{0} wurde getrennt",
+    "DeviceOnlineWithName": "{0} ist verbunden",
+    "FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}",
+    "Favorites": "Favoriten",
     "Folders": "Ordner",
     "Genres": "Genres",
-    "HeaderAlbumArtists": "Albom-Könstler",
+    "HeaderAlbumArtists": "Album-Künstler",
     "HeaderCameraUploads": "Kamera-Uploads",
-    "HeaderContinueWatching": "Wiiterluege",
-    "HeaderFavoriteAlbums": "Lieblingsalbe",
-    "HeaderFavoriteArtists": "Lieblings-Interprete",
-    "HeaderFavoriteEpisodes": "Lieblingsepisode",
-    "HeaderFavoriteShows": "Lieblingsserie",
+    "HeaderContinueWatching": "weiter schauen",
+    "HeaderFavoriteAlbums": "Lieblingsalben",
+    "HeaderFavoriteArtists": "Lieblings-Künstler",
+    "HeaderFavoriteEpisodes": "Lieblingsepisoden",
+    "HeaderFavoriteShows": "Lieblingsserien",
     "HeaderFavoriteSongs": "Lieblingslieder",
-    "HeaderLiveTV": "Live-Färnseh",
-    "HeaderNextUp": "Als nächts",
-    "HeaderRecordingGroups": "Ufnahmegruppe",
-    "HomeVideos": "Heimfilmli",
-    "Inherit": "Hinzuefüege",
-    "ItemAddedWithName": "{0} esch de Bibliothek dezuegfüegt worde",
-    "ItemRemovedWithName": "{0} esch vo de Bibliothek entfärnt worde",
-    "LabelIpAddressValue": "IP-Adrässe: {0}",
-    "LabelRunningTimeValue": "Loufziit: {0}",
-    "Latest": "Nöischti",
-    "MessageApplicationUpdated": "Jellyfin Server esch aktualisiert worde",
-    "MessageApplicationUpdatedTo": "Jellyfin Server esch of Version {0} aktualisiert worde",
-    "MessageNamedServerConfigurationUpdatedWithValue": "De Serveriistöuigsberiich {0} esch aktualisiert worde",
+    "HeaderLiveTV": "Live-Fernseh",
+    "HeaderNextUp": "Als Nächstes",
+    "HeaderRecordingGroups": "Aufnahme-Gruppen",
+    "HomeVideos": "Heimvideos",
+    "Inherit": "Vererben",
+    "ItemAddedWithName": "{0} wurde der Bibliothek hinzugefügt",
+    "ItemRemovedWithName": "{0} wurde aus der Bibliothek entfernt",
+    "LabelIpAddressValue": "IP-Adresse: {0}",
+    "LabelRunningTimeValue": "Laufzeit: {0}",
+    "Latest": "Neueste",
+    "MessageApplicationUpdated": "Jellyfin-Server wurde aktualisiert",
+    "MessageApplicationUpdatedTo": "Jellyfin-Server wurde auf Version {0} aktualisiert",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Der Server-Einstellungsbereich {0} wurde aktualisiert",
     "MessageServerConfigurationUpdated": "Serveriistöuige send aktualisiert worde",
     "MixedContent": "Gmeschti Inhäut",
     "Movies": "Film",
@@ -50,7 +50,7 @@
     "NotificationOptionAudioPlayback": "Audiowedergab gstartet",
     "NotificationOptionAudioPlaybackStopped": "Audiwedergab gstoppt",
     "NotificationOptionCameraImageUploaded": "Foti ueglade",
-    "NotificationOptionInstallationFailed": "Installationsfäuer",
+    "NotificationOptionInstallationFailed": "Installationsfehler",
     "NotificationOptionNewLibraryContent": "Nöie Inhaut hinzuegfüegt",
     "NotificationOptionPluginError": "Plugin-Fäuer",
     "NotificationOptionPluginInstalled": "Plugin installiert",
@@ -92,5 +92,27 @@
     "UserStoppedPlayingItemWithValues": "{0} het d'Wedergab vo {1} of {2} gstoppt",
     "ValueHasBeenAddedToLibrary": "{0} esch dinnere Biblithek hinzuegfüegt worde",
     "ValueSpecialEpisodeName": "Extra - {0}",
-    "VersionNumber": "Version {0}"
+    "VersionNumber": "Version {0}",
+    "TaskCleanLogs": "Lösche Log Pfad",
+    "TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",
+    "TaskRefreshLibrary": "Scanne alle Bibliotheken",
+    "TaskRefreshChapterImagesDescription": "Kreiert Vorschaubilder für Videos welche Kapitel haben.",
+    "TaskRefreshChapterImages": "Extrahiere Kapitel-Bilder",
+    "TaskCleanCacheDescription": "Löscht Zwischenspeicherdatein die nicht länger von System gebraucht werden.",
+    "TaskCleanCache": "Leere Cache Pfad",
+    "TasksChannelsCategory": "Internet Kanäle",
+    "TasksApplicationCategory": "Applikation",
+    "TasksLibraryCategory": "Bibliothek",
+    "TasksMaintenanceCategory": "Verwaltung",
+    "TaskDownloadMissingSubtitlesDescription": "Durchsucht das Internet nach fehlenden Untertiteln, basierend auf den Metadaten Einstellungen.",
+    "TaskDownloadMissingSubtitles": "Lade fehlende Untertitel herunter",
+    "TaskRefreshChannelsDescription": "Aktualisiert Internet Kanal Informationen.",
+    "TaskRefreshChannels": "Aktualisiere Kanäle",
+    "TaskCleanTranscodeDescription": "Löscht Transkodierdateien welche älter als ein Tag sind.",
+    "TaskCleanTranscode": "Räume Transcodier Verzeichnis auf",
+    "TaskUpdatePluginsDescription": "Lädt Aktualisierungen für Erweiterungen herunter und installiert diese, für welche automatische Aktualisierungen konfiguriert sind.",
+    "TaskUpdatePlugins": "Aktualisiere Erweiterungen",
+    "TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schausteller und Regisseure in deiner Bibliothek.",
+    "TaskRefreshPeople": "Aktualisiere Schauspieler",
+    "TaskCleanLogsDescription": "Löscht Log Dateien die älter als {0} Tage sind."
 }

+ 10 - 2
Emby.Server.Implementations/Localization/Core/he.json

@@ -62,7 +62,7 @@
     "NotificationOptionVideoPlayback": "Video playback started",
     "NotificationOptionVideoPlaybackStopped": "Video playback stopped",
     "Photos": "תמונות",
-    "Playlists": "רשימות ניגון",
+    "Playlists": "רשימות הפעלה",
     "Plugin": "Plugin",
     "PluginInstalledWithName": "{0} was installed",
     "PluginUninstalledWithName": "{0} was uninstalled",
@@ -99,5 +99,13 @@
     "TaskCleanCache": "נקה תיקיית מטמון",
     "TasksApplicationCategory": "יישום",
     "TasksLibraryCategory": "ספרייה",
-    "TasksMaintenanceCategory": "תחזוקה"
+    "TasksMaintenanceCategory": "תחזוקה",
+    "TaskUpdatePlugins": "עדכן תוספים",
+    "TaskRefreshPeopleDescription": "מעדכן מטא נתונים עבור שחקנים ובמאים בספריית המדיה שלך.",
+    "TaskRefreshPeople": "רענן אנשים",
+    "TaskCleanLogsDescription": "מוחק קבצי יומן בני יותר מ- {0} ימים.",
+    "TaskCleanLogs": "נקה תיקיית יומן",
+    "TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.",
+    "TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
+    "TasksChannelsCategory": "ערוצי אינטרנט"
 }

+ 10 - 2
Emby.Server.Implementations/Localization/Core/hr.json

@@ -30,7 +30,7 @@
     "Inherit": "Naslijedi",
     "ItemAddedWithName": "{0} je dodano u biblioteku",
     "ItemRemovedWithName": "{0} je uklonjen iz biblioteke",
-    "LabelIpAddressValue": "Ip adresa: {0}",
+    "LabelIpAddressValue": "IP adresa: {0}",
     "LabelRunningTimeValue": "Vrijeme rada: {0}",
     "Latest": "Najnovije",
     "MessageApplicationUpdated": "Jellyfin Server je ažuriran",
@@ -92,5 +92,13 @@
     "UserStoppedPlayingItemWithValues": "{0} je zaustavio {1}",
     "ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
     "ValueSpecialEpisodeName": "Specijal - {0}",
-    "VersionNumber": "Verzija {0}"
+    "VersionNumber": "Verzija {0}",
+    "TaskRefreshLibraryDescription": "Skenira vašu medijsku knjižnicu sa novim datotekama i osvježuje metapodatke.",
+    "TaskRefreshLibrary": "Skeniraj medijsku knjižnicu",
+    "TaskRefreshChapterImagesDescription": "Stvara sličice za videozapise koji imaju poglavlja.",
+    "TaskRefreshChapterImages": "Raspakiraj slike poglavlja",
+    "TaskCleanCacheDescription": "Briše priručne datoteke nepotrebne za sistem.",
+    "TaskCleanCache": "Očisti priručnu memoriju",
+    "TasksApplicationCategory": "Aplikacija",
+    "TasksMaintenanceCategory": "Održavanje"
 }

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

@@ -5,7 +5,7 @@
     "Artists": "Artisti",
     "AuthenticationSucceededWithUserName": "{0} autenticato con successo",
     "Books": "Libri",
-    "CameraImageUploadedFrom": "È stata caricata una nuova immagine della fotocamera dal device {0}",
+    "CameraImageUploadedFrom": "È stata caricata una nuova fotografia da {0}",
     "Channels": "Canali",
     "ChapterNameValue": "Capitolo {0}",
     "Collections": "Collezioni",

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

@@ -91,5 +91,12 @@
     "Songs": "Песни",
     "Shows": "Серии",
     "ServerNameNeedsToBeRestarted": "{0} треба да се рестартира",
-    "ScheduledTaskStartedWithName": "{0} започна"
+    "ScheduledTaskStartedWithName": "{0} започна",
+    "TaskRefreshChapterImages": "Извези Слики од Поглавје",
+    "TaskCleanCacheDescription": "Ги брише кешираните фајлови што не се повеќе потребни од системот.",
+    "TaskCleanCache": "Исчисти Го Кешот",
+    "TasksChannelsCategory": "Интернет Канали",
+    "TasksApplicationCategory": "Апликација",
+    "TasksLibraryCategory": "Библиотека",
+    "TasksMaintenanceCategory": "Одржување"
 }

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

@@ -97,5 +97,9 @@
     "TasksApplicationCategory": "Applikasjon",
     "TasksLibraryCategory": "Bibliotek",
     "TasksMaintenanceCategory": "Vedlikehold",
-    "TaskCleanCache": "Tøm buffer katalog"
+    "TaskCleanCache": "Tøm buffer katalog",
+    "TaskRefreshLibrary": "Skann mediebibliotek",
+    "TaskRefreshChapterImagesDescription": "Lager forhåndsvisningsbilder for videoer som har kapitler.",
+    "TaskRefreshChapterImages": "Trekk ut Kapittelbilder",
+    "TaskCleanCacheDescription": "Sletter mellomlagrede filer som ikke lengre trengs av systemet."
 }

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

@@ -5,7 +5,7 @@
     "Artists": "Artiesten",
     "AuthenticationSucceededWithUserName": "{0} is succesvol geverifieerd",
     "Books": "Boeken",
-    "CameraImageUploadedFrom": "Er is een nieuwe afbeelding toegevoegd via {0}",
+    "CameraImageUploadedFrom": "Er is een nieuwe camera afbeelding toegevoegd via {0}",
     "Channels": "Kanalen",
     "ChapterNameValue": "Hoofdstuk {0}",
     "Collections": "Verzamelingen",
@@ -26,7 +26,7 @@
     "HeaderLiveTV": "Live TV",
     "HeaderNextUp": "Volgende",
     "HeaderRecordingGroups": "Opnamegroepen",
-    "HomeVideos": "Start video's",
+    "HomeVideos": "Home video's",
     "Inherit": "Overerven",
     "ItemAddedWithName": "{0} is toegevoegd aan de bibliotheek",
     "ItemRemovedWithName": "{0} is verwijderd uit de bibliotheek",
@@ -50,7 +50,7 @@
     "NotificationOptionAudioPlayback": "Muziek gestart",
     "NotificationOptionAudioPlaybackStopped": "Muziek gestopt",
     "NotificationOptionCameraImageUploaded": "Camera-afbeelding geüpload",
-    "NotificationOptionInstallationFailed": "Installatie mislukking",
+    "NotificationOptionInstallationFailed": "Installatie mislukt",
     "NotificationOptionNewLibraryContent": "Nieuwe content toegevoegd",
     "NotificationOptionPluginError": "Plug-in fout",
     "NotificationOptionPluginInstalled": "Plug-in geïnstalleerd",

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

@@ -92,5 +92,26 @@
     "UserStoppedPlayingItemWithValues": "{0} je nehal predvajati {1} na {2}",
     "ValueHasBeenAddedToLibrary": "{0} je bil dodan vaši knjižnici",
     "ValueSpecialEpisodeName": "Poseben - {0}",
-    "VersionNumber": "Različica {0}"
+    "VersionNumber": "Različica {0}",
+    "TaskDownloadMissingSubtitles": "Prenesi manjkajoče podnapise",
+    "TaskRefreshChannelsDescription": "Osveži podatke spletnih kanalov.",
+    "TaskRefreshChannels": "Osveži kanale",
+    "TaskCleanTranscodeDescription": "Izbriše več kot dan stare datoteke prekodiranja.",
+    "TaskCleanTranscode": "Počisti mapo prekodiranja",
+    "TaskUpdatePluginsDescription": "Prenese in namesti posodobitve za dodatke, ki imajo omogočene samodejne posodobitve.",
+    "TaskUpdatePlugins": "Posodobi dodatke",
+    "TaskRefreshPeopleDescription": "Osveži metapodatke za igralce in režiserje v vaši knjižnici.",
+    "TaskRefreshPeople": "Osveži osebe",
+    "TaskCleanLogsDescription": "Izbriše dnevniške datoteke starejše od {0} dni.",
+    "TaskCleanLogs": "Počisti mapo dnevnika",
+    "TaskRefreshLibraryDescription": "Preišče vašo knjižnico za nove datoteke in osveži metapodatke.",
+    "TaskRefreshLibrary": "Preišči knjižnico predstavnosti",
+    "TaskRefreshChapterImagesDescription": "Ustvari sličice za poglavja videoposnetkov.",
+    "TaskRefreshChapterImages": "Izvleči slike poglavij",
+    "TaskCleanCacheDescription": "Izbriše predpomnjene datoteke, ki niso več potrebne.",
+    "TaskCleanCache": "Počisti mapo predpomnilnika",
+    "TasksChannelsCategory": "Spletni kanali",
+    "TasksApplicationCategory": "Aplikacija",
+    "TasksLibraryCategory": "Knjižnica",
+    "TasksMaintenanceCategory": "Vzdrževanje"
 }

+ 4 - 3
Emby.Server.Implementations/Localization/Core/sv.json

@@ -9,7 +9,7 @@
     "Channels": "Kanaler",
     "ChapterNameValue": "Kapitel {0}",
     "Collections": "Samlingar",
-    "DeviceOfflineWithName": "{0} har tappat anslutningen",
+    "DeviceOfflineWithName": "{0} har kopplat från",
     "DeviceOnlineWithName": "{0} är ansluten",
     "FailedLoginAttemptWithUserName": "Misslyckat inloggningsförsök från {0}",
     "Favorites": "Favoriter",
@@ -50,7 +50,7 @@
     "NotificationOptionAudioPlayback": "Ljuduppspelning har påbörjats",
     "NotificationOptionAudioPlaybackStopped": "Ljuduppspelning stoppades",
     "NotificationOptionCameraImageUploaded": "Kamerabild har laddats upp",
-    "NotificationOptionInstallationFailed": "Fel vid installation",
+    "NotificationOptionInstallationFailed": "Installationen misslyckades",
     "NotificationOptionNewLibraryContent": "Nytt innehåll har lagts till",
     "NotificationOptionPluginError": "Fel uppstod med tillägget",
     "NotificationOptionPluginInstalled": "Tillägg har installerats",
@@ -113,5 +113,6 @@
     "TasksChannelsCategory": "Internetkanaler",
     "TasksApplicationCategory": "Applikation",
     "TasksLibraryCategory": "Bibliotek",
-    "TasksMaintenanceCategory": "Underhåll"
+    "TasksMaintenanceCategory": "Underhåll",
+    "TaskRefreshPeople": "Uppdatera Personer"
 }

+ 36 - 0
Emby.Server.Implementations/Localization/Core/uk.json

@@ -0,0 +1,36 @@
+{
+    "MusicVideos": "Музичні відео",
+    "Music": "Музика",
+    "Movies": "Фільми",
+    "MessageApplicationUpdatedTo": "Jellyfin Server був оновлений до версії {0}",
+    "MessageApplicationUpdated": "Jellyfin Server був оновлений",
+    "Latest": "Останні",
+    "LabelIpAddressValue": "IP-адреси: {0}",
+    "ItemRemovedWithName": "{0} видалено з бібліотеки",
+    "ItemAddedWithName": "{0} додано до бібліотеки",
+    "HeaderNextUp": "Наступний",
+    "HeaderLiveTV": "Ефірне ТБ",
+    "HeaderFavoriteSongs": "Улюблені пісні",
+    "HeaderFavoriteShows": "Улюблені шоу",
+    "HeaderFavoriteEpisodes": "Улюблені серії",
+    "HeaderFavoriteArtists": "Улюблені виконавці",
+    "HeaderFavoriteAlbums": "Улюблені альбоми",
+    "HeaderContinueWatching": "Продовжити перегляд",
+    "HeaderCameraUploads": "Завантажено з камери",
+    "HeaderAlbumArtists": "Виконавці альбомів",
+    "Genres": "Жанри",
+    "Folders": "Директорії",
+    "Favorites": "Улюблені",
+    "DeviceOnlineWithName": "{0} під'єднано",
+    "DeviceOfflineWithName": "{0} від'єднано",
+    "Collections": "Колекції",
+    "ChapterNameValue": "Глава {0}",
+    "Channels": "Канали",
+    "CameraImageUploadedFrom": "Нова фотографія завантажена з {0}",
+    "Books": "Книги",
+    "AuthenticationSucceededWithUserName": "{0} успішно авторизовані",
+    "Artists": "Виконавці",
+    "Application": "Додаток",
+    "AppDeviceValues": "Додаток: {0}, Пристрій: {1}",
+    "Albums": "Альбоми"
+}

+ 5 - 2
Emby.Server.Implementations/Localization/Core/zh-HK.json

@@ -1,6 +1,6 @@
 {
     "Albums": "專輯",
-    "AppDeviceValues": "軟: {0}, 設備: {1}",
+    "AppDeviceValues": "軟: {0}, 設備: {1}",
     "Application": "應用程式",
     "Artists": "藝人",
     "AuthenticationSucceededWithUserName": "{0} 授權成功",
@@ -92,5 +92,8 @@
     "UserStoppedPlayingItemWithValues": "{0} 已在 {2} 上停止播放 {1}",
     "ValueHasBeenAddedToLibrary": "{0} 已添加到你的媒體庫",
     "ValueSpecialEpisodeName": "特典 - {0}",
-    "VersionNumber": "版本{0}"
+    "VersionNumber": "版本{0}",
+    "TaskDownloadMissingSubtitles": "下載遺失的字幕",
+    "TaskUpdatePlugins": "更新插件",
+    "TasksApplicationCategory": "應用程式"
 }

+ 0 - 39
Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs

@@ -1,39 +0,0 @@
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
-using WebSocketManager = Emby.Server.Implementations.WebSockets.WebSocketManager;
-
-namespace Emby.Server.Implementations.Middleware
-{
-    public class WebSocketMiddleware
-    {
-        private readonly RequestDelegate _next;
-        private readonly ILogger<WebSocketMiddleware> _logger;
-        private readonly WebSocketManager _webSocketManager;
-
-        public WebSocketMiddleware(RequestDelegate next, ILogger<WebSocketMiddleware> logger, WebSocketManager webSocketManager)
-        {
-            _next = next;
-            _logger = logger;
-            _webSocketManager = webSocketManager;
-        }
-
-        public async Task Invoke(HttpContext httpContext)
-        {
-            _logger.LogInformation("Handling request: " + httpContext.Request.Path);
-
-            if (httpContext.WebSockets.IsWebSocketRequest)
-            {
-                var webSocketContext = await httpContext.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false);
-                if (webSocketContext != null)
-                {
-                    await _webSocketManager.OnWebSocketConnected(webSocketContext).ConfigureAwait(false);
-                }
-            }
-            else
-            {
-                await _next.Invoke(httpContext).ConfigureAwait(false);
-            }
-        }
-    }
-}

+ 0 - 48
Emby.Server.Implementations/Net/IWebSocket.cs

@@ -1,48 +0,0 @@
-using System;
-using System.Net.WebSockets;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Emby.Server.Implementations.Net
-{
-    /// <summary>
-    /// Interface IWebSocket
-    /// </summary>
-    public interface IWebSocket : IDisposable
-    {
-        /// <summary>
-        /// Occurs when [closed].
-        /// </summary>
-        event EventHandler<EventArgs> Closed;
-
-        /// <summary>
-        /// Gets or sets the state.
-        /// </summary>
-        /// <value>The state.</value>
-        WebSocketState State { get; }
-
-        /// <summary>
-        /// Gets or sets the receive action.
-        /// </summary>
-        /// <value>The receive action.</value>
-        Action<byte[]> OnReceiveBytes { get; set; }
-
-        /// <summary>
-        /// Sends the async.
-        /// </summary>
-        /// <param name="bytes">The bytes.</param>
-        /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken);
-
-        /// <summary>
-        /// Sends the asynchronous.
-        /// </summary>
-        /// <param name="text">The text.</param>
-        /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken);
-    }
-}

+ 0 - 29
Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs

@@ -1,29 +0,0 @@
-using System;
-using Microsoft.AspNetCore.Http;
-
-namespace Emby.Server.Implementations.Net
-{
-    public class WebSocketConnectEventArgs : EventArgs
-    {
-        /// <summary>
-        /// Gets or sets the URL.
-        /// </summary>
-        /// <value>The URL.</value>
-        public string Url { get; set; }
-        /// <summary>
-        /// Gets or sets the query string.
-        /// </summary>
-        /// <value>The query string.</value>
-        public IQueryCollection QueryString { get; set; }
-        /// <summary>
-        /// Gets or sets the web socket.
-        /// </summary>
-        /// <value>The web socket.</value>
-        public IWebSocket WebSocket { get; set; }
-        /// <summary>
-        /// Gets or sets the endpoint.
-        /// </summary>
-        /// <value>The endpoint.</value>
-        public string Endpoint { get; set; }
-    }
-}

+ 2 - 14
Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Reflection;
+using MediaBrowser.Common.Extensions;
 
 namespace Emby.Server.Implementations.Services
 {
@@ -81,7 +82,7 @@ namespace Emby.Server.Implementations.Services
                 if (propertySerializerEntry.PropertyType == typeof(bool))
                 {
                     //InputExtensions.cs#530 MVC Checkbox helper emits extra hidden input field, generating 2 values, first is the real value
-                    propertyTextValue = LeftPart(propertyTextValue, ',');
+                    propertyTextValue = StringExtensions.LeftPart(propertyTextValue, ',').ToString();
                 }
 
                 var value = propertySerializerEntry.PropertyParseStringFn(propertyTextValue);
@@ -95,19 +96,6 @@ namespace Emby.Server.Implementations.Services
 
             return instance;
         }
-
-        public static string LeftPart(string strVal, char needle)
-        {
-            if (strVal == null)
-            {
-                return null;
-            }
-
-            var pos = strVal.IndexOf(needle);
-            return pos == -1
-                ? strVal
-                : strVal.Substring(0, pos);
-        }
     }
 
     internal static class TypeAccessor

+ 4 - 16
Emby.Server.Implementations/Services/UrlExtensions.cs

@@ -1,4 +1,5 @@
 using System;
+using MediaBrowser.Common.Extensions;
 
 namespace Emby.Server.Implementations.Services
 {
@@ -13,25 +14,12 @@ namespace Emby.Server.Implementations.Services
         public static string GetMethodName(this Type type)
         {
             var typeName = type.FullName != null // can be null, e.g. generic types
-                ? LeftPart(type.FullName, "[[")   // Generic Fullname
-                    .Replace(type.Namespace + ".", string.Empty) // Trim Namespaces
-                    .Replace("+", ".") // Convert nested into normal type
+                ? StringExtensions.LeftPart(type.FullName, "[[", StringComparison.Ordinal).ToString() // Generic Fullname
+                    .Replace(type.Namespace + ".", string.Empty, StringComparison.Ordinal) // Trim Namespaces
+                    .Replace("+", ".", StringComparison.Ordinal) // Convert nested into normal type
                 : type.Name;
 
             return type.IsGenericParameter ? "'" + typeName : typeName;
         }
-
-        private static string LeftPart(string strVal, string needle)
-        {
-            if (strVal == null)
-            {
-                return null;
-            }
-
-            var pos = strVal.IndexOf(needle, StringComparison.OrdinalIgnoreCase);
-            return pos == -1
-                ? strVal
-                : strVal.Substring(0, pos);
-        }
     }
 }

+ 0 - 191
Emby.Server.Implementations/Session/HttpSessionController.cs

@@ -1,191 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Net;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Session;
-
-namespace Emby.Server.Implementations.Session
-{
-    public class HttpSessionController : ISessionController
-    {
-        private readonly IHttpClient _httpClient;
-        private readonly IJsonSerializer _json;
-        private readonly ISessionManager _sessionManager;
-
-        public SessionInfo Session { get; private set; }
-
-        private readonly string _postUrl;
-
-        public HttpSessionController(IHttpClient httpClient,
-            IJsonSerializer json,
-            SessionInfo session,
-            string postUrl, ISessionManager sessionManager)
-        {
-            _httpClient = httpClient;
-            _json = json;
-            Session = session;
-            _postUrl = postUrl;
-            _sessionManager = sessionManager;
-        }
-
-        private string PostUrl => string.Format("http://{0}{1}", Session.RemoteEndPoint, _postUrl);
-
-        public bool IsSessionActive => (DateTime.UtcNow - Session.LastActivityDate).TotalMinutes <= 5;
-
-        public bool SupportsMediaControl => true;
-
-        private Task SendMessage(string name, string messageId, CancellationToken cancellationToken)
-        {
-            return SendMessage(name, messageId, new Dictionary<string, string>(), cancellationToken);
-        }
-
-        private Task SendMessage(string name, string messageId, Dictionary<string, string> args, CancellationToken cancellationToken)
-        {
-            args["messageId"] = messageId;
-            var url = PostUrl + "/" + name + ToQueryString(args);
-
-            return SendRequest(new HttpRequestOptions
-            {
-                Url = url,
-                CancellationToken = cancellationToken,
-                BufferContent = false
-            });
-        }
-
-        private Task SendPlayCommand(PlayRequest command, string messageId, CancellationToken cancellationToken)
-        {
-            var dict = new Dictionary<string, string>();
-
-            dict["ItemIds"] = string.Join(",", command.ItemIds.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
-
-            if (command.StartPositionTicks.HasValue)
-            {
-                dict["StartPositionTicks"] = command.StartPositionTicks.Value.ToString(CultureInfo.InvariantCulture);
-            }
-            if (command.AudioStreamIndex.HasValue)
-            {
-                dict["AudioStreamIndex"] = command.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture);
-            }
-            if (command.SubtitleStreamIndex.HasValue)
-            {
-                dict["SubtitleStreamIndex"] = command.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture);
-            }
-            if (command.StartIndex.HasValue)
-            {
-                dict["StartIndex"] = command.StartIndex.Value.ToString(CultureInfo.InvariantCulture);
-            }
-            if (!string.IsNullOrEmpty(command.MediaSourceId))
-            {
-                dict["MediaSourceId"] = command.MediaSourceId;
-            }
-
-            return SendMessage(command.PlayCommand.ToString(), messageId, dict, cancellationToken);
-        }
-
-        private Task SendPlaystateCommand(PlaystateRequest command, string messageId, CancellationToken cancellationToken)
-        {
-            var args = new Dictionary<string, string>();
-
-            if (command.Command == PlaystateCommand.Seek)
-            {
-                if (!command.SeekPositionTicks.HasValue)
-                {
-                    throw new ArgumentException("SeekPositionTicks cannot be null");
-                }
-
-                args["SeekPositionTicks"] = command.SeekPositionTicks.Value.ToString(CultureInfo.InvariantCulture);
-            }
-
-            return SendMessage(command.Command.ToString(), messageId, args, cancellationToken);
-        }
-
-        private string[] _supportedMessages = Array.Empty<string>();
-        public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken)
-        {
-            if (!IsSessionActive)
-            {
-                return Task.CompletedTask;
-            }
-
-            if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
-            {
-                return SendPlayCommand(data as PlayRequest, messageId, cancellationToken);
-            }
-            if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
-            {
-                return SendPlaystateCommand(data as PlaystateRequest, messageId, cancellationToken);
-            }
-            if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
-            {
-                var command = data as GeneralCommand;
-                return SendMessage(command.Name, messageId, command.Arguments, cancellationToken);
-            }
-
-            if (!_supportedMessages.Contains(name, StringComparer.OrdinalIgnoreCase))
-            {
-                return Task.CompletedTask;
-            }
-
-            var url = PostUrl + "/" + name;
-
-            url += "?messageId=" + messageId;
-
-            var options = new HttpRequestOptions
-            {
-                Url = url,
-                CancellationToken = cancellationToken,
-                BufferContent = false
-            };
-
-            if (data != null)
-            {
-                if (typeof(T) == typeof(string))
-                {
-                    var str = data as string;
-                    if (!string.IsNullOrEmpty(str))
-                    {
-                        options.RequestContent = str;
-                        options.RequestContentType = "application/json";
-                    }
-                }
-                else
-                {
-                    options.RequestContent = _json.SerializeToString(data);
-                    options.RequestContentType = "application/json";
-                }
-            }
-
-            return SendRequest(options);
-        }
-
-        private async Task SendRequest(HttpRequestOptions options)
-        {
-            using (var response = await _httpClient.Post(options).ConfigureAwait(false))
-            {
-
-            }
-        }
-
-        private static string ToQueryString(Dictionary<string, string> nvc)
-        {
-            var array = (from item in nvc
-                         select string.Format("{0}={1}", WebUtility.UrlEncode(item.Key), WebUtility.UrlEncode(item.Value)))
-                .ToArray();
-
-            var args = string.Join("&", array);
-
-            if (string.IsNullOrEmpty(args))
-            {
-                return args;
-            }
-
-            return "?" + args;
-        }
-    }
-}

+ 7 - 8
Emby.Server.Implementations/Session/SessionManager.cs

@@ -478,8 +478,7 @@ namespace Emby.Server.Implementations.Session
                 Client = appName,
                 DeviceId = deviceId,
                 ApplicationVersion = appVersion,
-                Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture),
-                ServerId = _appHost.SystemId
+                Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture)
             };
 
             var username = user?.Name;
@@ -1043,12 +1042,12 @@ namespace Emby.Server.Implementations.Session
 
         private static async Task SendMessageToSession<T>(SessionInfo session, string name, T data, CancellationToken cancellationToken)
         {
-            var controllers = session.SessionControllers.ToArray();
-            var messageId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+            var controllers = session.SessionControllers;
+            var messageId = Guid.NewGuid();
 
             foreach (var controller in controllers)
             {
-                await controller.SendMessage(name, messageId, data, controllers, cancellationToken).ConfigureAwait(false);
+                await controller.SendMessage(name, messageId, data, cancellationToken).ConfigureAwait(false);
             }
         }
 
@@ -1056,13 +1055,13 @@ namespace Emby.Server.Implementations.Session
         {
             IEnumerable<Task> GetTasks()
             {
-                var messageId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+                var messageId = Guid.NewGuid();
                 foreach (var session in sessions)
                 {
                     var controllers = session.SessionControllers;
                     foreach (var controller in controllers)
                     {
-                        yield return controller.SendMessage(name, messageId, data, controllers, cancellationToken);
+                        yield return controller.SendMessage(name, messageId, data, cancellationToken);
                     }
                 }
             }
@@ -1779,7 +1778,7 @@ namespace Emby.Server.Implementations.Session
                 throw new ArgumentNullException(nameof(info));
             }
 
-            var user = info.UserId.Equals(Guid.Empty)
+            var user = info.UserId == Guid.Empty
                 ? null
                 : _userManager.GetUserById(info.UserId);
 

+ 17 - 16
Emby.Server.Implementations/Session/SessionWebSocketListener.cs

@@ -8,7 +8,6 @@ using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Events;
 using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
 
@@ -17,7 +16,7 @@ namespace Emby.Server.Implementations.Session
     /// <summary>
     /// Class SessionWebSocketListener
     /// </summary>
-    public class SessionWebSocketListener : IWebSocketListener, IDisposable
+    public sealed class SessionWebSocketListener : IWebSocketListener, IDisposable
     {
         /// <summary>
         /// The timeout in seconds after which a WebSocket is considered to be lost.
@@ -43,11 +42,7 @@ namespace Emby.Server.Implementations.Session
         /// The _logger
         /// </summary>
         private readonly ILogger _logger;
-
-        /// <summary>
-        /// The _dto service
-        /// </summary>
-        private readonly IJsonSerializer _json;
+        private readonly ILoggerFactory _loggerFactory;
 
         private readonly IHttpServer _httpServer;
 
@@ -74,23 +69,27 @@ namespace Emby.Server.Implementations.Session
         /// <summary>
         /// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class.
         /// </summary>
+        /// <param name="logger">The logger.</param>
         /// <param name="sessionManager">The session manager.</param>
         /// <param name="loggerFactory">The logger factory.</param>
-        /// <param name="json">The json.</param>
         /// <param name="httpServer">The HTTP server.</param>
-        public SessionWebSocketListener(ISessionManager sessionManager, ILoggerFactory loggerFactory, IJsonSerializer json, IHttpServer httpServer)
+        public SessionWebSocketListener(
+            ILogger<SessionWebSocketListener> logger,
+            ISessionManager sessionManager,
+            ILoggerFactory loggerFactory,
+            IHttpServer httpServer)
         {
+            _logger = logger;
             _sessionManager = sessionManager;
-            _logger = loggerFactory.CreateLogger(GetType().Name);
-            _json = json;
+            _loggerFactory = loggerFactory;
             _httpServer = httpServer;
+
             httpServer.WebSocketConnected += OnServerManagerWebSocketConnected;
         }
 
-        void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
+        private void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
         {
-            var session = GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint);
-
+            var session = GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint.ToString());
             if (session != null)
             {
                 EnsureController(session, e.Argument);
@@ -98,7 +97,7 @@ namespace Emby.Server.Implementations.Session
             }
             else
             {
-                _logger.LogWarning("Unable to determine session based on url: {0}", e.Argument.Url);
+                _logger.LogWarning("Unable to determine session based on query string: {0}", e.Argument.QueryString);
             }
         }
 
@@ -119,6 +118,7 @@ namespace Emby.Server.Implementations.Session
             return _sessionManager.GetSessionByAuthenticationToken(token, deviceId, remoteEndpoint);
         }
 
+        /// <inheritdoc />
         public void Dispose()
         {
             _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected;
@@ -135,7 +135,8 @@ namespace Emby.Server.Implementations.Session
 
         private void EnsureController(SessionInfo session, IWebSocketConnection connection)
         {
-            var controllerInfo = session.EnsureController<WebSocketController>(s => new WebSocketController(s, _logger, _sessionManager));
+            var controllerInfo = session.EnsureController<WebSocketController>(
+                s => new WebSocketController(_loggerFactory.CreateLogger<WebSocketController>(), s, _sessionManager));
 
             var controller = (WebSocketController)controllerInfo.Item1;
             controller.AddWebSocket(connection);

+ 51 - 35
Emby.Server.Implementations/Session/WebSocketController.cs

@@ -1,3 +1,7 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+#nullable enable
+
 using System;
 using System.Collections.Generic;
 using System.Linq;
@@ -11,60 +15,63 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.Session
 {
-    public class WebSocketController : ISessionController, IDisposable
+    public sealed class WebSocketController : ISessionController, IDisposable
     {
-        public SessionInfo Session { get; private set; }
-        public IReadOnlyList<IWebSocketConnection> Sockets { get; private set; }
-
         private readonly ILogger _logger;
-
         private readonly ISessionManager _sessionManager;
+        private readonly SessionInfo _session;
 
-        public WebSocketController(SessionInfo session, ILogger logger, ISessionManager sessionManager)
+        private readonly List<IWebSocketConnection> _sockets;
+        private bool _disposed = false;
+
+        public WebSocketController(
+            ILogger<WebSocketController> logger,
+            SessionInfo session,
+            ISessionManager sessionManager)
         {
-            Session = session;
             _logger = logger;
+            _session = session;
             _sessionManager = sessionManager;
-            Sockets = new List<IWebSocketConnection>();
+            _sockets = new List<IWebSocketConnection>();
         }
 
         private bool HasOpenSockets => GetActiveSockets().Any();
 
+        /// <inheritdoc />
         public bool SupportsMediaControl => HasOpenSockets;
 
+        /// <inheritdoc />
         public bool IsSessionActive => HasOpenSockets;
 
         private IEnumerable<IWebSocketConnection> GetActiveSockets()
-        {
-            return Sockets
-                .OrderByDescending(i => i.LastActivityDate)
-                .Where(i => i.State == WebSocketState.Open);
-        }
+            => _sockets.Where(i => i.State == WebSocketState.Open);
 
         public void AddWebSocket(IWebSocketConnection connection)
         {
-            var sockets = Sockets.ToList();
-            sockets.Add(connection);
+            _logger.LogDebug("Adding websocket to session {Session}", _session.Id);
+            _sockets.Add(connection);
 
-            Sockets = sockets;
-
-            connection.Closed += connection_Closed;
+            connection.Closed += OnConnectionClosed;
         }
 
-        void connection_Closed(object sender, EventArgs e)
+        private void OnConnectionClosed(object sender, EventArgs e)
         {
             var connection = (IWebSocketConnection)sender;
-            var sockets = Sockets.ToList();
-            sockets.Remove(connection);
-
-            Sockets = sockets;
-
-            _sessionManager.CloseIfNeeded(Session);
+            _logger.LogDebug("Removing websocket from session {Session}", _session.Id);
+            _sockets.Remove(connection);
+            connection.Closed -= OnConnectionClosed;
+            _sessionManager.CloseIfNeeded(_session);
         }
 
-        public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken)
+        /// <inheritdoc />
+        public Task SendMessage<T>(
+            string name,
+            Guid messageId,
+            T data,
+            CancellationToken cancellationToken)
         {
             var socket = GetActiveSockets()
+                .OrderByDescending(i => i.LastActivityDate)
                 .FirstOrDefault();
 
             if (socket == null)
@@ -72,21 +79,30 @@ namespace Emby.Server.Implementations.Session
                 return Task.CompletedTask;
             }
 
-            return socket.SendAsync(new WebSocketMessage<T>
-            {
-                Data = data,
-                MessageType = name,
-                MessageId = messageId
-
-            }, cancellationToken);
+            return socket.SendAsync(
+                new WebSocketMessage<T>
+                {
+                    Data = data,
+                    MessageType = name,
+                    MessageId = messageId
+                },
+                cancellationToken);
         }
 
+        /// <inheritdoc />
         public void Dispose()
         {
-            foreach (var socket in Sockets.ToList())
+            if (_disposed)
             {
-                socket.Closed -= connection_Closed;
+                return;
             }
+
+            foreach (var socket in _sockets)
+            {
+                socket.Closed -= OnConnectionClosed;
+            }
+
+            _disposed = true;
         }
     }
 }

+ 0 - 105
Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs

@@ -1,105 +0,0 @@
-using System;
-using System.Net.WebSockets;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.Net;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.SocketSharp
-{
-    public class SharpWebSocket : IWebSocket
-    {
-        /// <summary>
-        /// The logger
-        /// </summary>
-        private readonly ILogger _logger;
-
-        public event EventHandler<EventArgs> Closed;
-
-        /// <summary>
-        /// Gets or sets the web socket.
-        /// </summary>
-        /// <value>The web socket.</value>
-        private readonly WebSocket _webSocket;
-
-        private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
-        private bool _disposed;
-
-        public SharpWebSocket(WebSocket socket, ILogger logger)
-        {
-            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
-            _webSocket = socket ?? throw new ArgumentNullException(nameof(socket));
-        }
-
-        /// <summary>
-        /// Gets the state.
-        /// </summary>
-        /// <value>The state.</value>
-        public WebSocketState State => _webSocket.State;
-
-        /// <summary>
-        /// Sends the async.
-        /// </summary>
-        /// <param name="bytes">The bytes.</param>
-        /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken)
-        {
-            return _webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Binary, endOfMessage, cancellationToken);
-        }
-
-        /// <summary>
-        /// Sends the asynchronous.
-        /// </summary>
-        /// <param name="text">The text.</param>
-        /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken)
-        {
-            return _webSocket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes(text)), WebSocketMessageType.Text, endOfMessage, cancellationToken);
-        }
-
-        /// <summary>
-        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
-        /// </summary>
-        public void Dispose()
-        {
-            Dispose(true);
-            GC.SuppressFinalize(this);
-        }
-
-        /// <summary>
-        /// Releases unmanaged and - optionally - managed resources.
-        /// </summary>
-        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
-        protected virtual void Dispose(bool dispose)
-        {
-            if (_disposed)
-            {
-                return;
-            }
-
-            if (dispose)
-            {
-                _cancellationTokenSource.Cancel();
-                if (_webSocket.State == WebSocketState.Open)
-                {
-                    _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by client",
-                        CancellationToken.None);
-                }
-                Closed?.Invoke(this, EventArgs.Empty);
-            }
-
-            _disposed = true;
-        }
-
-        /// <summary>
-        /// Gets or sets the receive action.
-        /// </summary>
-        /// <value>The receive action.</value>
-        public Action<byte[]> OnReceiveBytes { get; set; }
-    }
-}

+ 0 - 135
Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs

@@ -1,135 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net.WebSockets;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.HttpServer;
-using Emby.Server.Implementations.Net;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.SocketSharp
-{
-    public class WebSocketSharpListener : IHttpListener
-    {
-        private readonly ILogger _logger;
-
-        private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
-        private CancellationToken _disposeCancellationToken;
-
-        public WebSocketSharpListener(ILogger<WebSocketSharpListener> logger)
-        {
-            _logger = logger;
-            _disposeCancellationToken = _disposeCancellationTokenSource.Token;
-        }
-
-        public Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; }
-
-        public Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; }
-
-        public Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }
-
-        private static void LogRequest(ILogger logger, HttpRequest request)
-        {
-            var url = request.GetDisplayUrl();
-
-            logger.LogInformation("WS {Url}. UserAgent: {UserAgent}", url, request.Headers[HeaderNames.UserAgent].ToString());
-        }
-
-        public async Task ProcessWebSocketRequest(HttpContext ctx)
-        {
-            try
-            {
-                LogRequest(_logger, ctx.Request);
-                var endpoint = ctx.Connection.RemoteIpAddress.ToString();
-                var url = ctx.Request.GetDisplayUrl();
-
-                var webSocketContext = await ctx.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false);
-                var socket = new SharpWebSocket(webSocketContext, _logger);
-
-                WebSocketConnected(new WebSocketConnectEventArgs
-                {
-                    Url = url,
-                    QueryString = ctx.Request.Query,
-                    WebSocket = socket,
-                    Endpoint = endpoint
-                });
-
-                WebSocketReceiveResult result;
-                var message = new List<byte>();
-
-                do
-                {
-                    var buffer = WebSocket.CreateServerBuffer(4096);
-                    result = await webSocketContext.ReceiveAsync(buffer, _disposeCancellationToken);
-                    message.AddRange(buffer.Array.Take(result.Count));
-
-                    if (result.EndOfMessage)
-                    {
-                        socket.OnReceiveBytes(message.ToArray());
-                        message.Clear();
-                    }
-                } while (socket.State == WebSocketState.Open && result.MessageType != WebSocketMessageType.Close);
-
-
-                if (webSocketContext.State == WebSocketState.Open)
-                {
-                    await webSocketContext.CloseAsync(
-                        result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
-                        result.CloseStatusDescription,
-                        _disposeCancellationToken).ConfigureAwait(false);
-                }
-
-                socket.Dispose();
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "AcceptWebSocketAsync error");
-                if (!ctx.Response.HasStarted)
-                {
-                    ctx.Response.StatusCode = 500;
-                }
-            }
-        }
-
-        public Task Stop()
-        {
-            _disposeCancellationTokenSource.Cancel();
-            return Task.CompletedTask;
-        }
-
-        /// <summary>
-        /// Releases the unmanaged resources and disposes of the managed resources used.
-        /// </summary>
-        public void Dispose()
-        {
-            Dispose(true);
-            GC.SuppressFinalize(this);
-        }
-
-        private bool _disposed;
-
-        /// <summary>
-        /// Releases the unmanaged resources and disposes of the managed resources used.
-        /// </summary>
-        /// <param name="disposing">Whether or not the managed resources should be disposed.</param>
-        protected virtual void Dispose(bool disposing)
-        {
-            if (_disposed)
-            {
-                return;
-            }
-
-            if (disposing)
-            {
-                Stop().GetAwaiter().GetResult();
-            }
-
-            _disposed = true;
-        }
-    }
-}

+ 10 - 14
Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.IO;
 using System.Net;
 using System.Net.Mime;
+using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http.Extensions;
@@ -62,6 +63,9 @@ namespace Emby.Server.Implementations.SocketSharp
                     if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XRealIP), out ip))
                     {
                         ip = Request.HttpContext.Connection.RemoteIpAddress;
+
+                        // Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests)
+                        ip ??= IPAddress.Loopback;
                     }
                 }
 
@@ -89,7 +93,10 @@ namespace Emby.Server.Implementations.SocketSharp
 
         public IQueryCollection QueryString => Request.Query;
 
-        public bool IsLocal => Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress);
+        public bool IsLocal =>
+            (Request.HttpContext.Connection.LocalIpAddress == null
+            && Request.HttpContext.Connection.RemoteIpAddress == null)
+            || Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress);
 
         public string HttpMethod => Request.Method;
 
@@ -216,14 +223,14 @@ namespace Emby.Server.Implementations.SocketSharp
                     pi = pi.Slice(1);
                 }
 
-                format = LeftPart(pi, '/');
+                format = pi.LeftPart('/');
                 if (format.Length > FormatMaxLength)
                 {
                     return null;
                 }
             }
 
-            format = LeftPart(format, '.');
+            format = format.LeftPart('.');
             if (format.Contains("json", StringComparison.OrdinalIgnoreCase))
             {
                 return "application/json";
@@ -235,16 +242,5 @@ namespace Emby.Server.Implementations.SocketSharp
 
             return null;
         }
-
-        public static ReadOnlySpan<char> LeftPart(ReadOnlySpan<char> strVal, char needle)
-        {
-            if (strVal == null)
-            {
-                return null;
-            }
-
-            var pos = strVal.IndexOf(needle);
-            return pos == -1 ? strVal : strVal.Slice(0, pos);
-        }
     }
 }

+ 0 - 10
Emby.Server.Implementations/WebSockets/WebSocketHandler.cs

@@ -1,10 +0,0 @@
-using System.Threading.Tasks;
-using MediaBrowser.Model.Net;
-
-namespace Emby.Server.Implementations.WebSockets
-{
-    public interface IWebSocketHandler
-    {
-        Task ProcessMessage(WebSocketMessage<object> message, TaskCompletionSource<bool> taskCompletionSource);
-    }
-}

+ 0 - 102
Emby.Server.Implementations/WebSockets/WebSocketManager.cs

@@ -1,102 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net.WebSockets;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
-using Microsoft.Extensions.Logging;
-using UtfUnknown;
-
-namespace Emby.Server.Implementations.WebSockets
-{
-    public class WebSocketManager
-    {
-        private readonly IWebSocketHandler[] _webSocketHandlers;
-        private readonly IJsonSerializer _jsonSerializer;
-        private readonly ILogger<WebSocketManager> _logger;
-        private const int BufferSize = 4096;
-
-        public WebSocketManager(IWebSocketHandler[] webSocketHandlers, IJsonSerializer jsonSerializer, ILogger<WebSocketManager> logger)
-        {
-            _webSocketHandlers = webSocketHandlers;
-            _jsonSerializer = jsonSerializer;
-            _logger = logger;
-        }
-
-        public async Task OnWebSocketConnected(WebSocket webSocket)
-        {
-            var taskCompletionSource = new TaskCompletionSource<bool>();
-            var cancellationToken = new CancellationTokenSource().Token;
-            WebSocketReceiveResult result;
-            var message = new List<byte>();
-
-            // Keep listening for incoming messages, otherwise the socket closes automatically
-            do
-            {
-                var buffer = WebSocket.CreateServerBuffer(BufferSize);
-                result = await webSocket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false);
-                message.AddRange(buffer.Array.Take(result.Count));
-
-                if (result.EndOfMessage)
-                {
-                    await ProcessMessage(message.ToArray(), taskCompletionSource).ConfigureAwait(false);
-                    message.Clear();
-                }
-            } while (!taskCompletionSource.Task.IsCompleted &&
-                     webSocket.State == WebSocketState.Open &&
-                     result.MessageType != WebSocketMessageType.Close);
-
-            if (webSocket.State == WebSocketState.Open)
-            {
-                await webSocket.CloseAsync(
-                    result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
-                    result.CloseStatusDescription,
-                    cancellationToken).ConfigureAwait(false);
-            }
-        }
-
-        private async Task ProcessMessage(byte[] messageBytes, TaskCompletionSource<bool> taskCompletionSource)
-        {
-            var charset = CharsetDetector.DetectFromBytes(messageBytes).Detected?.EncodingName;
-            var message = string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase)
-                ? Encoding.UTF8.GetString(messageBytes, 0, messageBytes.Length)
-                : Encoding.ASCII.GetString(messageBytes, 0, messageBytes.Length);
-
-            // All messages are expected to be valid JSON objects
-            if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase))
-            {
-                _logger.LogDebug("Received web socket message that is not a json structure: {Message}", message);
-                return;
-            }
-
-            try
-            {
-                var info = _jsonSerializer.DeserializeFromString<WebSocketMessage<object>>(message);
-
-                _logger.LogDebug("Websocket message received: {0}", info.MessageType);
-
-                var tasks = _webSocketHandlers.Select(handler => Task.Run(() =>
-                {
-                    try
-                    {
-                        handler.ProcessMessage(info, taskCompletionSource).ConfigureAwait(false);
-                    }
-                    catch (Exception ex)
-                    {
-                        _logger.LogError(ex, "{HandlerType} failed processing WebSocket message {MessageType}",
-                            handler.GetType().Name, info.MessageType ?? string.Empty);
-                    }
-                }));
-
-                await Task.WhenAll(tasks);
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error processing web socket message");
-            }
-        }
-    }
-}

+ 195 - 0
Jellyfin.Data/Entities/Artwork.cs

@@ -0,0 +1,195 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace Jellyfin.Data.Entities
+{
+    public partial class Artwork
+    {
+        partial void Init();
+
+        /// <summary>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </summary>
+        protected Artwork()
+        {
+            Init();
+        }
+
+        /// <summary>
+        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+        /// </summary>
+        public static Artwork CreateArtworkUnsafe()
+        {
+            return new Artwork();
+        }
+
+        /// <summary>
+        /// Public constructor with required data
+        /// </summary>
+        /// <param name="path"></param>
+        /// <param name="kind"></param>
+        /// <param name="_metadata0"></param>
+        /// <param name="_personrole1"></param>
+        public Artwork(string path, Enums.ArtKind kind, Metadata _metadata0, PersonRole _personrole1)
+        {
+            if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path));
+            this.Path = path;
+
+            this.Kind = kind;
+
+            if (_metadata0 == null) throw new ArgumentNullException(nameof(_metadata0));
+            _metadata0.Artwork.Add(this);
+
+            if (_personrole1 == null) throw new ArgumentNullException(nameof(_personrole1));
+            _personrole1.Artwork = this;
+
+
+            Init();
+        }
+
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </summary>
+        /// <param name="path"></param>
+        /// <param name="kind"></param>
+        /// <param name="_metadata0"></param>
+        /// <param name="_personrole1"></param>
+        public static Artwork Create(string path, Enums.ArtKind kind, Metadata _metadata0, PersonRole _personrole1)
+        {
+            return new Artwork(path, kind, _metadata0, _personrole1);
+        }
+
+        /*************************************************************************
+         * Properties
+         *************************************************************************/
+
+        /// <summary>
+        /// Backing field for Id
+        /// </summary>
+        internal int _Id;
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before setting.
+        /// </summary>
+        partial void SetId(int oldValue, ref int newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before returning.
+        /// </summary>
+        partial void GetId(ref int result);
+
+        /// <summary>
+        /// Identity, Indexed, Required
+        /// </summary>
+        [Key]
+        [Required]
+        public int Id
+        {
+            get
+            {
+                int value = _Id;
+                GetId(ref value);
+                return (_Id = value);
+            }
+            protected set
+            {
+                int oldValue = _Id;
+                SetId(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Id = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for Path
+        /// </summary>
+        protected string _Path;
+        /// <summary>
+        /// When provided in a partial class, allows value of Path to be changed before setting.
+        /// </summary>
+        partial void SetPath(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Path to be changed before returning.
+        /// </summary>
+        partial void GetPath(ref string result);
+
+        /// <summary>
+        /// Required, Max length = 65535
+        /// </summary>
+        [Required]
+        [MaxLength(65535)]
+        [StringLength(65535)]
+        public string Path
+        {
+            get
+            {
+                string value = _Path;
+                GetPath(ref value);
+                return (_Path = value);
+            }
+            set
+            {
+                string oldValue = _Path;
+                SetPath(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Path = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for Kind
+        /// </summary>
+        internal Enums.ArtKind _Kind;
+        /// <summary>
+        /// When provided in a partial class, allows value of Kind to be changed before setting.
+        /// </summary>
+        partial void SetKind(Enums.ArtKind oldValue, ref Enums.ArtKind newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Kind to be changed before returning.
+        /// </summary>
+        partial void GetKind(ref Enums.ArtKind result);
+
+        /// <summary>
+        /// Indexed, Required
+        /// </summary>
+        [Required]
+        public Enums.ArtKind Kind
+        {
+            get
+            {
+                Enums.ArtKind value = _Kind;
+                GetKind(ref value);
+                return (_Kind = value);
+            }
+            set
+            {
+                Enums.ArtKind oldValue = _Kind;
+                SetKind(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Kind = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Required, ConcurrenyToken
+        /// </summary>
+        [ConcurrencyCheck]
+        [Required]
+        public uint RowVersion { get; set; }
+
+        public void OnSavingChanges()
+        {
+            RowVersion++;
+        }
+
+        /*************************************************************************
+         * Navigation properties
+         *************************************************************************/
+
+    }
+}
+

+ 69 - 0
Jellyfin.Data/Entities/Book.cs

@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+    public partial class Book : LibraryItem
+    {
+        partial void Init();
+
+        /// <summary>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </summary>
+        protected Book()
+        {
+            BookMetadata = new HashSet<BookMetadata>();
+            Releases = new HashSet<Release>();
+
+            Init();
+        }
+
+        /// <summary>
+        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+        /// </summary>
+        public static Book CreateBookUnsafe()
+        {
+            return new Book();
+        }
+
+        /// <summary>
+        /// Public constructor with required data
+        /// </summary>
+        /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+        public Book(Guid urlid, DateTime dateadded)
+        {
+            this.UrlId = urlid;
+
+            this.BookMetadata = new HashSet<BookMetadata>();
+            this.Releases = new HashSet<Release>();
+
+            Init();
+        }
+
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </summary>
+        /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+        public static Book Create(Guid urlid, DateTime dateadded)
+        {
+            return new Book(urlid, dateadded);
+        }
+
+        /*************************************************************************
+         * Properties
+         *************************************************************************/
+
+        /*************************************************************************
+         * Navigation properties
+         *************************************************************************/
+
+        [ForeignKey("BookMetadata_BookMetadata_Id")]
+        public virtual ICollection<BookMetadata> BookMetadata { get; protected set; }
+
+        [ForeignKey("Release_Releases_Id")]
+        public virtual ICollection<Release> Releases { get; protected set; }
+
+    }
+}
+

+ 107 - 0
Jellyfin.Data/Entities/BookMetadata.cs

@@ -0,0 +1,107 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+    public partial class BookMetadata : Metadata
+    {
+        partial void Init();
+
+        /// <summary>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </summary>
+        protected BookMetadata()
+        {
+            Publishers = new HashSet<Company>();
+
+            Init();
+        }
+
+        /// <summary>
+        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+        /// </summary>
+        public static BookMetadata CreateBookMetadataUnsafe()
+        {
+            return new BookMetadata();
+        }
+
+        /// <summary>
+        /// Public constructor with required data
+        /// </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="_book0"></param>
+        public BookMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Book _book0)
+        {
+            if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title));
+            this.Title = title;
+
+            if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language));
+            this.Language = language;
+
+            if (_book0 == null) throw new ArgumentNullException(nameof(_book0));
+            _book0.BookMetadata.Add(this);
+
+            this.Publishers = new HashSet<Company>();
+
+            Init();
+        }
+
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </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="_book0"></param>
+        public static BookMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Book _book0)
+        {
+            return new BookMetadata(title, language, dateadded, datemodified, _book0);
+        }
+
+        /*************************************************************************
+         * Properties
+         *************************************************************************/
+
+        /// <summary>
+        /// Backing field for ISBN
+        /// </summary>
+        protected long? _ISBN;
+        /// <summary>
+        /// When provided in a partial class, allows value of ISBN to be changed before setting.
+        /// </summary>
+        partial void SetISBN(long? oldValue, ref long? newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of ISBN to be changed before returning.
+        /// </summary>
+        partial void GetISBN(ref long? result);
+
+        public long? ISBN
+        {
+            get
+            {
+                long? value = _ISBN;
+                GetISBN(ref value);
+                return (_ISBN = value);
+            }
+            set
+            {
+                long? oldValue = _ISBN;
+                SetISBN(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _ISBN = value;
+                }
+            }
+        }
+
+        /*************************************************************************
+         * Navigation properties
+         *************************************************************************/
+
+        [ForeignKey("Company_Publishers_Id")]
+        public virtual ICollection<Company> Publishers { get; protected set; }
+
+    }
+}
+

+ 263 - 0
Jellyfin.Data/Entities/Chapter.cs

@@ -0,0 +1,263 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+    public partial class Chapter
+    {
+        partial void Init();
+
+        /// <summary>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </summary>
+        protected Chapter()
+        {
+            Init();
+        }
+
+        /// <summary>
+        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+        /// </summary>
+        public static Chapter CreateChapterUnsafe()
+        {
+            return new Chapter();
+        }
+
+        /// <summary>
+        /// Public constructor with required data
+        /// </summary>
+        /// <param name="language">ISO-639-3 3-character language codes</param>
+        /// <param name="timestart"></param>
+        /// <param name="_release0"></param>
+        public Chapter(string language, long timestart, Release _release0)
+        {
+            if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language));
+            this.Language = language;
+
+            this.TimeStart = timestart;
+
+            if (_release0 == null) throw new ArgumentNullException(nameof(_release0));
+            _release0.Chapters.Add(this);
+
+
+            Init();
+        }
+
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </summary>
+        /// <param name="language">ISO-639-3 3-character language codes</param>
+        /// <param name="timestart"></param>
+        /// <param name="_release0"></param>
+        public static Chapter Create(string language, long timestart, Release _release0)
+        {
+            return new Chapter(language, timestart, _release0);
+        }
+
+        /*************************************************************************
+         * Properties
+         *************************************************************************/
+
+        /// <summary>
+        /// Backing field for Id
+        /// </summary>
+        internal int _Id;
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before setting.
+        /// </summary>
+        partial void SetId(int oldValue, ref int newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before returning.
+        /// </summary>
+        partial void GetId(ref int result);
+
+        /// <summary>
+        /// Identity, Indexed, Required
+        /// </summary>
+        [Key]
+        [Required]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id
+        {
+            get
+            {
+                int value = _Id;
+                GetId(ref value);
+                return (_Id = value);
+            }
+            protected set
+            {
+                int oldValue = _Id;
+                SetId(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Id = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for Name
+        /// </summary>
+        protected string _Name;
+        /// <summary>
+        /// When provided in a partial class, allows value of Name to be changed before setting.
+        /// </summary>
+        partial void SetName(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Name to be changed before returning.
+        /// </summary>
+        partial void GetName(ref string result);
+
+        /// <summary>
+        /// Max length = 1024
+        /// </summary>
+        [MaxLength(1024)]
+        [StringLength(1024)]
+        public string Name
+        {
+            get
+            {
+                string value = _Name;
+                GetName(ref value);
+                return (_Name = value);
+            }
+            set
+            {
+                string oldValue = _Name;
+                SetName(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Name = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for Language
+        /// </summary>
+        protected string _Language;
+        /// <summary>
+        /// When provided in a partial class, allows value of Language to be changed before setting.
+        /// </summary>
+        partial void SetLanguage(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Language to be changed before returning.
+        /// </summary>
+        partial void GetLanguage(ref string result);
+
+        /// <summary>
+        /// Required, Min length = 3, Max length = 3
+        /// ISO-639-3 3-character language codes
+        /// </summary>
+        [Required]
+        [MinLength(3)]
+        [MaxLength(3)]
+        [StringLength(3)]
+        public string Language
+        {
+            get
+            {
+                string value = _Language;
+                GetLanguage(ref value);
+                return (_Language = value);
+            }
+            set
+            {
+                string oldValue = _Language;
+                SetLanguage(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Language = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for TimeStart
+        /// </summary>
+        protected long _TimeStart;
+        /// <summary>
+        /// When provided in a partial class, allows value of TimeStart to be changed before setting.
+        /// </summary>
+        partial void SetTimeStart(long oldValue, ref long newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of TimeStart to be changed before returning.
+        /// </summary>
+        partial void GetTimeStart(ref long result);
+
+        /// <summary>
+        /// Required
+        /// </summary>
+        [Required]
+        public long TimeStart
+        {
+            get
+            {
+                long value = _TimeStart;
+                GetTimeStart(ref value);
+                return (_TimeStart = value);
+            }
+            set
+            {
+                long oldValue = _TimeStart;
+                SetTimeStart(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _TimeStart = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for TimeEnd
+        /// </summary>
+        protected long? _TimeEnd;
+        /// <summary>
+        /// When provided in a partial class, allows value of TimeEnd to be changed before setting.
+        /// </summary>
+        partial void SetTimeEnd(long? oldValue, ref long? newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of TimeEnd to be changed before returning.
+        /// </summary>
+        partial void GetTimeEnd(ref long? result);
+
+        public long? TimeEnd
+        {
+            get
+            {
+                long? value = _TimeEnd;
+                GetTimeEnd(ref value);
+                return (_TimeEnd = value);
+            }
+            set
+            {
+                long? oldValue = _TimeEnd;
+                SetTimeEnd(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _TimeEnd = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Required, ConcurrenyToken
+        /// </summary>
+        [ConcurrencyCheck]
+        [Required]
+        public uint RowVersion { get; set; }
+
+        public void OnSavingChanges()
+        {
+            RowVersion++;
+        }
+
+        /*************************************************************************
+         * Navigation properties
+         *************************************************************************/
+
+    }
+}
+

+ 120 - 0
Jellyfin.Data/Entities/Collection.cs

@@ -0,0 +1,120 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+    public partial class Collection
+    {
+        partial void Init();
+
+        /// <summary>
+        /// Default constructor
+        /// </summary>
+        public Collection()
+        {
+            CollectionItem = new LinkedList<CollectionItem>();
+
+            Init();
+        }
+
+        /*************************************************************************
+         * Properties
+         *************************************************************************/
+
+        /// <summary>
+        /// Backing field for Id
+        /// </summary>
+        internal int _Id;
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before setting.
+        /// </summary>
+        partial void SetId(int oldValue, ref int newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before returning.
+        /// </summary>
+        partial void GetId(ref int result);
+
+        /// <summary>
+        /// Identity, Indexed, Required
+        /// </summary>
+        [Key]
+        [Required]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id
+        {
+            get
+            {
+                int value = _Id;
+                GetId(ref value);
+                return (_Id = value);
+            }
+            protected set
+            {
+                int oldValue = _Id;
+                SetId(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Id = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for Name
+        /// </summary>
+        protected string _Name;
+        /// <summary>
+        /// When provided in a partial class, allows value of Name to be changed before setting.
+        /// </summary>
+        partial void SetName(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Name to be changed before returning.
+        /// </summary>
+        partial void GetName(ref string result);
+
+        /// <summary>
+        /// Max length = 1024
+        /// </summary>
+        [MaxLength(1024)]
+        [StringLength(1024)]
+        public string Name
+        {
+            get
+            {
+                string value = _Name;
+                GetName(ref value);
+                return (_Name = value);
+            }
+            set
+            {
+                string oldValue = _Name;
+                SetName(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Name = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Required, ConcurrenyToken
+        /// </summary>
+        [ConcurrencyCheck]
+        [Required]
+        public uint RowVersion { get; set; }
+
+        public void OnSavingChanges()
+        {
+            RowVersion++;
+        }
+
+        /*************************************************************************
+         * Navigation properties
+         *************************************************************************/
+        [ForeignKey("CollectionItem_CollectionItem_Id")]
+        public virtual ICollection<CollectionItem> CollectionItem { get; protected set; }
+
+    }
+}
+

+ 143 - 0
Jellyfin.Data/Entities/CollectionItem.cs

@@ -0,0 +1,143 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+    public partial class CollectionItem
+    {
+        partial void Init();
+
+        /// <summary>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </summary>
+        protected CollectionItem()
+        {
+            // NOTE: This class has one-to-one associations with CollectionItem.
+            // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other.
+
+            Init();
+        }
+
+        /// <summary>
+        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+        /// </summary>
+        public static CollectionItem CreateCollectionItemUnsafe()
+        {
+            return new CollectionItem();
+        }
+
+        /// <summary>
+        /// Public constructor with required data
+        /// </summary>
+        /// <param name="_collection0"></param>
+        /// <param name="_collectionitem1"></param>
+        /// <param name="_collectionitem2"></param>
+        public CollectionItem(Collection _collection0, CollectionItem _collectionitem1, CollectionItem _collectionitem2)
+        {
+            // NOTE: This class has one-to-one associations with CollectionItem.
+            // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other.
+
+            if (_collection0 == null) throw new ArgumentNullException(nameof(_collection0));
+            _collection0.CollectionItem.Add(this);
+
+            if (_collectionitem1 == null) throw new ArgumentNullException(nameof(_collectionitem1));
+            _collectionitem1.Next = this;
+
+            if (_collectionitem2 == null) throw new ArgumentNullException(nameof(_collectionitem2));
+            _collectionitem2.Previous = this;
+
+
+            Init();
+        }
+
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </summary>
+        /// <param name="_collection0"></param>
+        /// <param name="_collectionitem1"></param>
+        /// <param name="_collectionitem2"></param>
+        public static CollectionItem Create(Collection _collection0, CollectionItem _collectionitem1, CollectionItem _collectionitem2)
+        {
+            return new CollectionItem(_collection0, _collectionitem1, _collectionitem2);
+        }
+
+        /*************************************************************************
+         * Properties
+         *************************************************************************/
+
+        /// <summary>
+        /// Backing field for Id
+        /// </summary>
+        internal int _Id;
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before setting.
+        /// </summary>
+        partial void SetId(int oldValue, ref int newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before returning.
+        /// </summary>
+        partial void GetId(ref int result);
+
+        /// <summary>
+        /// Identity, Indexed, Required
+        /// </summary>
+        [Key]
+        [Required]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id
+        {
+            get
+            {
+                int value = _Id;
+                GetId(ref value);
+                return (_Id = value);
+            }
+            protected set
+            {
+                int oldValue = _Id;
+                SetId(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Id = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Required, ConcurrenyToken
+        /// </summary>
+        [ConcurrencyCheck]
+        [Required]
+        public uint RowVersion { get; set; }
+
+        public void OnSavingChanges()
+        {
+            RowVersion++;
+        }
+
+        /*************************************************************************
+         * Navigation properties
+         *************************************************************************/
+
+        /// <summary>
+        /// Required
+        /// </summary>
+        [ForeignKey("LibraryItem_Id")]
+        public virtual LibraryItem LibraryItem { get; set; }
+
+        /// <remarks>
+        /// TODO check if this properly updated dependant and has the proper principal relationship
+        /// </remarks>
+        [ForeignKey("CollectionItem_Next_Id")]
+        public virtual CollectionItem Next { get; set; }
+
+        /// <remarks>
+        /// TODO check if this properly updated dependant and has the proper principal relationship
+        /// </remarks>
+        [ForeignKey("CollectionItem_Previous_Id")]
+        public virtual CollectionItem Previous { get; set; }
+
+    }
+}
+

+ 137 - 0
Jellyfin.Data/Entities/Company.cs

@@ -0,0 +1,137 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+    public partial class Company
+    {
+        partial void Init();
+
+        /// <summary>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </summary>
+        protected Company()
+        {
+            CompanyMetadata = new HashSet<CompanyMetadata>();
+
+            Init();
+        }
+
+        /// <summary>
+        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+        /// </summary>
+        public static Company CreateCompanyUnsafe()
+        {
+            return new Company();
+        }
+
+        /// <summary>
+        /// Public constructor with required data
+        /// </summary>
+        /// <param name="_moviemetadata0"></param>
+        /// <param name="_seriesmetadata1"></param>
+        /// <param name="_musicalbummetadata2"></param>
+        /// <param name="_bookmetadata3"></param>
+        /// <param name="_company4"></param>
+        public Company(MovieMetadata _moviemetadata0, SeriesMetadata _seriesmetadata1, MusicAlbumMetadata _musicalbummetadata2, BookMetadata _bookmetadata3, Company _company4)
+        {
+            if (_moviemetadata0 == null) throw new ArgumentNullException(nameof(_moviemetadata0));
+            _moviemetadata0.Studios.Add(this);
+
+            if (_seriesmetadata1 == null) throw new ArgumentNullException(nameof(_seriesmetadata1));
+            _seriesmetadata1.Networks.Add(this);
+
+            if (_musicalbummetadata2 == null) throw new ArgumentNullException(nameof(_musicalbummetadata2));
+            _musicalbummetadata2.Labels.Add(this);
+
+            if (_bookmetadata3 == null) throw new ArgumentNullException(nameof(_bookmetadata3));
+            _bookmetadata3.Publishers.Add(this);
+
+            if (_company4 == null) throw new ArgumentNullException(nameof(_company4));
+            _company4.Parent = this;
+
+            this.CompanyMetadata = new HashSet<CompanyMetadata>();
+
+            Init();
+        }
+
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </summary>
+        /// <param name="_moviemetadata0"></param>
+        /// <param name="_seriesmetadata1"></param>
+        /// <param name="_musicalbummetadata2"></param>
+        /// <param name="_bookmetadata3"></param>
+        /// <param name="_company4"></param>
+        public static Company Create(MovieMetadata _moviemetadata0, SeriesMetadata _seriesmetadata1, MusicAlbumMetadata _musicalbummetadata2, BookMetadata _bookmetadata3, Company _company4)
+        {
+            return new Company(_moviemetadata0, _seriesmetadata1, _musicalbummetadata2, _bookmetadata3, _company4);
+        }
+
+        /*************************************************************************
+         * Properties
+         *************************************************************************/
+
+        /// <summary>
+        /// Backing field for Id
+        /// </summary>
+        internal int _Id;
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before setting.
+        /// </summary>
+        partial void SetId(int oldValue, ref int newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before returning.
+        /// </summary>
+        partial void GetId(ref int result);
+
+        /// <summary>
+        /// Identity, Indexed, Required
+        /// </summary>
+        [Key]
+        [Required]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id
+        {
+            get
+            {
+                int value = _Id;
+                GetId(ref value);
+                return (_Id = value);
+            }
+            protected set
+            {
+                int oldValue = _Id;
+                SetId(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Id = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Required, ConcurrenyToken
+        /// </summary>
+        [ConcurrencyCheck]
+        [Required]
+        public uint RowVersion { get; set; }
+
+        public void OnSavingChanges()
+        {
+            RowVersion++;
+        }
+
+        /*************************************************************************
+         * Navigation properties
+         *************************************************************************/
+        [ForeignKey("CompanyMetadata_CompanyMetadata_Id")]
+        public virtual ICollection<CompanyMetadata> CompanyMetadata { get; protected set; }
+        [ForeignKey("Company_Parent_Id")]
+        public virtual Company Parent { get; set; }
+
+    }
+}
+

+ 216 - 0
Jellyfin.Data/Entities/CompanyMetadata.cs

@@ -0,0 +1,216 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace Jellyfin.Data.Entities
+{
+    public partial class CompanyMetadata : Metadata
+    {
+        partial void Init();
+
+        /// <summary>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </summary>
+        protected CompanyMetadata()
+        {
+            Init();
+        }
+
+        /// <summary>
+        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+        /// </summary>
+        public static CompanyMetadata CreateCompanyMetadataUnsafe()
+        {
+            return new CompanyMetadata();
+        }
+
+        /// <summary>
+        /// Public constructor with required data
+        /// </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="_company0"></param>
+        public CompanyMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Company _company0)
+        {
+            if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title));
+            this.Title = title;
+
+            if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language));
+            this.Language = language;
+
+            if (_company0 == null) throw new ArgumentNullException(nameof(_company0));
+            _company0.CompanyMetadata.Add(this);
+
+
+            Init();
+        }
+
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </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="_company0"></param>
+        public static CompanyMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Company _company0)
+        {
+            return new CompanyMetadata(title, language, dateadded, datemodified, _company0);
+        }
+
+        /*************************************************************************
+         * Properties
+         *************************************************************************/
+
+        /// <summary>
+        /// Backing field for Description
+        /// </summary>
+        protected string _Description;
+        /// <summary>
+        /// When provided in a partial class, allows value of Description to be changed before setting.
+        /// </summary>
+        partial void SetDescription(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Description to be changed before returning.
+        /// </summary>
+        partial void GetDescription(ref string result);
+
+        /// <summary>
+        /// Max length = 65535
+        /// </summary>
+        [MaxLength(65535)]
+        [StringLength(65535)]
+        public string Description
+        {
+            get
+            {
+                string value = _Description;
+                GetDescription(ref value);
+                return (_Description = value);
+            }
+            set
+            {
+                string oldValue = _Description;
+                SetDescription(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Description = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for Headquarters
+        /// </summary>
+        protected string _Headquarters;
+        /// <summary>
+        /// When provided in a partial class, allows value of Headquarters to be changed before setting.
+        /// </summary>
+        partial void SetHeadquarters(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Headquarters to be changed before returning.
+        /// </summary>
+        partial void GetHeadquarters(ref string result);
+
+        /// <summary>
+        /// Max length = 255
+        /// </summary>
+        [MaxLength(255)]
+        [StringLength(255)]
+        public string Headquarters
+        {
+            get
+            {
+                string value = _Headquarters;
+                GetHeadquarters(ref value);
+                return (_Headquarters = value);
+            }
+            set
+            {
+                string oldValue = _Headquarters;
+                SetHeadquarters(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Headquarters = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for Country
+        /// </summary>
+        protected string _Country;
+        /// <summary>
+        /// When provided in a partial class, allows value of Country to be changed before setting.
+        /// </summary>
+        partial void SetCountry(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Country to be changed before returning.
+        /// </summary>
+        partial void GetCountry(ref string result);
+
+        /// <summary>
+        /// Max length = 2
+        /// </summary>
+        [MaxLength(2)]
+        [StringLength(2)]
+        public string Country
+        {
+            get
+            {
+                string value = _Country;
+                GetCountry(ref value);
+                return (_Country = value);
+            }
+            set
+            {
+                string oldValue = _Country;
+                SetCountry(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Country = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for Homepage
+        /// </summary>
+        protected string _Homepage;
+        /// <summary>
+        /// When provided in a partial class, allows value of Homepage to be changed before setting.
+        /// </summary>
+        partial void SetHomepage(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Homepage to be changed before returning.
+        /// </summary>
+        partial void GetHomepage(ref string result);
+
+        /// <summary>
+        /// Max length = 1024
+        /// </summary>
+        [MaxLength(1024)]
+        [StringLength(1024)]
+        public string Homepage
+        {
+            get
+            {
+                string value = _Homepage;
+                GetHomepage(ref value);
+                return (_Homepage = value);
+            }
+            set
+            {
+                string oldValue = _Homepage;
+                SetHomepage(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Homepage = value;
+                }
+            }
+        }
+
+        /*************************************************************************
+         * Navigation properties
+         *************************************************************************/
+
+    }
+}
+

+ 68 - 0
Jellyfin.Data/Entities/CustomItem.cs

@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+    public partial class CustomItem : LibraryItem
+    {
+        partial void Init();
+
+        /// <summary>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </summary>
+        protected CustomItem()
+        {
+            CustomItemMetadata = new HashSet<CustomItemMetadata>();
+            Releases = new HashSet<Release>();
+
+            Init();
+        }
+
+        /// <summary>
+        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+        /// </summary>
+        public static CustomItem CreateCustomItemUnsafe()
+        {
+            return new CustomItem();
+        }
+
+        /// <summary>
+        /// Public constructor with required data
+        /// </summary>
+        /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+        public CustomItem(Guid urlid, DateTime dateadded)
+        {
+            this.UrlId = urlid;
+
+            this.CustomItemMetadata = new HashSet<CustomItemMetadata>();
+            this.Releases = new HashSet<Release>();
+
+            Init();
+        }
+
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </summary>
+        /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+        public static CustomItem Create(Guid urlid, DateTime dateadded)
+        {
+            return new CustomItem(urlid, dateadded);
+        }
+
+        /*************************************************************************
+         * Properties
+         *************************************************************************/
+
+        /*************************************************************************
+         * Navigation properties
+         *************************************************************************/
+        [ForeignKey("CustomItemMetadata_CustomItemMetadata_Id")]
+        public virtual ICollection<CustomItemMetadata> CustomItemMetadata { get; protected set; }
+
+        [ForeignKey("Release_Releases_Id")]
+        public virtual ICollection<Release> Releases { get; protected set; }
+
+    }
+}
+

+ 67 - 0
Jellyfin.Data/Entities/CustomItemMetadata.cs

@@ -0,0 +1,67 @@
+using System;
+
+namespace Jellyfin.Data.Entities
+{
+    public partial class CustomItemMetadata : Metadata
+    {
+        partial void Init();
+
+        /// <summary>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </summary>
+        protected CustomItemMetadata()
+        {
+            Init();
+        }
+
+        /// <summary>
+        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+        /// </summary>
+        public static CustomItemMetadata CreateCustomItemMetadataUnsafe()
+        {
+            return new CustomItemMetadata();
+        }
+
+        /// <summary>
+        /// Public constructor with required data
+        /// </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="_customitem0"></param>
+        public CustomItemMetadata(string title, string language, DateTime dateadded, DateTime datemodified, CustomItem _customitem0)
+        {
+            if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title));
+            this.Title = title;
+
+            if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language));
+            this.Language = language;
+
+            if (_customitem0 == null) throw new ArgumentNullException(nameof(_customitem0));
+            _customitem0.CustomItemMetadata.Add(this);
+
+
+            Init();
+        }
+
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </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="_customitem0"></param>
+        public static CustomItemMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, CustomItem _customitem0)
+        {
+            return new CustomItemMetadata(title, language, dateadded, datemodified, _customitem0);
+        }
+
+        /*************************************************************************
+         * Properties
+         *************************************************************************/
+
+        /*************************************************************************
+         * Navigation properties
+         *************************************************************************/
+
+    }
+}
+

+ 110 - 0
Jellyfin.Data/Entities/Episode.cs

@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+    public partial class Episode : LibraryItem
+    {
+        partial void Init();
+
+        /// <summary>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </summary>
+        protected Episode()
+        {
+            // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem.
+            // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other.
+
+            Releases = new HashSet<Release>();
+            EpisodeMetadata = new HashSet<EpisodeMetadata>();
+
+            Init();
+        }
+
+        /// <summary>
+        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+        /// </summary>
+        public static Episode CreateEpisodeUnsafe()
+        {
+            return new Episode();
+        }
+
+        /// <summary>
+        /// Public constructor with required data
+        /// </summary>
+        /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+        /// <param name="_season0"></param>
+        public Episode(Guid urlid, DateTime dateadded, Season _season0)
+        {
+            // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem.
+            // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other.
+
+            this.UrlId = urlid;
+
+            if (_season0 == null) throw new ArgumentNullException(nameof(_season0));
+            _season0.Episodes.Add(this);
+
+            this.Releases = new HashSet<Release>();
+            this.EpisodeMetadata = new HashSet<EpisodeMetadata>();
+
+            Init();
+        }
+
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </summary>
+        /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+        /// <param name="_season0"></param>
+        public static Episode Create(Guid urlid, DateTime dateadded, Season _season0)
+        {
+            return new Episode(urlid, dateadded, _season0);
+        }
+
+        /*************************************************************************
+         * Properties
+         *************************************************************************/
+
+        /// <summary>
+        /// Backing field for EpisodeNumber
+        /// </summary>
+        protected int? _EpisodeNumber;
+        /// <summary>
+        /// When provided in a partial class, allows value of EpisodeNumber to be changed before setting.
+        /// </summary>
+        partial void SetEpisodeNumber(int? oldValue, ref int? newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of EpisodeNumber to be changed before returning.
+        /// </summary>
+        partial void GetEpisodeNumber(ref int? result);
+
+        public int? EpisodeNumber
+        {
+            get
+            {
+                int? value = _EpisodeNumber;
+                GetEpisodeNumber(ref value);
+                return (_EpisodeNumber = value);
+            }
+            set
+            {
+                int? oldValue = _EpisodeNumber;
+                SetEpisodeNumber(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _EpisodeNumber = value;
+                }
+            }
+        }
+
+        /*************************************************************************
+         * Navigation properties
+         *************************************************************************/
+        [ForeignKey("Release_Releases_Id")]
+        public virtual ICollection<Release> Releases { get; protected set; }
+        [ForeignKey("EpisodeMetadata_EpisodeMetadata_Id")]
+        public virtual ICollection<EpisodeMetadata> EpisodeMetadata { get; protected set; }
+
+    }
+}
+

+ 179 - 0
Jellyfin.Data/Entities/EpisodeMetadata.cs

@@ -0,0 +1,179 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace Jellyfin.Data.Entities
+{
+    public partial class EpisodeMetadata : Metadata
+    {
+        partial void Init();
+
+        /// <summary>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </summary>
+        protected EpisodeMetadata()
+        {
+            Init();
+        }
+
+        /// <summary>
+        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+        /// </summary>
+        public static EpisodeMetadata CreateEpisodeMetadataUnsafe()
+        {
+            return new EpisodeMetadata();
+        }
+
+        /// <summary>
+        /// Public constructor with required data
+        /// </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="_episode0"></param>
+        public EpisodeMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Episode _episode0)
+        {
+            if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title));
+            this.Title = title;
+
+            if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language));
+            this.Language = language;
+
+            if (_episode0 == null) throw new ArgumentNullException(nameof(_episode0));
+            _episode0.EpisodeMetadata.Add(this);
+
+
+            Init();
+        }
+
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </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="_episode0"></param>
+        public static EpisodeMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Episode _episode0)
+        {
+            return new EpisodeMetadata(title, language, dateadded, datemodified, _episode0);
+        }
+
+        /*************************************************************************
+         * Properties
+         *************************************************************************/
+
+        /// <summary>
+        /// Backing field for Outline
+        /// </summary>
+        protected string _Outline;
+        /// <summary>
+        /// When provided in a partial class, allows value of Outline to be changed before setting.
+        /// </summary>
+        partial void SetOutline(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Outline to be changed before returning.
+        /// </summary>
+        partial void GetOutline(ref string result);
+
+        /// <summary>
+        /// Max length = 1024
+        /// </summary>
+        [MaxLength(1024)]
+        [StringLength(1024)]
+        public string Outline
+        {
+            get
+            {
+                string value = _Outline;
+                GetOutline(ref value);
+                return (_Outline = value);
+            }
+            set
+            {
+                string oldValue = _Outline;
+                SetOutline(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Outline = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for Plot
+        /// </summary>
+        protected string _Plot;
+        /// <summary>
+        /// When provided in a partial class, allows value of Plot to be changed before setting.
+        /// </summary>
+        partial void SetPlot(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Plot to be changed before returning.
+        /// </summary>
+        partial void GetPlot(ref string result);
+
+        /// <summary>
+        /// Max length = 65535
+        /// </summary>
+        [MaxLength(65535)]
+        [StringLength(65535)]
+        public string Plot
+        {
+            get
+            {
+                string value = _Plot;
+                GetPlot(ref value);
+                return (_Plot = value);
+            }
+            set
+            {
+                string oldValue = _Plot;
+                SetPlot(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Plot = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for Tagline
+        /// </summary>
+        protected string _Tagline;
+        /// <summary>
+        /// When provided in a partial class, allows value of Tagline to be changed before setting.
+        /// </summary>
+        partial void SetTagline(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Tagline to be changed before returning.
+        /// </summary>
+        partial void GetTagline(ref string result);
+
+        /// <summary>
+        /// Max length = 1024
+        /// </summary>
+        [MaxLength(1024)]
+        [StringLength(1024)]
+        public string Tagline
+        {
+            get
+            {
+                string value = _Tagline;
+                GetTagline(ref value);
+                return (_Tagline = value);
+            }
+            set
+            {
+                string oldValue = _Tagline;
+                SetTagline(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Tagline = value;
+                }
+            }
+        }
+
+        /*************************************************************************
+         * Navigation properties
+         *************************************************************************/
+
+    }
+}
+

+ 152 - 0
Jellyfin.Data/Entities/Genre.cs

@@ -0,0 +1,152 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+    public partial class Genre
+    {
+        partial void Init();
+
+        /// <summary>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </summary>
+        protected Genre()
+        {
+            Init();
+        }
+
+        /// <summary>
+        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+        /// </summary>
+        public static Genre CreateGenreUnsafe()
+        {
+            return new Genre();
+        }
+
+        /// <summary>
+        /// Public constructor with required data
+        /// </summary>
+        /// <param name="name"></param>
+        /// <param name="_metadata0"></param>
+        public Genre(string name, Metadata _metadata0)
+        {
+            if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name));
+            this.Name = name;
+
+            if (_metadata0 == null) throw new ArgumentNullException(nameof(_metadata0));
+            _metadata0.Genres.Add(this);
+
+
+            Init();
+        }
+
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </summary>
+        /// <param name="name"></param>
+        /// <param name="_metadata0"></param>
+        public static Genre Create(string name, Metadata _metadata0)
+        {
+            return new Genre(name, _metadata0);
+        }
+
+        /*************************************************************************
+         * Properties
+         *************************************************************************/
+
+        /// <summary>
+        /// Backing field for Id
+        /// </summary>
+        internal int _Id;
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before setting.
+        /// </summary>
+        partial void SetId(int oldValue, ref int newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before returning.
+        /// </summary>
+        partial void GetId(ref int result);
+
+        /// <summary>
+        /// Identity, Indexed, Required
+        /// </summary>
+        [Key]
+        [Required]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id
+        {
+            get
+            {
+                int value = _Id;
+                GetId(ref value);
+                return (_Id = value);
+            }
+            protected set
+            {
+                int oldValue = _Id;
+                SetId(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Id = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for Name
+        /// </summary>
+        internal string _Name;
+        /// <summary>
+        /// When provided in a partial class, allows value of Name to be changed before setting.
+        /// </summary>
+        partial void SetName(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Name to be changed before returning.
+        /// </summary>
+        partial void GetName(ref string result);
+
+        /// <summary>
+        /// Indexed, Required, Max length = 255
+        /// </summary>
+        [Required]
+        [MaxLength(255)]
+        [StringLength(255)]
+        public string Name
+        {
+            get
+            {
+                string value = _Name;
+                GetName(ref value);
+                return (_Name = value);
+            }
+            set
+            {
+                string oldValue = _Name;
+                SetName(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Name = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Required, ConcurrenyToken
+        /// </summary>
+        [ConcurrencyCheck]
+        [Required]
+        public uint RowVersion { get; set; }
+
+        public void OnSavingChanges()
+        {
+            RowVersion++;
+        }
+
+        /*************************************************************************
+         * Navigation properties
+         *************************************************************************/
+
+    }
+}
+

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

@@ -0,0 +1,109 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+    public partial class Group
+    {
+        partial void Init();
+
+        /// <summary>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </summary>
+        protected Group()
+        {
+            GroupPermissions = new HashSet<Permission>();
+            ProviderMappings = new HashSet<ProviderMapping>();
+            Preferences = new HashSet<Preference>();
+
+            Init();
+        }
+
+        /// <summary>
+        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+        /// </summary>
+        public static Group CreateGroupUnsafe()
+        {
+            return new Group();
+        }
+
+        /// <summary>
+        /// Public constructor with required data
+        /// </summary>
+        /// <param name="name"></param>
+        /// <param name="_user0"></param>
+        public Group(string name, User _user0)
+        {
+            if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name));
+            this.Name = name;
+
+            if (_user0 == null) throw new ArgumentNullException(nameof(_user0));
+            _user0.Groups.Add(this);
+
+            this.GroupPermissions = new HashSet<Permission>();
+            this.ProviderMappings = new HashSet<ProviderMapping>();
+            this.Preferences = new HashSet<Preference>();
+
+            Init();
+        }
+
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </summary>
+        /// <param name="name"></param>
+        /// <param name="_user0"></param>
+        public static Group Create(string name, User _user0)
+        {
+            return new Group(name, _user0);
+        }
+
+        /*************************************************************************
+         * Properties
+         *************************************************************************/
+
+        /// <summary>
+        /// Identity, Indexed, Required
+        /// </summary>
+        [Key]
+        [Required]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id { get; protected set; }
+
+        /// <summary>
+        /// Required, Max length = 255
+        /// </summary>
+        [Required]
+        [MaxLength(255)]
+        [StringLength(255)]
+        public string Name { get; set; }
+
+        /// <summary>
+        /// Required, ConcurrenyToken
+        /// </summary>
+        [ConcurrencyCheck]
+        [Required]
+        public uint RowVersion { get; set; }
+
+        public void OnSavingChanges()
+        {
+            RowVersion++;
+        }
+
+        /*************************************************************************
+         * Navigation properties
+         *************************************************************************/
+
+        [ForeignKey("Permission_GroupPermissions_Id")]
+        public virtual ICollection<Permission> GroupPermissions { get; protected set; }
+
+        [ForeignKey("ProviderMapping_ProviderMappings_Id")]
+        public virtual ICollection<ProviderMapping> ProviderMappings { get; protected set; }
+
+        [ForeignKey("Preference_Preferences_Id")]
+        public virtual ICollection<Preference> Preferences { get; protected set; }
+
+    }
+}
+

+ 147 - 0
Jellyfin.Data/Entities/Library.cs

@@ -0,0 +1,147 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+    public partial class Library
+    {
+        partial void Init();
+
+        /// <summary>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </summary>
+        protected Library()
+        {
+            Init();
+        }
+
+        /// <summary>
+        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+        /// </summary>
+        public static Library CreateLibraryUnsafe()
+        {
+            return new Library();
+        }
+
+        /// <summary>
+        /// Public constructor with required data
+        /// </summary>
+        /// <param name="name"></param>
+        public Library(string name)
+        {
+            if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name));
+            this.Name = name;
+
+
+            Init();
+        }
+
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </summary>
+        /// <param name="name"></param>
+        public static Library Create(string name)
+        {
+            return new Library(name);
+        }
+
+        /*************************************************************************
+         * Properties
+         *************************************************************************/
+
+        /// <summary>
+        /// Backing field for Id
+        /// </summary>
+        internal int _Id;
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before setting.
+        /// </summary>
+        partial void SetId(int oldValue, ref int newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before returning.
+        /// </summary>
+        partial void GetId(ref int result);
+
+        /// <summary>
+        /// Identity, Indexed, Required
+        /// </summary>
+        [Key]
+        [Required]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id
+        {
+            get
+            {
+                int value = _Id;
+                GetId(ref value);
+                return (_Id = value);
+            }
+            protected set
+            {
+                int oldValue = _Id;
+                SetId(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Id = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for Name
+        /// </summary>
+        protected string _Name;
+        /// <summary>
+        /// When provided in a partial class, allows value of Name to be changed before setting.
+        /// </summary>
+        partial void SetName(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Name to be changed before returning.
+        /// </summary>
+        partial void GetName(ref string result);
+
+        /// <summary>
+        /// Required, Max length = 1024
+        /// </summary>
+        [Required]
+        [MaxLength(1024)]
+        [StringLength(1024)]
+        public string Name
+        {
+            get
+            {
+                string value = _Name;
+                GetName(ref value);
+                return (_Name = value);
+            }
+            set
+            {
+                string oldValue = _Name;
+                SetName(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Name = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Required, ConcurrenyToken
+        /// </summary>
+        [ConcurrencyCheck]
+        [Required]
+        public uint RowVersion { get; set; }
+
+        public void OnSavingChanges()
+        {
+            RowVersion++;
+        }
+
+        /*************************************************************************
+         * Navigation properties
+         *************************************************************************/
+
+    }
+}
+

+ 170 - 0
Jellyfin.Data/Entities/LibraryItem.cs

@@ -0,0 +1,170 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+    public abstract partial class LibraryItem
+    {
+        partial void Init();
+
+        /// <summary>
+        /// Default constructor. Protected due to being abstract.
+        /// </summary>
+        protected LibraryItem()
+        {
+            Init();
+        }
+
+        /// <summary>
+        /// Public constructor with required data
+        /// </summary>
+        /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+        protected LibraryItem(Guid urlid, DateTime dateadded)
+        {
+            this.UrlId = urlid;
+
+
+            Init();
+        }
+
+        /*************************************************************************
+         * Properties
+         *************************************************************************/
+
+        /// <summary>
+        /// Backing field for Id
+        /// </summary>
+        internal int _Id;
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before setting.
+        /// </summary>
+        partial void SetId(int oldValue, ref int newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before returning.
+        /// </summary>
+        partial void GetId(ref int result);
+
+        /// <summary>
+        /// Identity, Indexed, Required
+        /// </summary>
+        [Key]
+        [Required]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id
+        {
+            get
+            {
+                int value = _Id;
+                GetId(ref value);
+                return (_Id = value);
+            }
+            protected set
+            {
+                int oldValue = _Id;
+                SetId(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Id = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for UrlId
+        /// </summary>
+        internal Guid _UrlId;
+        /// <summary>
+        /// When provided in a partial class, allows value of UrlId to be changed before setting.
+        /// </summary>
+        partial void SetUrlId(Guid oldValue, ref Guid newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of UrlId to be changed before returning.
+        /// </summary>
+        partial void GetUrlId(ref Guid result);
+
+        /// <summary>
+        /// Indexed, Required
+        /// This is whats gets displayed in the Urls and API requests. This could also be a string.
+        /// </summary>
+        [Required]
+        public Guid UrlId
+        {
+            get
+            {
+                Guid value = _UrlId;
+                GetUrlId(ref value);
+                return (_UrlId = value);
+            }
+            set
+            {
+                Guid oldValue = _UrlId;
+                SetUrlId(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _UrlId = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for DateAdded
+        /// </summary>
+        protected DateTime _DateAdded;
+        /// <summary>
+        /// When provided in a partial class, allows value of DateAdded to be changed before setting.
+        /// </summary>
+        partial void SetDateAdded(DateTime oldValue, ref DateTime newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of DateAdded to be changed before returning.
+        /// </summary>
+        partial void GetDateAdded(ref DateTime result);
+
+        /// <summary>
+        /// Required
+        /// </summary>
+        [Required]
+        public DateTime DateAdded
+        {
+            get
+            {
+                DateTime value = _DateAdded;
+                GetDateAdded(ref value);
+                return (_DateAdded = value);
+            }
+            internal set
+            {
+                DateTime oldValue = _DateAdded;
+                SetDateAdded(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _DateAdded = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Required, ConcurrenyToken
+        /// </summary>
+        [ConcurrencyCheck]
+        [Required]
+        public uint RowVersion { get; set; }
+
+        public void OnSavingChanges()
+        {
+            RowVersion++;
+        }
+
+        /*************************************************************************
+         * Navigation properties
+         *************************************************************************/
+
+        /// <summary>
+        /// Required
+        /// </summary>
+        [ForeignKey("LibraryRoot_Id")]
+        public virtual LibraryRoot LibraryRoot { get; set; }
+
+    }
+}
+

+ 192 - 0
Jellyfin.Data/Entities/LibraryRoot.cs

@@ -0,0 +1,192 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+    public partial class LibraryRoot
+    {
+        partial void Init();
+
+        /// <summary>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </summary>
+        protected LibraryRoot()
+        {
+            Init();
+        }
+
+        /// <summary>
+        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+        /// </summary>
+        public static LibraryRoot CreateLibraryRootUnsafe()
+        {
+            return new LibraryRoot();
+        }
+
+        /// <summary>
+        /// Public constructor with required data
+        /// </summary>
+        /// <param name="path">Absolute Path</param>
+        public LibraryRoot(string path)
+        {
+            if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path));
+            this.Path = path;
+
+
+            Init();
+        }
+
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </summary>
+        /// <param name="path">Absolute Path</param>
+        public static LibraryRoot Create(string path)
+        {
+            return new LibraryRoot(path);
+        }
+
+        /*************************************************************************
+         * Properties
+         *************************************************************************/
+
+        /// <summary>
+        /// Backing field for Id
+        /// </summary>
+        internal int _Id;
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before setting.
+        /// </summary>
+        partial void SetId(int oldValue, ref int newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before returning.
+        /// </summary>
+        partial void GetId(ref int result);
+
+        /// <summary>
+        /// Identity, Indexed, Required
+        /// </summary>
+        [Key]
+        [Required]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id
+        {
+            get
+            {
+                int value = _Id;
+                GetId(ref value);
+                return (_Id = value);
+            }
+            protected set
+            {
+                int oldValue = _Id;
+                SetId(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Id = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for Path
+        /// </summary>
+        protected string _Path;
+        /// <summary>
+        /// When provided in a partial class, allows value of Path to be changed before setting.
+        /// </summary>
+        partial void SetPath(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Path to be changed before returning.
+        /// </summary>
+        partial void GetPath(ref string result);
+
+        /// <summary>
+        /// Required, Max length = 65535
+        /// Absolute Path
+        /// </summary>
+        [Required]
+        [MaxLength(65535)]
+        [StringLength(65535)]
+        public string Path
+        {
+            get
+            {
+                string value = _Path;
+                GetPath(ref value);
+                return (_Path = value);
+            }
+            set
+            {
+                string oldValue = _Path;
+                SetPath(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Path = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for NetworkPath
+        /// </summary>
+        protected string _NetworkPath;
+        /// <summary>
+        /// When provided in a partial class, allows value of NetworkPath to be changed before setting.
+        /// </summary>
+        partial void SetNetworkPath(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of NetworkPath to be changed before returning.
+        /// </summary>
+        partial void GetNetworkPath(ref string result);
+
+        /// <summary>
+        /// Max length = 65535
+        /// Absolute network path, for example for transcoding sattelites.
+        /// </summary>
+        [MaxLength(65535)]
+        [StringLength(65535)]
+        public string NetworkPath
+        {
+            get
+            {
+                string value = _NetworkPath;
+                GetNetworkPath(ref value);
+                return (_NetworkPath = value);
+            }
+            set
+            {
+                string oldValue = _NetworkPath;
+                SetNetworkPath(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _NetworkPath = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Required, ConcurrenyToken
+        /// </summary>
+        [ConcurrencyCheck]
+        [Required]
+        public uint RowVersion { get; set; }
+
+        public void OnSavingChanges()
+        {
+            RowVersion++;
+        }
+
+        /*************************************************************************
+         * Navigation properties
+         *************************************************************************/
+
+        /// <summary>
+        /// Required
+        /// </summary>
+        [ForeignKey("Library_Id")]
+        public virtual Library Library { get; set; }
+
+    }
+}
+

+ 200 - 0
Jellyfin.Data/Entities/MediaFile.cs

@@ -0,0 +1,200 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+    public partial class MediaFile
+    {
+        partial void Init();
+
+        /// <summary>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </summary>
+        protected MediaFile()
+        {
+            MediaFileStreams = new HashSet<MediaFileStream>();
+
+            Init();
+        }
+
+        /// <summary>
+        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+        /// </summary>
+        public static MediaFile CreateMediaFileUnsafe()
+        {
+            return new MediaFile();
+        }
+
+        /// <summary>
+        /// Public constructor with required data
+        /// </summary>
+        /// <param name="path">Relative to the LibraryRoot</param>
+        /// <param name="kind"></param>
+        /// <param name="_release0"></param>
+        public MediaFile(string path, Enums.MediaFileKind kind, Release _release0)
+        {
+            if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path));
+            this.Path = path;
+
+            this.Kind = kind;
+
+            if (_release0 == null) throw new ArgumentNullException(nameof(_release0));
+            _release0.MediaFiles.Add(this);
+
+            this.MediaFileStreams = new HashSet<MediaFileStream>();
+
+            Init();
+        }
+
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </summary>
+        /// <param name="path">Relative to the LibraryRoot</param>
+        /// <param name="kind"></param>
+        /// <param name="_release0"></param>
+        public static MediaFile Create(string path, Enums.MediaFileKind kind, Release _release0)
+        {
+            return new MediaFile(path, kind, _release0);
+        }
+
+        /*************************************************************************
+         * Properties
+         *************************************************************************/
+
+        /// <summary>
+        /// Backing field for Id
+        /// </summary>
+        internal int _Id;
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before setting.
+        /// </summary>
+        partial void SetId(int oldValue, ref int newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before returning.
+        /// </summary>
+        partial void GetId(ref int result);
+
+        /// <summary>
+        /// Identity, Indexed, Required
+        /// </summary>
+        [Key]
+        [Required]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id
+        {
+            get
+            {
+                int value = _Id;
+                GetId(ref value);
+                return (_Id = value);
+            }
+            protected set
+            {
+                int oldValue = _Id;
+                SetId(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Id = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for Path
+        /// </summary>
+        protected string _Path;
+        /// <summary>
+        /// When provided in a partial class, allows value of Path to be changed before setting.
+        /// </summary>
+        partial void SetPath(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Path to be changed before returning.
+        /// </summary>
+        partial void GetPath(ref string result);
+
+        /// <summary>
+        /// Required, Max length = 65535
+        /// Relative to the LibraryRoot
+        /// </summary>
+        [Required]
+        [MaxLength(65535)]
+        [StringLength(65535)]
+        public string Path
+        {
+            get
+            {
+                string value = _Path;
+                GetPath(ref value);
+                return (_Path = value);
+            }
+            set
+            {
+                string oldValue = _Path;
+                SetPath(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Path = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for Kind
+        /// </summary>
+        protected Enums.MediaFileKind _Kind;
+        /// <summary>
+        /// When provided in a partial class, allows value of Kind to be changed before setting.
+        /// </summary>
+        partial void SetKind(Enums.MediaFileKind oldValue, ref Enums.MediaFileKind newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Kind to be changed before returning.
+        /// </summary>
+        partial void GetKind(ref Enums.MediaFileKind result);
+
+        /// <summary>
+        /// Required
+        /// </summary>
+        [Required]
+        public Enums.MediaFileKind Kind
+        {
+            get
+            {
+                Enums.MediaFileKind value = _Kind;
+                GetKind(ref value);
+                return (_Kind = value);
+            }
+            set
+            {
+                Enums.MediaFileKind oldValue = _Kind;
+                SetKind(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Kind = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Required, ConcurrenyToken
+        /// </summary>
+        [ConcurrencyCheck]
+        [Required]
+        public uint RowVersion { get; set; }
+
+        public void OnSavingChanges()
+        {
+            RowVersion++;
+        }
+
+        /*************************************************************************
+         * Navigation properties
+         *************************************************************************/
+
+        [ForeignKey("MediaFileStream_MediaFileStreams_Id")]
+        public virtual ICollection<MediaFileStream> MediaFileStreams { get; protected set; }
+
+    }
+}
+

+ 149 - 0
Jellyfin.Data/Entities/MediaFileStream.cs

@@ -0,0 +1,149 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+    public partial class MediaFileStream
+    {
+        partial void Init();
+
+        /// <summary>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </summary>
+        protected MediaFileStream()
+        {
+            Init();
+        }
+
+        /// <summary>
+        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+        /// </summary>
+        public static MediaFileStream CreateMediaFileStreamUnsafe()
+        {
+            return new MediaFileStream();
+        }
+
+        /// <summary>
+        /// Public constructor with required data
+        /// </summary>
+        /// <param name="streamnumber"></param>
+        /// <param name="_mediafile0"></param>
+        public MediaFileStream(int streamnumber, MediaFile _mediafile0)
+        {
+            this.StreamNumber = streamnumber;
+
+            if (_mediafile0 == null) throw new ArgumentNullException(nameof(_mediafile0));
+            _mediafile0.MediaFileStreams.Add(this);
+
+
+            Init();
+        }
+
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </summary>
+        /// <param name="streamnumber"></param>
+        /// <param name="_mediafile0"></param>
+        public static MediaFileStream Create(int streamnumber, MediaFile _mediafile0)
+        {
+            return new MediaFileStream(streamnumber, _mediafile0);
+        }
+
+        /*************************************************************************
+         * Properties
+         *************************************************************************/
+
+        /// <summary>
+        /// Backing field for Id
+        /// </summary>
+        internal int _Id;
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before setting.
+        /// </summary>
+        partial void SetId(int oldValue, ref int newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before returning.
+        /// </summary>
+        partial void GetId(ref int result);
+
+        /// <summary>
+        /// Identity, Indexed, Required
+        /// </summary>
+        [Key]
+        [Required]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id
+        {
+            get
+            {
+                int value = _Id;
+                GetId(ref value);
+                return (_Id = value);
+            }
+            protected set
+            {
+                int oldValue = _Id;
+                SetId(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Id = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for StreamNumber
+        /// </summary>
+        protected int _StreamNumber;
+        /// <summary>
+        /// When provided in a partial class, allows value of StreamNumber to be changed before setting.
+        /// </summary>
+        partial void SetStreamNumber(int oldValue, ref int newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of StreamNumber to be changed before returning.
+        /// </summary>
+        partial void GetStreamNumber(ref int result);
+
+        /// <summary>
+        /// Required
+        /// </summary>
+        [Required]
+        public int StreamNumber
+        {
+            get
+            {
+                int value = _StreamNumber;
+                GetStreamNumber(ref value);
+                return (_StreamNumber = value);
+            }
+            set
+            {
+                int oldValue = _StreamNumber;
+                SetStreamNumber(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _StreamNumber = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Required, ConcurrenyToken
+        /// </summary>
+        [ConcurrencyCheck]
+        [Required]
+        public uint RowVersion { get; set; }
+
+        public void OnSavingChanges()
+        {
+            RowVersion++;
+        }
+
+        /*************************************************************************
+         * Navigation properties
+         *************************************************************************/
+
+    }
+}
+

+ 380 - 0
Jellyfin.Data/Entities/Metadata.cs

@@ -0,0 +1,380 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+    public abstract partial class Metadata
+    {
+        partial void Init();
+
+        /// <summary>
+        /// Default constructor. Protected due to being abstract.
+        /// </summary>
+        protected Metadata()
+        {
+            PersonRoles = new HashSet<PersonRole>();
+            Genres = new HashSet<Genre>();
+            Artwork = new HashSet<Artwork>();
+            Ratings = new HashSet<Rating>();
+            Sources = new HashSet<MetadataProviderId>();
+
+            Init();
+        }
+
+        /// <summary>
+        /// Public constructor with required data
+        /// </summary>
+        /// <param name="title">The title or name of the object</param>
+        /// <param name="language">ISO-639-3 3-character language codes</param>
+        protected Metadata(string title, string language, DateTime dateadded, DateTime datemodified)
+        {
+            if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title));
+            this.Title = title;
+
+            if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language));
+            this.Language = language;
+
+            this.PersonRoles = new HashSet<PersonRole>();
+            this.Genres = new HashSet<Genre>();
+            this.Artwork = new HashSet<Artwork>();
+            this.Ratings = new HashSet<Rating>();
+            this.Sources = new HashSet<MetadataProviderId>();
+
+            Init();
+        }
+
+        /*************************************************************************
+         * Properties
+         *************************************************************************/
+
+        /// <summary>
+        /// Backing field for Id
+        /// </summary>
+        internal int _Id;
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before setting.
+        /// </summary>
+        partial void SetId(int oldValue, ref int newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before returning.
+        /// </summary>
+        partial void GetId(ref int result);
+
+        /// <summary>
+        /// Identity, Indexed, Required
+        /// </summary>
+        [Key]
+        [Required]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id
+        {
+            get
+            {
+                int value = _Id;
+                GetId(ref value);
+                return (_Id = value);
+            }
+            protected set
+            {
+                int oldValue = _Id;
+                SetId(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Id = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for Title
+        /// </summary>
+        protected string _Title;
+        /// <summary>
+        /// When provided in a partial class, allows value of Title to be changed before setting.
+        /// </summary>
+        partial void SetTitle(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Title to be changed before returning.
+        /// </summary>
+        partial void GetTitle(ref string result);
+
+        /// <summary>
+        /// Required, Max length = 1024
+        /// The title or name of the object
+        /// </summary>
+        [Required]
+        [MaxLength(1024)]
+        [StringLength(1024)]
+        public string Title
+        {
+            get
+            {
+                string value = _Title;
+                GetTitle(ref value);
+                return (_Title = value);
+            }
+            set
+            {
+                string oldValue = _Title;
+                SetTitle(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Title = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for OriginalTitle
+        /// </summary>
+        protected string _OriginalTitle;
+        /// <summary>
+        /// When provided in a partial class, allows value of OriginalTitle to be changed before setting.
+        /// </summary>
+        partial void SetOriginalTitle(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of OriginalTitle to be changed before returning.
+        /// </summary>
+        partial void GetOriginalTitle(ref string result);
+
+        /// <summary>
+        /// Max length = 1024
+        /// </summary>
+        [MaxLength(1024)]
+        [StringLength(1024)]
+        public string OriginalTitle
+        {
+            get
+            {
+                string value = _OriginalTitle;
+                GetOriginalTitle(ref value);
+                return (_OriginalTitle = value);
+            }
+            set
+            {
+                string oldValue = _OriginalTitle;
+                SetOriginalTitle(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _OriginalTitle = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for SortTitle
+        /// </summary>
+        protected string _SortTitle;
+        /// <summary>
+        /// When provided in a partial class, allows value of SortTitle to be changed before setting.
+        /// </summary>
+        partial void SetSortTitle(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of SortTitle to be changed before returning.
+        /// </summary>
+        partial void GetSortTitle(ref string result);
+
+        /// <summary>
+        /// Max length = 1024
+        /// </summary>
+        [MaxLength(1024)]
+        [StringLength(1024)]
+        public string SortTitle
+        {
+            get
+            {
+                string value = _SortTitle;
+                GetSortTitle(ref value);
+                return (_SortTitle = value);
+            }
+            set
+            {
+                string oldValue = _SortTitle;
+                SetSortTitle(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _SortTitle = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for Language
+        /// </summary>
+        protected string _Language;
+        /// <summary>
+        /// When provided in a partial class, allows value of Language to be changed before setting.
+        /// </summary>
+        partial void SetLanguage(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Language to be changed before returning.
+        /// </summary>
+        partial void GetLanguage(ref string result);
+
+        /// <summary>
+        /// Required, Min length = 3, Max length = 3
+        /// ISO-639-3 3-character language codes
+        /// </summary>
+        [Required]
+        [MinLength(3)]
+        [MaxLength(3)]
+        [StringLength(3)]
+        public string Language
+        {
+            get
+            {
+                string value = _Language;
+                GetLanguage(ref value);
+                return (_Language = value);
+            }
+            set
+            {
+                string oldValue = _Language;
+                SetLanguage(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Language = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for ReleaseDate
+        /// </summary>
+        protected DateTimeOffset? _ReleaseDate;
+        /// <summary>
+        /// When provided in a partial class, allows value of ReleaseDate to be changed before setting.
+        /// </summary>
+        partial void SetReleaseDate(DateTimeOffset? oldValue, ref DateTimeOffset? newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of ReleaseDate to be changed before returning.
+        /// </summary>
+        partial void GetReleaseDate(ref DateTimeOffset? result);
+
+        public DateTimeOffset? ReleaseDate
+        {
+            get
+            {
+                DateTimeOffset? value = _ReleaseDate;
+                GetReleaseDate(ref value);
+                return (_ReleaseDate = value);
+            }
+            set
+            {
+                DateTimeOffset? oldValue = _ReleaseDate;
+                SetReleaseDate(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _ReleaseDate = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for DateAdded
+        /// </summary>
+        protected DateTime _DateAdded;
+        /// <summary>
+        /// When provided in a partial class, allows value of DateAdded to be changed before setting.
+        /// </summary>
+        partial void SetDateAdded(DateTime oldValue, ref DateTime newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of DateAdded to be changed before returning.
+        /// </summary>
+        partial void GetDateAdded(ref DateTime result);
+
+        /// <summary>
+        /// Required
+        /// </summary>
+        [Required]
+        public DateTime DateAdded
+        {
+            get
+            {
+                DateTime value = _DateAdded;
+                GetDateAdded(ref value);
+                return (_DateAdded = value);
+            }
+            internal set
+            {
+                DateTime oldValue = _DateAdded;
+                SetDateAdded(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _DateAdded = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for DateModified
+        /// </summary>
+        protected DateTime _DateModified;
+        /// <summary>
+        /// When provided in a partial class, allows value of DateModified to be changed before setting.
+        /// </summary>
+        partial void SetDateModified(DateTime oldValue, ref DateTime newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of DateModified to be changed before returning.
+        /// </summary>
+        partial void GetDateModified(ref DateTime result);
+
+        /// <summary>
+        /// Required
+        /// </summary>
+        [Required]
+        public DateTime DateModified
+        {
+            get
+            {
+                DateTime value = _DateModified;
+                GetDateModified(ref value);
+                return (_DateModified = value);
+            }
+            internal set
+            {
+                DateTime oldValue = _DateModified;
+                SetDateModified(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _DateModified = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Required, ConcurrenyToken
+        /// </summary>
+        [ConcurrencyCheck]
+        [Required]
+        public uint RowVersion { get; set; }
+
+        public void OnSavingChanges()
+        {
+            RowVersion++;
+        }
+
+        /*************************************************************************
+         * Navigation properties
+         *************************************************************************/
+
+        [ForeignKey("PersonRole_PersonRoles_Id")]
+        public virtual ICollection<PersonRole> PersonRoles { get; protected set; }
+
+        [ForeignKey("PersonRole_PersonRoles_Id")]
+        public virtual ICollection<Genre> Genres { get; protected set; }
+
+        [ForeignKey("PersonRole_PersonRoles_Id")]
+        public virtual ICollection<Artwork> Artwork { get; protected set; }
+
+        [ForeignKey("PersonRole_PersonRoles_Id")]
+        public virtual ICollection<Rating> Ratings { get; protected set; }
+
+        [ForeignKey("PersonRole_PersonRoles_Id")]
+        public virtual ICollection<MetadataProviderId> Sources { get; protected set; }
+
+    }
+}
+

+ 147 - 0
Jellyfin.Data/Entities/MetadataProvider.cs

@@ -0,0 +1,147 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+    public partial class MetadataProvider
+    {
+        partial void Init();
+
+        /// <summary>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </summary>
+        protected MetadataProvider()
+        {
+            Init();
+        }
+
+        /// <summary>
+        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+        /// </summary>
+        public static MetadataProvider CreateMetadataProviderUnsafe()
+        {
+            return new MetadataProvider();
+        }
+
+        /// <summary>
+        /// Public constructor with required data
+        /// </summary>
+        /// <param name="name"></param>
+        public MetadataProvider(string name)
+        {
+            if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name));
+            this.Name = name;
+
+
+            Init();
+        }
+
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </summary>
+        /// <param name="name"></param>
+        public static MetadataProvider Create(string name)
+        {
+            return new MetadataProvider(name);
+        }
+
+        /*************************************************************************
+         * Properties
+         *************************************************************************/
+
+        /// <summary>
+        /// Backing field for Id
+        /// </summary>
+        internal int _Id;
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before setting.
+        /// </summary>
+        partial void SetId(int oldValue, ref int newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before returning.
+        /// </summary>
+        partial void GetId(ref int result);
+
+        /// <summary>
+        /// Identity, Indexed, Required
+        /// </summary>
+        [Key]
+        [Required]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id
+        {
+            get
+            {
+                int value = _Id;
+                GetId(ref value);
+                return (_Id = value);
+            }
+            protected set
+            {
+                int oldValue = _Id;
+                SetId(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Id = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for Name
+        /// </summary>
+        protected string _Name;
+        /// <summary>
+        /// When provided in a partial class, allows value of Name to be changed before setting.
+        /// </summary>
+        partial void SetName(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Name to be changed before returning.
+        /// </summary>
+        partial void GetName(ref string result);
+
+        /// <summary>
+        /// Required, Max length = 1024
+        /// </summary>
+        [Required]
+        [MaxLength(1024)]
+        [StringLength(1024)]
+        public string Name
+        {
+            get
+            {
+                string value = _Name;
+                GetName(ref value);
+                return (_Name = value);
+            }
+            set
+            {
+                string oldValue = _Name;
+                SetName(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Name = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Required, ConcurrenyToken
+        /// </summary>
+        [ConcurrencyCheck]
+        [Required]
+        public uint RowVersion { get; set; }
+
+        public void OnSavingChanges()
+        {
+            RowVersion++;
+        }
+
+        /*************************************************************************
+         * Navigation properties
+         *************************************************************************/
+
+    }
+}
+

+ 179 - 0
Jellyfin.Data/Entities/MetadataProviderId.cs

@@ -0,0 +1,179 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+    public partial class MetadataProviderId
+    {
+        partial void Init();
+
+        /// <summary>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </summary>
+        protected MetadataProviderId()
+        {
+            // NOTE: This class has one-to-one associations with MetadataProviderId.
+            // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other.
+
+            Init();
+        }
+
+        /// <summary>
+        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+        /// </summary>
+        public static MetadataProviderId CreateMetadataProviderIdUnsafe()
+        {
+            return new MetadataProviderId();
+        }
+
+        /// <summary>
+        /// Public constructor with required data
+        /// </summary>
+        /// <param name="providerid"></param>
+        /// <param name="_metadata0"></param>
+        /// <param name="_person1"></param>
+        /// <param name="_personrole2"></param>
+        /// <param name="_ratingsource3"></param>
+        public MetadataProviderId(string providerid, Metadata _metadata0, Person _person1, PersonRole _personrole2, RatingSource _ratingsource3)
+        {
+            // NOTE: This class has one-to-one associations with MetadataProviderId.
+            // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other.
+
+            if (string.IsNullOrEmpty(providerid)) throw new ArgumentNullException(nameof(providerid));
+            this.ProviderId = providerid;
+
+            if (_metadata0 == null) throw new ArgumentNullException(nameof(_metadata0));
+            _metadata0.Sources.Add(this);
+
+            if (_person1 == null) throw new ArgumentNullException(nameof(_person1));
+            _person1.Sources.Add(this);
+
+            if (_personrole2 == null) throw new ArgumentNullException(nameof(_personrole2));
+            _personrole2.Sources.Add(this);
+
+            if (_ratingsource3 == null) throw new ArgumentNullException(nameof(_ratingsource3));
+            _ratingsource3.Source = this;
+
+
+            Init();
+        }
+
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </summary>
+        /// <param name="providerid"></param>
+        /// <param name="_metadata0"></param>
+        /// <param name="_person1"></param>
+        /// <param name="_personrole2"></param>
+        /// <param name="_ratingsource3"></param>
+        public static MetadataProviderId Create(string providerid, Metadata _metadata0, Person _person1, PersonRole _personrole2, RatingSource _ratingsource3)
+        {
+            return new MetadataProviderId(providerid, _metadata0, _person1, _personrole2, _ratingsource3);
+        }
+
+        /*************************************************************************
+         * Properties
+         *************************************************************************/
+
+        /// <summary>
+        /// Backing field for Id
+        /// </summary>
+        internal int _Id;
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before setting.
+        /// </summary>
+        partial void SetId(int oldValue, ref int newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Id to be changed before returning.
+        /// </summary>
+        partial void GetId(ref int result);
+
+        /// <summary>
+        /// Identity, Indexed, Required
+        /// </summary>
+        [Key]
+        [Required]
+        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+        public int Id
+        {
+            get
+            {
+                int value = _Id;
+                GetId(ref value);
+                return (_Id = value);
+            }
+            protected set
+            {
+                int oldValue = _Id;
+                SetId(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Id = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for ProviderId
+        /// </summary>
+        protected string _ProviderId;
+        /// <summary>
+        /// When provided in a partial class, allows value of ProviderId to be changed before setting.
+        /// </summary>
+        partial void SetProviderId(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of ProviderId to be changed before returning.
+        /// </summary>
+        partial void GetProviderId(ref string result);
+
+        /// <summary>
+        /// Required, Max length = 255
+        /// </summary>
+        [Required]
+        [MaxLength(255)]
+        [StringLength(255)]
+        public string ProviderId
+        {
+            get
+            {
+                string value = _ProviderId;
+                GetProviderId(ref value);
+                return (_ProviderId = value);
+            }
+            set
+            {
+                string oldValue = _ProviderId;
+                SetProviderId(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _ProviderId = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Required, ConcurrenyToken
+        /// </summary>
+        [ConcurrencyCheck]
+        [Required]
+        public uint RowVersion { get; set; }
+
+        public void OnSavingChanges()
+        {
+            RowVersion++;
+        }
+
+        /*************************************************************************
+         * Navigation properties
+         *************************************************************************/
+
+        /// <summary>
+        /// Required
+        /// </summary>
+        [ForeignKey("MetadataProvider_Id")]
+        public virtual MetadataProvider MetadataProvider { get; set; }
+
+    }
+}
+

+ 69 - 0
Jellyfin.Data/Entities/Movie.cs

@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+    public partial class Movie : LibraryItem
+    {
+        partial void Init();
+
+        /// <summary>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </summary>
+        protected Movie()
+        {
+            Releases = new HashSet<Release>();
+            MovieMetadata = new HashSet<MovieMetadata>();
+
+            Init();
+        }
+
+        /// <summary>
+        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+        /// </summary>
+        public static Movie CreateMovieUnsafe()
+        {
+            return new Movie();
+        }
+
+        /// <summary>
+        /// Public constructor with required data
+        /// </summary>
+        /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+        public Movie(Guid urlid, DateTime dateadded)
+        {
+            this.UrlId = urlid;
+
+            this.Releases = new HashSet<Release>();
+            this.MovieMetadata = new HashSet<MovieMetadata>();
+
+            Init();
+        }
+
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </summary>
+        /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param>
+        public static Movie Create(Guid urlid, DateTime dateadded)
+        {
+            return new Movie(urlid, dateadded);
+        }
+
+        /*************************************************************************
+         * Properties
+         *************************************************************************/
+
+        /*************************************************************************
+         * Navigation properties
+         *************************************************************************/
+
+        [ForeignKey("Release_Releases_Id")]
+        public virtual ICollection<Release> Releases { get; protected set; }
+
+        [ForeignKey("MovieMetadata_MovieMetadata_Id")]
+        public virtual ICollection<MovieMetadata> MovieMetadata { get; protected set; }
+
+    }
+}
+

+ 223 - 0
Jellyfin.Data/Entities/MovieMetadata.cs

@@ -0,0 +1,223 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Jellyfin.Data.Entities
+{
+    public partial class MovieMetadata : Metadata
+    {
+        partial void Init();
+
+        /// <summary>
+        /// Default constructor. Protected due to required properties, but present because EF needs it.
+        /// </summary>
+        protected MovieMetadata()
+        {
+            Studios = new HashSet<Company>();
+
+            Init();
+        }
+
+        /// <summary>
+        /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
+        /// </summary>
+        public static MovieMetadata CreateMovieMetadataUnsafe()
+        {
+            return new MovieMetadata();
+        }
+
+        /// <summary>
+        /// Public constructor with required data
+        /// </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="_movie0"></param>
+        public MovieMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Movie _movie0)
+        {
+            if (string.IsNullOrEmpty(title)) throw new ArgumentNullException(nameof(title));
+            this.Title = title;
+
+            if (string.IsNullOrEmpty(language)) throw new ArgumentNullException(nameof(language));
+            this.Language = language;
+
+            if (_movie0 == null) throw new ArgumentNullException(nameof(_movie0));
+            _movie0.MovieMetadata.Add(this);
+
+            this.Studios = new HashSet<Company>();
+
+            Init();
+        }
+
+        /// <summary>
+        /// Static create function (for use in LINQ queries, etc.)
+        /// </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="_movie0"></param>
+        public static MovieMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Movie _movie0)
+        {
+            return new MovieMetadata(title, language, dateadded, datemodified, _movie0);
+        }
+
+        /*************************************************************************
+         * Properties
+         *************************************************************************/
+
+        /// <summary>
+        /// Backing field for Outline
+        /// </summary>
+        protected string _Outline;
+        /// <summary>
+        /// When provided in a partial class, allows value of Outline to be changed before setting.
+        /// </summary>
+        partial void SetOutline(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Outline to be changed before returning.
+        /// </summary>
+        partial void GetOutline(ref string result);
+
+        /// <summary>
+        /// Max length = 1024
+        /// </summary>
+        [MaxLength(1024)]
+        [StringLength(1024)]
+        public string Outline
+        {
+            get
+            {
+                string value = _Outline;
+                GetOutline(ref value);
+                return (_Outline = value);
+            }
+            set
+            {
+                string oldValue = _Outline;
+                SetOutline(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Outline = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for Plot
+        /// </summary>
+        protected string _Plot;
+        /// <summary>
+        /// When provided in a partial class, allows value of Plot to be changed before setting.
+        /// </summary>
+        partial void SetPlot(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Plot to be changed before returning.
+        /// </summary>
+        partial void GetPlot(ref string result);
+
+        /// <summary>
+        /// Max length = 65535
+        /// </summary>
+        [MaxLength(65535)]
+        [StringLength(65535)]
+        public string Plot
+        {
+            get
+            {
+                string value = _Plot;
+                GetPlot(ref value);
+                return (_Plot = value);
+            }
+            set
+            {
+                string oldValue = _Plot;
+                SetPlot(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Plot = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for Tagline
+        /// </summary>
+        protected string _Tagline;
+        /// <summary>
+        /// When provided in a partial class, allows value of Tagline to be changed before setting.
+        /// </summary>
+        partial void SetTagline(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Tagline to be changed before returning.
+        /// </summary>
+        partial void GetTagline(ref string result);
+
+        /// <summary>
+        /// Max length = 1024
+        /// </summary>
+        [MaxLength(1024)]
+        [StringLength(1024)]
+        public string Tagline
+        {
+            get
+            {
+                string value = _Tagline;
+                GetTagline(ref value);
+                return (_Tagline = value);
+            }
+            set
+            {
+                string oldValue = _Tagline;
+                SetTagline(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Tagline = value;
+                }
+            }
+        }
+
+        /// <summary>
+        /// Backing field for Country
+        /// </summary>
+        protected string _Country;
+        /// <summary>
+        /// When provided in a partial class, allows value of Country to be changed before setting.
+        /// </summary>
+        partial void SetCountry(string oldValue, ref string newValue);
+        /// <summary>
+        /// When provided in a partial class, allows value of Country to be changed before returning.
+        /// </summary>
+        partial void GetCountry(ref string result);
+
+        /// <summary>
+        /// Max length = 2
+        /// </summary>
+        [MaxLength(2)]
+        [StringLength(2)]
+        public string Country
+        {
+            get
+            {
+                string value = _Country;
+                GetCountry(ref value);
+                return (_Country = value);
+            }
+            set
+            {
+                string oldValue = _Country;
+                SetCountry(oldValue, ref value);
+                if (oldValue != value)
+                {
+                    _Country = value;
+                }
+            }
+        }
+
+        /*************************************************************************
+         * Navigation properties
+         *************************************************************************/
+        [ForeignKey("Company_Studios_Id")]
+        public virtual ICollection<Company> Studios { get; protected set; }
+
+    }
+}
+

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