Pārlūkot izejas kodu

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

Greenback 4 gadi atpakaļ
vecāks
revīzija
ebe650afa9
100 mainītis faili ar 1136 papildinājumiem un 1228 dzēšanām
  1. 62 0
      .ci/azure-pipelines-api-client.yml
  2. 32 1
      .ci/azure-pipelines-package.yml
  3. 1 1
      .ci/azure-pipelines-test.yml
  4. 9 0
      .ci/azure-pipelines.yml
  5. 1 0
      .gitignore
  6. 1 0
      CONTRIBUTORS.md
  7. 4 4
      Emby.Dlna/PlayTo/PlayToController.cs
  8. 9 9
      Emby.Dlna/PlayTo/PlayToManager.cs
  9. 2 0
      Emby.Server.Implementations/ApplicationHost.cs
  10. 4 3
      Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
  11. 6 5
      Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
  12. 1 1
      Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
  13. 3 2
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  14. 1 1
      Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
  15. 22 14
      Emby.Server.Implementations/Session/SessionManager.cs
  16. 2 1
      Emby.Server.Implementations/Session/SessionWebSocketListener.cs
  17. 2 1
      Emby.Server.Implementations/Session/WebSocketController.cs
  18. 3 5
      Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
  19. 3 2
      Jellyfin.Api/Controllers/SessionController.cs
  20. 1 1
      Jellyfin.Api/Helpers/MediaInfoHelper.cs
  21. 1 4
      Jellyfin.Api/Helpers/TranscodingJobHelper.cs
  22. 64 0
      Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs
  23. 29 0
      Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinderProvider.cs
  24. 9 5
      Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
  25. 9 5
      Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
  26. 8 1
      Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
  27. 5 0
      Jellyfin.Data/Entities/User.cs
  28. 2 1
      Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedNotifier.cs
  29. 2 1
      Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationCancelledNotifier.cs
  30. 2 1
      Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedNotifier.cs
  31. 2 1
      Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledNotifier.cs
  32. 2 1
      Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallingNotifier.cs
  33. 2 1
      Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledNotifier.cs
  34. 2 1
      Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedNotifier.cs
  35. 2 1
      Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs
  36. 464 0
      Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs
  37. 28 0
      Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs
  38. 4 1
      Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
  39. 1 1
      Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs
  40. 3 1
      Jellyfin.Server.Implementations/Users/UserManager.cs
  41. 3 0
      Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
  42. 2 2
      Jellyfin.Server/Middleware/ExceptionMiddleware.cs
  43. 8 1
      Jellyfin.Server/Startup.cs
  44. 20 19
      MediaBrowser.Common/Json/Converters/JsonNullableStructConverter.cs
  45. 27 0
      MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs
  46. 1 6
      MediaBrowser.Common/Json/JsonDefaults.cs
  47. 20 7
      MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
  48. 2 1
      MediaBrowser.Controller/Session/ISessionController.cs
  49. 4 4
      MediaBrowser.Controller/Session/ISessionManager.cs
  50. 2 2
      MediaBrowser.Controller/Session/SessionInfo.cs
  51. 21 31
      MediaBrowser.Controller/SyncPlay/GroupInfo.cs
  52. 0 2
      MediaBrowser.Model/Configuration/LibraryOptions.cs
  53. 6 4
      MediaBrowser.Model/Dlna/StreamBuilder.cs
  54. 46 0
      MediaBrowser.Model/Extensions/EnumerableExtensions.cs
  55. 2 1
      MediaBrowser.Model/Net/WebSocketMessage.cs
  56. 2 2
      MediaBrowser.Model/Session/ClientCapabilities.cs
  57. 50 0
      MediaBrowser.Model/Session/SessionMessageType.cs
  58. 4 0
      MediaBrowser.Model/Users/UserPolicy.cs
  59. 8 0
      MediaBrowser.Providers/Manager/ProviderManager.cs
  60. 1 0
      MediaBrowser.Providers/MediaBrowser.Providers.csproj
  61. 0 26
      MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs
  62. 46 97
      MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
  63. 51 203
      MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
  64. 0 14
      MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionImages.cs
  65. 0 23
      MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionResult.cs
  66. 0 17
      MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/Part.cs
  67. 0 21
      MediaBrowser.Providers/Plugins/Tmdb/Models/General/Backdrop.cs
  68. 0 19
      MediaBrowser.Providers/Plugins/Tmdb/Models/General/Crew.cs
  69. 0 17
      MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs
  70. 0 11
      MediaBrowser.Providers/Plugins/Tmdb/Models/General/Genre.cs
  71. 0 13
      MediaBrowser.Providers/Plugins/Tmdb/Models/General/Images.cs
  72. 0 11
      MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keyword.cs
  73. 0 11
      MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keywords.cs
  74. 0 21
      MediaBrowser.Providers/Plugins/Tmdb/Models/General/Poster.cs
  75. 0 17
      MediaBrowser.Providers/Plugins/Tmdb/Models/General/Profile.cs
  76. 0 23
      MediaBrowser.Providers/Plugins/Tmdb/Models/General/Still.cs
  77. 0 11
      MediaBrowser.Providers/Plugins/Tmdb/Models/General/StillImages.cs
  78. 0 23
      MediaBrowser.Providers/Plugins/Tmdb/Models/General/Video.cs
  79. 0 11
      MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs
  80. 0 15
      MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/BelongsToCollection.cs
  81. 0 19
      MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Cast.cs
  82. 0 14
      MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Casts.cs
  83. 0 15
      MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Country.cs
  84. 0 80
      MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs
  85. 0 11
      MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCompany.cs
  86. 0 11
      MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCountry.cs
  87. 0 11
      MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Releases.cs
  88. 0 11
      MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/SpokenLanguage.cs
  89. 0 11
      MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs
  90. 0 13
      MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Youtube.cs
  91. 0 12
      MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs
  92. 0 38
      MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonResult.cs
  93. 0 11
      MediaBrowser.Providers/Plugins/Tmdb/Models/Search/ExternalIdLookupResult.cs
  94. 0 78
      MediaBrowser.Providers/Plugins/Tmdb/Models/Search/MovieResult.cs
  95. 0 31
      MediaBrowser.Providers/Plugins/Tmdb/Models/Search/PersonSearchResult.cs
  96. 0 33
      MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TmdbSearchResult.cs
  97. 0 25
      MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TvResult.cs
  98. 0 19
      MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Cast.cs
  99. 0 11
      MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRating.cs
  100. 0 11
      MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRatings.cs

+ 62 - 0
.ci/azure-pipelines-api-client.yml

@@ -0,0 +1,62 @@
+parameters:
+  - name: LinuxImage
+    type: string
+    default: "ubuntu-latest"
+  - name: GeneratorVersion
+    type: string
+    default: "5.0.0-beta2"
+
+jobs:
+- job: GenerateApiClients
+  displayName: 'Generate Api Clients'
+  dependsOn: Test
+
+  pool:
+    vmImage: "${{ parameters.LinuxImage }}"
+
+  steps:
+    - task: DownloadPipelineArtifact@2
+      displayName: 'Download OpenAPI Spec Artifact'
+      inputs:
+        source: 'current'
+        artifact: "OpenAPI Spec"
+        path: "$(System.ArtifactsDirectory)/openapispec"
+        runVersion: "latest"
+
+    - task: CmdLine@2
+      displayName: 'Download OpenApi Generator'
+      inputs:
+        script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar"
+
+# Generate npm api client
+# Unstable
+    - task: CmdLine@2
+      displayName: 'Build unstable typescript axios client'
+      condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
+      inputs:
+        script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory) $(Build.BuildNumber)"
+
+    - task: Npm@1
+      displayName: 'Publish unstable typescript axios client'
+      condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
+      inputs:
+        command: publish
+        publishRegistry: useFeed
+        publishFeed: 'unstable@Local'
+        workingDir: ./apiclient/generated/typescript/axios
+
+# Stable
+    - task: CmdLine@2
+      displayName: 'Build stable typescript axios client'
+      condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+      inputs:
+        script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)"
+
+    - task: Npm@1
+      displayName: 'Publish stable typescript axios client'
+      condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+      inputs:
+        command: publish
+        publishRegistry: useExternalRegistry
+        publishEndpoint: 'jellyfin-bot for NPM'
+        workingDir: ./apiclient/generated/typescript/axios

+ 32 - 1
.ci/azure-pipelines-package.yml

@@ -63,7 +63,38 @@ jobs:
       sshEndpoint: repository
       sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
       contents: '**'
-      targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
+
+- job: OpenAPISpec
+  dependsOn: Test
+  condition: or(startsWith(variables['Build.SourceBranch'], 'refs/heads/master'),startsWith(variables['Build.SourceBranch'], 'refs/tags/v'))
+  displayName: 'Push OpenAPI Spec to repository'
+
+  pool:
+    vmImage: 'ubuntu-latest'
+
+  steps:
+  - task: DownloadPipelineArtifact@2
+    displayName: 'Download OpenAPI Spec'
+    inputs:
+      source: 'current'
+      artifact: "OpenAPI Spec"
+      path: "$(System.ArtifactsDirectory)/openapispec"
+      runVersion: "latest"
+
+  - task: SSH@0
+    displayName: 'Create target directory on repository server'
+    inputs:
+      sshEndpoint: repository
+      runOptions: 'inline'
+      inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)'
+
+  - task: CopyFilesOverSSH@0
+    displayName: 'Upload artifacts to repository server'
+    inputs:
+      sshEndpoint: repository
+      sourceFolder: '$(System.ArtifactsDirectory)/openapispec'
+      contents: 'openapi.json'
+      targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)'
 
 - job: BuildDocker
   displayName: 'Build Docker'

+ 1 - 1
.ci/azure-pipelines-test.yml

@@ -56,7 +56,7 @@ jobs:
         inputs:
           command: "test"
           projects: ${{ parameters.TestProjects }}
-          arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal "-p:GenerateDocumentationFile=False"'
+          arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal'
           publishTestResults: true
           testRunTitle: $(Agent.JobName)
           workingDirectory: "$(Build.SourcesDirectory)"

+ 9 - 0
.ci/azure-pipelines.yml

@@ -34,6 +34,12 @@ jobs:
         Linux: 'ubuntu-latest'
         Windows: 'windows-latest'
         macOS: 'macos-latest'
+        
+- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
+  - template: azure-pipelines-test.yml
+    parameters:
+      ImageNames:
+        Linux: 'ubuntu-latest'
 
 - ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
   - template: azure-pipelines-abi.yml
@@ -55,3 +61,6 @@ jobs:
 
 - ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
   - template: azure-pipelines-package.yml
+
+- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
+  - template: azure-pipelines-api-client.yml

+ 1 - 0
.gitignore

@@ -276,3 +276,4 @@ BenchmarkDotNet.Artifacts
 web/
 web-src.*
 MediaBrowser.WebDashboard/jellyfin-web
+apiclient/generated

+ 1 - 0
CONTRIBUTORS.md

@@ -137,6 +137,7 @@
  - [KristupasSavickas](https://github.com/KristupasSavickas)
  - [Pusta](https://github.com/pusta)
  - [nielsvanvelzen](https://github.com/nielsvanvelzen)
+ - [skyfrk](https://github.com/skyfrk)
 
 # Emby Contributors
 

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

@@ -811,7 +811,7 @@ namespace Emby.Dlna.PlayTo
         }
 
         /// <inheritdoc />
-        public Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken)
+        public Task SendMessage<T>(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken)
         {
             if (_disposed)
             {
@@ -823,17 +823,17 @@ namespace Emby.Dlna.PlayTo
                 return Task.CompletedTask;
             }
 
-            if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
+            if (name == SessionMessageType.Play)
             {
                 return SendPlayCommand(data as PlayRequest, cancellationToken);
             }
 
-            if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
+            if (name == SessionMessageType.PlayState)
             {
                 return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
             }
 
-            if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
+            if (name == SessionMessageType.GeneralCommand)
             {
                 return SendGeneralCommand(data as GeneralCommand, cancellationToken);
             }

+ 9 - 9
Emby.Dlna/PlayTo/PlayToManager.cs

@@ -209,15 +209,15 @@ namespace Emby.Dlna.PlayTo
 
                     SupportedCommands = new[]
                     {
-                        GeneralCommandType.VolumeDown.ToString(),
-                        GeneralCommandType.VolumeUp.ToString(),
-                        GeneralCommandType.Mute.ToString(),
-                        GeneralCommandType.Unmute.ToString(),
-                        GeneralCommandType.ToggleMute.ToString(),
-                        GeneralCommandType.SetVolume.ToString(),
-                        GeneralCommandType.SetAudioStreamIndex.ToString(),
-                        GeneralCommandType.SetSubtitleStreamIndex.ToString(),
-                        GeneralCommandType.PlayMediaSource.ToString()
+                        GeneralCommandType.VolumeDown,
+                        GeneralCommandType.VolumeUp,
+                        GeneralCommandType.Mute,
+                        GeneralCommandType.Unmute,
+                        GeneralCommandType.ToggleMute,
+                        GeneralCommandType.SetVolume,
+                        GeneralCommandType.SetAudioStreamIndex,
+                        GeneralCommandType.SetSubtitleStreamIndex,
+                        GeneralCommandType.PlayMediaSource
                     },
 
                     SupportsMediaControl = true

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

@@ -101,6 +101,7 @@ using MediaBrowser.Model.Tasks;
 using MediaBrowser.Providers.Chapters;
 using MediaBrowser.Providers.Manager;
 using MediaBrowser.Providers.Plugins.TheTvdb;
+using MediaBrowser.Providers.Plugins.Tmdb;
 using MediaBrowser.Providers.Subtitles;
 using MediaBrowser.XbmcMetadata.Providers;
 using Microsoft.AspNetCore.DataProtection.Repositories;
@@ -549,6 +550,7 @@ namespace Emby.Server.Implementations
 
             ServiceCollection.AddSingleton(_fileSystemManager);
             ServiceCollection.AddSingleton<TvdbClientManager>();
+            ServiceCollection.AddSingleton<TmdbClientManager>();
 
             ServiceCollection.AddSingleton(NetManager);
 

+ 4 - 3
Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs

@@ -16,6 +16,7 @@ using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Session;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.EntryPoints
@@ -105,7 +106,7 @@ namespace Emby.Server.Implementations.EntryPoints
 
             try
             {
-                _sessionManager.SendMessageToAdminSessions("RefreshProgress", dict, CancellationToken.None);
+                _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, dict, CancellationToken.None);
             }
             catch
             {
@@ -123,7 +124,7 @@ namespace Emby.Server.Implementations.EntryPoints
 
                 try
                 {
-                    _sessionManager.SendMessageToAdminSessions("RefreshProgress", collectionFolderDict, CancellationToken.None);
+                    _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, collectionFolderDict, CancellationToken.None);
                 }
                 catch
                 {
@@ -345,7 +346,7 @@ namespace Emby.Server.Implementations.EntryPoints
 
                 try
                 {
-                    await _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, "LibraryChanged", info, cancellationToken).ConfigureAwait(false);
+                    await _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, SessionMessageType.LibraryChanged, info, cancellationToken).ConfigureAwait(false);
                 }
                 catch (Exception ex)
                 {

+ 6 - 5
Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs

@@ -10,6 +10,7 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.EntryPoints
@@ -46,25 +47,25 @@ namespace Emby.Server.Implementations.EntryPoints
 
         private async void OnLiveTvManagerSeriesTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
         {
-            await SendMessage("SeriesTimerCreated", e.Argument).ConfigureAwait(false);
+            await SendMessage(SessionMessageType.SeriesTimerCreated, e.Argument).ConfigureAwait(false);
         }
 
         private async void OnLiveTvManagerTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
         {
-            await SendMessage("TimerCreated", e.Argument).ConfigureAwait(false);
+            await SendMessage(SessionMessageType.TimerCreated, e.Argument).ConfigureAwait(false);
         }
 
         private async void OnLiveTvManagerSeriesTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
         {
-            await SendMessage("SeriesTimerCancelled", e.Argument).ConfigureAwait(false);
+            await SendMessage(SessionMessageType.SeriesTimerCancelled, e.Argument).ConfigureAwait(false);
         }
 
         private async void OnLiveTvManagerTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
         {
-            await SendMessage("TimerCancelled", e.Argument).ConfigureAwait(false);
+            await SendMessage(SessionMessageType.TimerCancelled, e.Argument).ConfigureAwait(false);
         }
 
-        private async Task SendMessage(string name, TimerEventInfo info)
+        private async Task SendMessage(SessionMessageType name, TimerEventInfo info)
         {
             var users = _userManager.Users.Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess)).Select(i => i.Id).ToList();
 

+ 1 - 1
Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs

@@ -115,7 +115,7 @@ namespace Emby.Server.Implementations.EntryPoints
 
         private Task SendNotifications(Guid userId, List<BaseItem> changedItems, CancellationToken cancellationToken)
         {
-            return _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, "UserDataChanged", () => GetUserDataChangeInfo(userId, changedItems), cancellationToken);
+            return _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, SessionMessageType.UserDataChanged, () => GetUserDataChangeInfo(userId, changedItems), cancellationToken);
         }
 
         private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, List<BaseItem> changedItems)

+ 3 - 2
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs

@@ -11,6 +11,7 @@ using System.Threading.Tasks;
 using MediaBrowser.Common.Json;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Session;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
 
@@ -227,7 +228,7 @@ namespace Emby.Server.Implementations.HttpServer
                 Connection = this
             };
 
-            if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal))
+            if (info.MessageType == SessionMessageType.KeepAlive)
             {
                 await SendKeepAliveResponse().ConfigureAwait(false);
             }
@@ -244,7 +245,7 @@ namespace Emby.Server.Implementations.HttpServer
                 new WebSocketMessage<string>
                 {
                     MessageId = Guid.NewGuid(),
-                    MessageType = "KeepAlive"
+                    MessageType = SessionMessageType.KeepAlive
                 }, CancellationToken.None);
         }
 

+ 1 - 1
Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs

@@ -148,7 +148,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
         public bool IsHidden => false;
 
         /// <inheritdoc />
-        public bool IsEnabled => false;
+        public bool IsEnabled => true;
 
         /// <inheritdoc />
         public bool IsLogged => true;

+ 22 - 14
Emby.Server.Implementations/Session/SessionManager.cs

@@ -1064,10 +1064,10 @@ namespace Emby.Server.Implementations.Session
                 AssertCanControl(session, controllingSession);
             }
 
-            return SendMessageToSession(session, "GeneralCommand", command, cancellationToken);
+            return SendMessageToSession(session, SessionMessageType.GeneralCommand, command, cancellationToken);
         }
 
-        private static async Task SendMessageToSession<T>(SessionInfo session, string name, T data, CancellationToken cancellationToken)
+        private static async Task SendMessageToSession<T>(SessionInfo session, SessionMessageType name, T data, CancellationToken cancellationToken)
         {
             var controllers = session.SessionControllers;
             var messageId = Guid.NewGuid();
@@ -1078,7 +1078,7 @@ namespace Emby.Server.Implementations.Session
             }
         }
 
-        private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, string name, T data, CancellationToken cancellationToken)
+        private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, SessionMessageType name, T data, CancellationToken cancellationToken)
         {
             IEnumerable<Task> GetTasks()
             {
@@ -1178,7 +1178,7 @@ namespace Emby.Server.Implementations.Session
                 }
             }
 
-            await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false);
+            await SendMessageToSession(session, SessionMessageType.Play, command, cancellationToken).ConfigureAwait(false);
         }
 
         /// <inheritdoc />
@@ -1186,7 +1186,7 @@ namespace Emby.Server.Implementations.Session
         {
             CheckDisposed();
             var session = GetSessionToRemoteControl(sessionId);
-            await SendMessageToSession(session, "SyncPlayCommand", command, cancellationToken).ConfigureAwait(false);
+            await SendMessageToSession(session, SessionMessageType.SyncPlayCommand, command, cancellationToken).ConfigureAwait(false);
         }
 
         /// <inheritdoc />
@@ -1194,7 +1194,7 @@ namespace Emby.Server.Implementations.Session
         {
             CheckDisposed();
             var session = GetSessionToRemoteControl(sessionId);
-            await SendMessageToSession(session, "SyncPlayGroupUpdate", command, cancellationToken).ConfigureAwait(false);
+            await SendMessageToSession(session, SessionMessageType.SyncPlayGroupUpdate, command, cancellationToken).ConfigureAwait(false);
         }
 
         private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
@@ -1297,7 +1297,7 @@ namespace Emby.Server.Implementations.Session
                 }
             }
 
-            return SendMessageToSession(session, "Playstate", command, cancellationToken);
+            return SendMessageToSession(session, SessionMessageType.PlayState, command, cancellationToken);
         }
 
         private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
@@ -1322,7 +1322,7 @@ namespace Emby.Server.Implementations.Session
         {
             CheckDisposed();
 
-            return SendMessageToSessions(Sessions, "RestartRequired", string.Empty, cancellationToken);
+            return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken);
         }
 
         /// <summary>
@@ -1334,7 +1334,7 @@ namespace Emby.Server.Implementations.Session
         {
             CheckDisposed();
 
-            return SendMessageToSessions(Sessions, "ServerShuttingDown", string.Empty, cancellationToken);
+            return SendMessageToSessions(Sessions, SessionMessageType.ServerShuttingDown, string.Empty, cancellationToken);
         }
 
         /// <summary>
@@ -1348,7 +1348,7 @@ namespace Emby.Server.Implementations.Session
 
             _logger.LogDebug("Beginning SendServerRestartNotification");
 
-            return SendMessageToSessions(Sessions, "ServerRestarting", string.Empty, cancellationToken);
+            return SendMessageToSessions(Sessions, SessionMessageType.ServerRestarting, string.Empty, cancellationToken);
         }
 
         /// <summary>
@@ -1484,6 +1484,14 @@ namespace Emby.Server.Implementations.Session
                 throw new SecurityException("User is not allowed access from this device.");
             }
 
+            int sessionsCount = Sessions.Count(i => i.UserId.Equals(user.Id));
+            int maxActiveSessions = user.MaxActiveSessions;
+            _logger.LogInformation("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCount, maxActiveSessions);
+            if (maxActiveSessions >= 1 && sessionsCount >= maxActiveSessions)
+            {
+                throw new SecurityException("User is at their maximum number of sessions.");
+            }
+
             var token = GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName);
 
             var session = LogSessionActivity(
@@ -1866,7 +1874,7 @@ namespace Emby.Server.Implementations.Session
         }
 
         /// <inheritdoc />
-        public Task SendMessageToAdminSessions<T>(string name, T data, CancellationToken cancellationToken)
+        public Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken)
         {
             CheckDisposed();
 
@@ -1879,7 +1887,7 @@ namespace Emby.Server.Implementations.Session
         }
 
         /// <inheritdoc />
-        public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, Func<T> dataFn, CancellationToken cancellationToken)
+        public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, CancellationToken cancellationToken)
         {
             CheckDisposed();
 
@@ -1894,7 +1902,7 @@ namespace Emby.Server.Implementations.Session
         }
 
         /// <inheritdoc />
-        public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, T data, CancellationToken cancellationToken)
+        public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken cancellationToken)
         {
             CheckDisposed();
 
@@ -1903,7 +1911,7 @@ namespace Emby.Server.Implementations.Session
         }
 
         /// <inheritdoc />
-        public Task SendMessageToUserDeviceSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken)
+        public Task SendMessageToUserDeviceSessions<T>(string deviceId, SessionMessageType name, T data, CancellationToken cancellationToken)
         {
             CheckDisposed();
 

+ 2 - 1
Emby.Server.Implementations/Session/SessionWebSocketListener.cs

@@ -8,6 +8,7 @@ using Jellyfin.Data.Events;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Session;
 using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
 
@@ -316,7 +317,7 @@ namespace Emby.Server.Implementations.Session
             return webSocket.SendAsync(
                 new WebSocketMessage<int>
                 {
-                    MessageType = "ForceKeepAlive",
+                    MessageType = SessionMessageType.ForceKeepAlive,
                     Data = WebSocketLostTimeout
                 },
                 CancellationToken.None);

+ 2 - 1
Emby.Server.Implementations/Session/WebSocketController.cs

@@ -11,6 +11,7 @@ using System.Threading.Tasks;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Session;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.Session
@@ -65,7 +66,7 @@ namespace Emby.Server.Implementations.Session
 
         /// <inheritdoc />
         public Task SendMessage<T>(
-            string name,
+            SessionMessageType name,
             Guid messageId,
             T data,
             CancellationToken cancellationToken)

+ 3 - 5
Emby.Server.Implementations/SyncPlay/SyncPlayController.cs

@@ -301,8 +301,7 @@ namespace Emby.Server.Implementations.SyncPlay
             if (_group.IsPaused)
             {
                 // Pick a suitable time that accounts for latency
-                var delay = _group.GetHighestPing() * 2;
-                delay = delay < _group.DefaultPing ? _group.DefaultPing : delay;
+                var delay = Math.Max(_group.GetHighestPing() * 2, GroupInfo.DefaultPing);
 
                 // Unpause group and set starting point in future
                 // Clients will start playback at LastActivity (datetime) from PositionTicks (playback position)
@@ -452,8 +451,7 @@ namespace Emby.Server.Implementations.SyncPlay
                     else
                     {
                         // Client, that was buffering, resumed playback but did not update others in time
-                        delay = _group.GetHighestPing() * 2;
-                        delay = delay < _group.DefaultPing ? _group.DefaultPing : delay;
+                        delay = Math.Max(_group.GetHighestPing() * 2, GroupInfo.DefaultPing);
 
                         _group.LastActivity = currentTime.AddMilliseconds(
                             delay);
@@ -497,7 +495,7 @@ namespace Emby.Server.Implementations.SyncPlay
         private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request)
         {
             // Collected pings are used to account for network latency when unpausing playback
-            _group.UpdatePing(session, request.Ping ?? _group.DefaultPing);
+            _group.UpdatePing(session, request.Ping ?? GroupInfo.DefaultPing);
         }
 
         /// <inheritdoc />

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

@@ -5,6 +5,7 @@ using System.Linq;
 using System.Threading;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Helpers;
+using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Library;
@@ -378,7 +379,7 @@ namespace Jellyfin.Api.Controllers
         public ActionResult PostCapabilities(
             [FromQuery] string? id,
             [FromQuery] string? playableMediaTypes,
-            [FromQuery] string? supportedCommands,
+            [FromQuery] GeneralCommandType[] supportedCommands,
             [FromQuery] bool supportsMediaControl = false,
             [FromQuery] bool supportsSync = false,
             [FromQuery] bool supportsPersistentIdentifier = true)
@@ -391,7 +392,7 @@ namespace Jellyfin.Api.Controllers
             _sessionManager.ReportCapabilities(id, new ClientCapabilities
             {
                 PlayableMediaTypes = RequestHelpers.Split(playableMediaTypes, ',', true),
-                SupportedCommands = RequestHelpers.Split(supportedCommands, ',', true),
+                SupportedCommands = supportedCommands,
                 SupportsMediaControl = supportsMediaControl,
                 SupportsSync = supportsSync,
                 SupportsPersistentIdentifier = supportsPersistentIdentifier

+ 1 - 1
Jellyfin.Api/Helpers/MediaInfoHelper.cs

@@ -554,7 +554,7 @@ namespace Jellyfin.Api.Helpers
         private long? GetMaxBitrate(long? clientMaxBitrate, User user, string ipAddress)
         {
             var maxBitrate = clientMaxBitrate;
-            var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0;
+            var remoteClientMaxBitrate = user.RemoteClientBitrateLimit ?? 0;
 
             if (remoteClientMaxBitrate <= 0)
             {

+ 1 - 4
Jellyfin.Api/Helpers/TranscodingJobHelper.cs

@@ -740,10 +740,7 @@ namespace Jellyfin.Api.Helpers
         /// <param name="state">The state.</param>
         private void OnFfMpegProcessExited(Process process, TranscodingJobDto job, StreamState state)
         {
-            if (job != null)
-            {
-                job.HasExited = true;
-            }
+            job.HasExited = true;
 
             _logger.LogDebug("Disposing stream resources");
             state.Dispose();

+ 64 - 0
Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs

@@ -0,0 +1,64 @@
+using System;
+using System.ComponentModel;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace Jellyfin.Api.ModelBinders
+{
+    /// <summary>
+    /// Comma delimited array model binder.
+    /// Returns an empty array of specified type if there is no query parameter.
+    /// </summary>
+    public class CommaDelimitedArrayModelBinder : IModelBinder
+    {
+        /// <inheritdoc/>
+        public Task BindModelAsync(ModelBindingContext bindingContext)
+        {
+            var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
+            var elementType = bindingContext.ModelType.GetElementType();
+            var converter = TypeDescriptor.GetConverter(elementType);
+
+            if (valueProviderResult == ValueProviderResult.None)
+            {
+                return Task.CompletedTask;
+            }
+
+            if (valueProviderResult.Length > 1)
+            {
+                var result = Array.CreateInstance(elementType, valueProviderResult.Length);
+
+                for (int i = 0; i < valueProviderResult.Length; i++)
+                {
+                    var value = converter.ConvertFromString(valueProviderResult.Values[i].Trim());
+
+                    result.SetValue(value, i);
+                }
+
+                bindingContext.Result = ModelBindingResult.Success(result);
+            }
+            else
+            {
+                var value = valueProviderResult.FirstValue;
+
+                if (value != null)
+                {
+                    var values = Array.ConvertAll(
+                        value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries),
+                        x => converter.ConvertFromString(x?.Trim()));
+
+                    var typedValues = Array.CreateInstance(elementType, values.Length);
+                    values.CopyTo(typedValues, 0);
+
+                    bindingContext.Result = ModelBindingResult.Success(typedValues);
+                }
+                else
+                {
+                    var emptyResult = Array.CreateInstance(elementType, 0);
+                    bindingContext.Result = ModelBindingResult.Success(emptyResult);
+                }
+            }
+
+            return Task.CompletedTask;
+        }
+    }
+}

+ 29 - 0
Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinderProvider.cs

@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace Jellyfin.Api.ModelBinders
+{
+    /// <summary>
+    /// Comma delimited array model binder provider.
+    /// </summary>
+    public class CommaDelimitedArrayModelBinderProvider : IModelBinderProvider
+    {
+        private readonly IModelBinder _binder;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="CommaDelimitedArrayModelBinderProvider"/> class.
+        /// </summary>
+        public CommaDelimitedArrayModelBinderProvider()
+        {
+            _binder = new CommaDelimitedArrayModelBinder();
+        }
+
+        /// <inheritdoc />
+        public IModelBinder? GetBinder(ModelBinderProviderContext context)
+        {
+            return context.Metadata.ModelType.IsArray ? _binder : null;
+        }
+    }
+}

+ 9 - 5
Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs

@@ -3,6 +3,7 @@ using System.Threading.Tasks;
 using Jellyfin.Data.Events;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Model.Activity;
+using MediaBrowser.Model.Session;
 using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.Api.WebSocketListeners
@@ -29,11 +30,14 @@ namespace Jellyfin.Api.WebSocketListeners
             _activityManager.EntryCreated += OnEntryCreated;
         }
 
-        /// <summary>
-        /// Gets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        protected override string Name => "ActivityLogEntry";
+        /// <inheritdoc />
+        protected override SessionMessageType Type => SessionMessageType.ActivityLogEntry;
+
+        /// <inheritdoc />
+        protected override SessionMessageType StartType => SessionMessageType.ActivityLogEntryStart;
+
+        /// <inheritdoc />
+        protected override SessionMessageType StopType => SessionMessageType.ActivityLogEntryStop;
 
         /// <summary>
         /// Gets the data to send.

+ 9 - 5
Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs

@@ -3,6 +3,7 @@ using System.Linq;
 using System.Threading.Tasks;
 using Jellyfin.Data.Events;
 using MediaBrowser.Controller.Net;
+using MediaBrowser.Model.Session;
 using MediaBrowser.Model.Tasks;
 using Microsoft.Extensions.Logging;
 
@@ -33,11 +34,14 @@ namespace Jellyfin.Api.WebSocketListeners
             _taskManager.TaskCompleted += OnTaskCompleted;
         }
 
-        /// <summary>
-        /// Gets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        protected override string Name => "ScheduledTasksInfo";
+        /// <inheritdoc />
+        protected override SessionMessageType Type => SessionMessageType.ScheduledTasksInfo;
+
+        /// <inheritdoc />
+        protected override SessionMessageType StartType => SessionMessageType.ScheduledTasksInfoStart;
+
+        /// <inheritdoc />
+        protected override SessionMessageType StopType => SessionMessageType.ScheduledTasksInfoStop;
 
         /// <summary>
         /// Gets the data to send.

+ 8 - 1
Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs

@@ -3,6 +3,7 @@ using System.Threading.Tasks;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
 using Microsoft.Extensions.Logging;
 
 namespace Jellyfin.Api.WebSocketListeners
@@ -34,7 +35,13 @@ namespace Jellyfin.Api.WebSocketListeners
         }
 
         /// <inheritdoc />
-        protected override string Name => "Sessions";
+        protected override SessionMessageType Type => SessionMessageType.Sessions;
+
+        /// <inheritdoc />
+        protected override SessionMessageType StartType => SessionMessageType.SessionsStart;
+
+        /// <inheritdoc />
+        protected override SessionMessageType StopType => SessionMessageType.SessionsStop;
 
         /// <summary>
         /// Gets the data to send.

+ 5 - 0
Jellyfin.Data/Entities/User.cs

@@ -188,6 +188,11 @@ namespace Jellyfin.Data.Entities
         /// </summary>
         public int? LoginAttemptsBeforeLockout { get; set; }
 
+        /// <summary>
+        /// Gets or sets the maximum number of active sessions the user can have at once.
+        /// </summary>
+        public int MaxActiveSessions { get; set; }
+
         /// <summary>
         /// Gets or sets the subtitle mode.
         /// </summary>

+ 2 - 1
Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedNotifier.cs

@@ -2,6 +2,7 @@
 using System.Threading.Tasks;
 using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
 using MediaBrowser.Model.Tasks;
 
 namespace Jellyfin.Server.Implementations.Events.Consumers.System
@@ -25,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.System
         /// <inheritdoc />
         public async Task OnEvent(TaskCompletionEventArgs eventArgs)
         {
-            await _sessionManager.SendMessageToAdminSessions("ScheduledTaskEnded", eventArgs.Result, CancellationToken.None).ConfigureAwait(false);
+            await _sessionManager.SendMessageToAdminSessions(SessionMessageType.ScheduledTaskEnded, eventArgs.Result, CancellationToken.None).ConfigureAwait(false);
         }
     }
 }

+ 2 - 1
Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationCancelledNotifier.cs

@@ -3,6 +3,7 @@ using System.Threading.Tasks;
 using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Events.Updates;
 using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
 
 namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
 {
@@ -25,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
         /// <inheritdoc />
         public async Task OnEvent(PluginInstallationCancelledEventArgs eventArgs)
         {
-            await _sessionManager.SendMessageToAdminSessions("PackageInstallationCancelled", eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
+            await _sessionManager.SendMessageToAdminSessions(SessionMessageType.PackageInstallationCancelled, eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
         }
     }
 }

+ 2 - 1
Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedNotifier.cs

@@ -3,6 +3,7 @@ using System.Threading.Tasks;
 using MediaBrowser.Common.Updates;
 using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
 
 namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
 {
@@ -25,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
         /// <inheritdoc />
         public async Task OnEvent(InstallationFailedEventArgs eventArgs)
         {
-            await _sessionManager.SendMessageToAdminSessions("PackageInstallationFailed", eventArgs.InstallationInfo, CancellationToken.None).ConfigureAwait(false);
+            await _sessionManager.SendMessageToAdminSessions(SessionMessageType.PackageInstallationFailed, eventArgs.InstallationInfo, CancellationToken.None).ConfigureAwait(false);
         }
     }
 }

+ 2 - 1
Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledNotifier.cs

@@ -3,6 +3,7 @@ using System.Threading.Tasks;
 using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Events.Updates;
 using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
 
 namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
 {
@@ -25,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
         /// <inheritdoc />
         public async Task OnEvent(PluginInstalledEventArgs eventArgs)
         {
-            await _sessionManager.SendMessageToAdminSessions("PackageInstallationCompleted", eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
+            await _sessionManager.SendMessageToAdminSessions(SessionMessageType.PackageInstallationCompleted, eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
         }
     }
 }

+ 2 - 1
Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallingNotifier.cs

@@ -3,6 +3,7 @@ using System.Threading.Tasks;
 using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Events.Updates;
 using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
 
 namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
 {
@@ -25,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
         /// <inheritdoc />
         public async Task OnEvent(PluginInstallingEventArgs eventArgs)
         {
-            await _sessionManager.SendMessageToAdminSessions("PackageInstalling", eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
+            await _sessionManager.SendMessageToAdminSessions(SessionMessageType.PackageInstalling, eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
         }
     }
 }

+ 2 - 1
Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledNotifier.cs

@@ -3,6 +3,7 @@ using System.Threading.Tasks;
 using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Events.Updates;
 using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
 
 namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
 {
@@ -25,7 +26,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Updates
         /// <inheritdoc />
         public async Task OnEvent(PluginUninstalledEventArgs eventArgs)
         {
-            await _sessionManager.SendMessageToAdminSessions("PluginUninstalled", eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
+            await _sessionManager.SendMessageToAdminSessions(SessionMessageType.PackageUninstalled, eventArgs.Argument, CancellationToken.None).ConfigureAwait(false);
         }
     }
 }

+ 2 - 1
Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedNotifier.cs

@@ -6,6 +6,7 @@ using System.Threading.Tasks;
 using Jellyfin.Data.Events.Users;
 using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
 
 namespace Jellyfin.Server.Implementations.Events.Consumers.Users
 {
@@ -30,7 +31,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Users
         {
             await _sessionManager.SendMessageToUserSessions(
                 new List<Guid> { eventArgs.Argument.Id },
-                "UserDeleted",
+                SessionMessageType.UserDeleted,
                 eventArgs.Argument.Id.ToString("N", CultureInfo.InvariantCulture),
                 CancellationToken.None).ConfigureAwait(false);
         }

+ 2 - 1
Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs

@@ -6,6 +6,7 @@ using Jellyfin.Data.Events.Users;
 using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Session;
+using MediaBrowser.Model.Session;
 
 namespace Jellyfin.Server.Implementations.Events.Consumers.Users
 {
@@ -33,7 +34,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Users
         {
             await _sessionManager.SendMessageToUserSessions(
                 new List<Guid> { e.Argument.Id },
-                "UserUpdated",
+                SessionMessageType.UserUpdated,
                 _userManager.GetUserDto(e.Argument),
                 CancellationToken.None).ConfigureAwait(false);
         }

+ 464 - 0
Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs

@@ -0,0 +1,464 @@
+#pragma warning disable CS1591
+
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    [DbContext(typeof(JellyfinDb))]
+    [Migration("20201004171403_AddMaxActiveSessions")]
+    partial class AddMaxActiveSessions
+    {
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasDefaultSchema("jellyfin")
+                .HasAnnotation("ProductVersion", "3.1.8");
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DayOfWeek")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double>("EndHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<double>("StartHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("AccessSchedules");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ItemId")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(256);
+
+                    b.Property<int>("LogSeverity")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(512);
+
+                    b.Property<string>("Overview")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(512);
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ShortOverview")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(512);
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(256);
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.ToTable("ActivityLogs");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ChromecastVersion")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(32);
+
+                    b.Property<string>("DashboardTheme")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(32);
+
+                    b.Property<bool>("EnableNextVideoInfoOverlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ScrollDirection")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowBackdrop")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowSidebar")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipBackwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipForwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("TvHome")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(32);
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.HasIndex("UserId", "Client")
+                        .IsUnique();
+
+                    b.ToTable("DisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DisplayPreferencesId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DisplayPreferencesId");
+
+                    b.ToTable("HomeSection");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("LastModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(512);
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("ImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(32);
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("RememberIndexing")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSorting")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortBy")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(64);
+
+                    b.Property<int>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ViewType")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("ItemDisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Permission_Permissions_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("Value")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Permission_Permissions_Guid");
+
+                    b.ToTable("Permissions");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Preference_Preferences_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Value")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(65535);
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Preference_Preferences_Guid");
+
+                    b.ToTable("Preferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AudioLanguagePreference")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.Property<string>("AuthenticationProviderId")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.Property<bool>("DisplayCollectionsView")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("DisplayMissingEpisodes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("EasyPassword")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(65535);
+
+                    b.Property<bool>("EnableAutoLogin")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableLocalPassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableNextEpisodeAutoPlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableUserPreferenceAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("HidePlayedInLatest")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("InternalId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("InvalidLoginAttemptCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastActivityDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("LastLoginDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("LoginAttemptsBeforeLockout")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("MaxActiveSessions")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("MaxParentalAgeRating")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("MustUpdatePassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Password")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(65535);
+
+                    b.Property<string>("PasswordResetProviderId")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.Property<bool>("PlayDefaultAudioTrack")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberAudioSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSubtitleSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("RemoteClientBitrateLimit")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SubtitleLanguagePreference")
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.Property<int>("SubtitleMode")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SyncPlayAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Username")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasMaxLength(255);
+
+                    b.HasKey("Id");
+
+                    b.ToTable("Users");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("AccessSchedules")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithOne("DisplayPreferences")
+                        .HasForeignKey("Jellyfin.Data.Entities.DisplayPreferences", "UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+                        .WithMany("HomeSections")
+                        .HasForeignKey("DisplayPreferencesId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithOne("ProfileImage")
+                        .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("ItemDisplayPreferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("Permissions")
+                        .HasForeignKey("Permission_Permissions_Guid");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("Preferences")
+                        .HasForeignKey("Preference_Preferences_Guid");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}

+ 28 - 0
Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs

@@ -0,0 +1,28 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1601
+
+using Microsoft.EntityFrameworkCore.Migrations;
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    public partial class AddMaxActiveSessions : Migration
+    {
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AddColumn<int>(
+                name: "MaxActiveSessions",
+                schema: "jellyfin",
+                table: "Users",
+                nullable: false,
+                defaultValue: 0);
+        }
+
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropColumn(
+                name: "MaxActiveSessions",
+                schema: "jellyfin",
+                table: "Users");
+        }
+    }
+}

+ 4 - 1
Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs

@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
 #pragma warning disable 612, 618
             modelBuilder
                 .HasDefaultSchema("jellyfin")
-                .HasAnnotation("ProductVersion", "3.1.7");
+                .HasAnnotation("ProductVersion", "3.1.8");
 
             modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
                 {
@@ -344,6 +344,9 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.Property<int?>("LoginAttemptsBeforeLockout")
                         .HasColumnType("INTEGER");
 
+                    b.Property<int>("MaxActiveSessions")
+                        .HasColumnType("INTEGER");
+
                     b.Property<int?>("MaxParentalAgeRating")
                         .HasColumnType("INTEGER");
 

+ 1 - 1
Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs

@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Users
         public string Name => "InvalidOrMissingAuthenticationProvider";
 
         /// <inheritdoc />
-        public bool IsEnabled => true;
+        public bool IsEnabled => false;
 
         /// <inheritdoc />
         public Task<ProviderAuthenticationResult> Authenticate(string username, string password)

+ 3 - 1
Jellyfin.Server.Implementations/Users/UserManager.cs

@@ -379,6 +379,7 @@ namespace Jellyfin.Server.Implementations.Users
                     PasswordResetProviderId = user.PasswordResetProviderId,
                     InvalidLoginAttemptCount = user.InvalidLoginAttemptCount,
                     LoginAttemptsBeforeLockout = user.LoginAttemptsBeforeLockout ?? -1,
+                    MaxActiveSessions = user.MaxActiveSessions,
                     IsAdministrator = user.HasPermission(PermissionKind.IsAdministrator),
                     IsHidden = user.HasPermission(PermissionKind.IsHidden),
                     IsDisabled = user.HasPermission(PermissionKind.IsDisabled),
@@ -701,6 +702,7 @@ namespace Jellyfin.Server.Implementations.Users
             user.PasswordResetProviderId = policy.PasswordResetProviderId;
             user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
             user.LoginAttemptsBeforeLockout = maxLoginAttempts;
+            user.MaxActiveSessions = policy.MaxActiveSessions;
             user.SyncPlayAccess = policy.SyncPlayAccess;
             user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
             user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
@@ -799,7 +801,7 @@ namespace Jellyfin.Server.Implementations.Users
 
         private IList<IPasswordResetProvider> GetPasswordResetProviders(User user)
         {
-            var passwordResetProviderId = user?.PasswordResetProviderId;
+            var passwordResetProviderId = user.PasswordResetProviderId;
             var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray();
 
             if (!string.IsNullOrEmpty(passwordResetProviderId))

+ 3 - 0
Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs

@@ -16,6 +16,7 @@ using Jellyfin.Api.Auth.LocalAccessPolicy;
 using Jellyfin.Api.Auth.RequiresElevationPolicy;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Controllers;
+using Jellyfin.Api.ModelBinders;
 using Jellyfin.Server.Configuration;
 using Jellyfin.Server.Filters;
 using Jellyfin.Server.Formatters;
@@ -166,6 +167,8 @@ namespace Jellyfin.Server.Extensions
 
                     opts.OutputFormatters.Add(new CssOutputFormatter());
                     opts.OutputFormatters.Add(new XmlOutputFormatter());
+
+                    opts.ModelBinderProviders.Insert(0, new CommaDelimitedArrayModelBinderProvider());
                 })
 
                 // Clear app parts to avoid other assemblies being picked up

+ 2 - 2
Jellyfin.Server/Middleware/ExceptionMiddleware.cs

@@ -125,8 +125,8 @@ namespace Jellyfin.Server.Middleware
             switch (ex)
             {
                 case ArgumentException _: return StatusCodes.Status400BadRequest;
-                case AuthenticationException _:
-                case SecurityException _: return StatusCodes.Status401Unauthorized;
+                case AuthenticationException _: return StatusCodes.Status401Unauthorized;
+                case SecurityException _: return StatusCodes.Status403Forbidden;
                 case DirectoryNotFoundException _:
                 case FileNotFoundException _:
                 case ResourceNotFoundException _: return StatusCodes.Status404NotFound;

+ 8 - 1
Jellyfin.Server/Startup.cs

@@ -1,6 +1,7 @@
 using System;
 using System.ComponentModel;
 using System.Net.Http.Headers;
+using System.Net.Mime;
 using Jellyfin.Api.TypeConverters;
 using Jellyfin.Networking.Configuration;
 using Jellyfin.Server.Extensions;
@@ -12,6 +13,7 @@ using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Extensions;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.StaticFiles;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.FileProviders;
@@ -124,10 +126,15 @@ namespace Jellyfin.Server
                 mainApp.UseStaticFiles();
                 if (appConfig.HostWebClient())
                 {
+                    var extensionProvider = new FileExtensionContentTypeProvider();
+
+                    // subtitles octopus requires .data files.
+                    extensionProvider.Mappings.Add(".data", MediaTypeNames.Application.Octet);
                     mainApp.UseStaticFiles(new StaticFileOptions
                     {
                         FileProvider = new PhysicalFileProvider(_serverConfigurationManager.ApplicationPaths.WebPath),
-                        RequestPath = "/web"
+                        RequestPath = "/web",
+                        ContentTypeProvider = extensionProvider
                     });
                 }
 

+ 20 - 19
MediaBrowser.Common/Json/Converters/JsonNullableStructConverter.cs

@@ -8,37 +8,38 @@ namespace MediaBrowser.Common.Json.Converters
     /// Converts a nullable struct or value to/from JSON.
     /// Required - some clients send an empty string.
     /// </summary>
-    /// <typeparam name="T">The struct type.</typeparam>
-    public class JsonNullableStructConverter<T> : JsonConverter<T?>
-        where T : struct
+    /// <typeparam name="TStruct">The struct type.</typeparam>
+    public class JsonNullableStructConverter<TStruct> : JsonConverter<TStruct?>
+        where TStruct : struct
     {
-        private readonly JsonConverter<T?> _baseJsonConverter;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="JsonNullableStructConverter{T}"/> class.
-        /// </summary>
-        /// <param name="baseJsonConverter">The base json converter.</param>
-        public JsonNullableStructConverter(JsonConverter<T?> baseJsonConverter)
-        {
-            _baseJsonConverter = baseJsonConverter;
-        }
-
         /// <inheritdoc />
-        public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        public override TStruct? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
         {
-            // Handle empty string.
+            if (reader.TokenType == JsonTokenType.Null)
+            {
+                return null;
+            }
+
+            // Token is empty string.
             if (reader.TokenType == JsonTokenType.String && ((reader.HasValueSequence && reader.ValueSequence.IsEmpty) || reader.ValueSpan.IsEmpty))
             {
                 return null;
             }
 
-            return _baseJsonConverter.Read(ref reader, typeToConvert, options);
+            return JsonSerializer.Deserialize<TStruct>(ref reader, options);
         }
 
         /// <inheritdoc />
-        public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
+        public override void Write(Utf8JsonWriter writer, TStruct? value, JsonSerializerOptions options)
         {
-            _baseJsonConverter.Write(writer, value, options);
+            if (value.HasValue)
+            {
+                JsonSerializer.Serialize(writer, value.Value, options);
+            }
+            else
+            {
+                writer.WriteNullValue();
+            }
         }
     }
 }

+ 27 - 0
MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs

@@ -0,0 +1,27 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+    /// <summary>
+    /// Json nullable struct converter factory.
+    /// </summary>
+    public class JsonNullableStructConverterFactory : JsonConverterFactory
+    {
+        /// <inheritdoc />
+        public override bool CanConvert(Type typeToConvert)
+        {
+            return typeToConvert.IsGenericType
+                   && typeToConvert.GetGenericTypeDefinition() == typeof(Nullable<>)
+                   && typeToConvert.GenericTypeArguments[0].IsValueType;
+        }
+
+        /// <inheritdoc />
+        public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
+        {
+            var structType = typeToConvert.GenericTypeArguments[0];
+            return (JsonConverter)Activator.CreateInstance(typeof(JsonNullableStructConverter<>).MakeGenericType(structType));
+        }
+    }
+}

+ 1 - 6
MediaBrowser.Common/Json/JsonDefaults.cs

@@ -39,14 +39,9 @@ namespace MediaBrowser.Common.Json
                 NumberHandling = JsonNumberHandling.AllowReadingFromString
             };
 
-            // Get built-in converters for fallback converting.
-            var baseNullableInt32Converter = (JsonConverter<int?>)options.GetConverter(typeof(int?));
-            var baseNullableInt64Converter = (JsonConverter<long?>)options.GetConverter(typeof(long?));
-
             options.Converters.Add(new JsonGuidConverter());
             options.Converters.Add(new JsonStringEnumConverter());
-            options.Converters.Add(new JsonNullableStructConverter<int>(baseNullableInt32Converter));
-            options.Converters.Add(new JsonNullableStructConverter<long>(baseNullableInt64Converter));
+            options.Converters.Add(new JsonNullableStructConverterFactory());
 
             return options;
         }

+ 20 - 7
MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs

@@ -8,6 +8,7 @@ using System.Net.WebSockets;
 using System.Threading;
 using System.Threading.Tasks;
 using MediaBrowser.Model.Net;
+using MediaBrowser.Model.Session;
 using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Controller.Net
@@ -28,10 +29,22 @@ namespace MediaBrowser.Controller.Net
             new List<Tuple<IWebSocketConnection, CancellationTokenSource, TStateType>>();
 
         /// <summary>
-        /// Gets the name.
+        /// Gets the type used for the messages sent to the client.
         /// </summary>
-        /// <value>The name.</value>
-        protected abstract string Name { get; }
+        /// <value>The type.</value>
+        protected abstract SessionMessageType Type { get; }
+
+        /// <summary>
+        /// Gets the message type received from the client to start sending messages.
+        /// </summary>
+        /// <value>The type.</value>
+        protected abstract SessionMessageType StartType { get; }
+
+        /// <summary>
+        /// Gets the message type received from the client to stop sending messages.
+        /// </summary>
+        /// <value>The type.</value>
+        protected abstract SessionMessageType StopType { get; }
 
         /// <summary>
         /// Gets the data to send.
@@ -66,12 +79,12 @@ namespace MediaBrowser.Controller.Net
                 throw new ArgumentNullException(nameof(message));
             }
 
-            if (string.Equals(message.MessageType, Name + "Start", StringComparison.OrdinalIgnoreCase))
+            if (message.MessageType == StartType)
             {
                 Start(message);
             }
 
-            if (string.Equals(message.MessageType, Name + "Stop", StringComparison.OrdinalIgnoreCase))
+            if (message.MessageType == StopType)
             {
                 Stop(message);
             }
@@ -159,7 +172,7 @@ namespace MediaBrowser.Controller.Net
                         new WebSocketMessage<TReturnDataType>
                         {
                             MessageId = Guid.NewGuid(),
-                            MessageType = Name,
+                            MessageType = Type,
                             Data = data
                         },
                         cancellationToken).ConfigureAwait(false);
@@ -176,7 +189,7 @@ namespace MediaBrowser.Controller.Net
             }
             catch (Exception ex)
             {
-                Logger.LogError(ex, "Error sending web socket message {Name}", Name);
+                Logger.LogError(ex, "Error sending web socket message {Name}", Type);
                 DisposeConnection(tuple);
             }
         }

+ 2 - 1
MediaBrowser.Controller/Session/ISessionController.cs

@@ -3,6 +3,7 @@
 using System;
 using System.Threading;
 using System.Threading.Tasks;
+using MediaBrowser.Model.Session;
 
 namespace MediaBrowser.Controller.Session
 {
@@ -23,6 +24,6 @@ namespace MediaBrowser.Controller.Session
         /// <summary>
         /// Sends the message.
         /// </summary>
-        Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken);
+        Task SendMessage<T>(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken);
     }
 }

+ 4 - 4
MediaBrowser.Controller/Session/ISessionManager.cs

@@ -188,16 +188,16 @@ namespace MediaBrowser.Controller.Session
         /// <param name="data">The data.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        Task SendMessageToAdminSessions<T>(string name, T data, CancellationToken cancellationToken);
+        Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken);
 
         /// <summary>
         /// Sends the message to user sessions.
         /// </summary>
         /// <typeparam name="T"></typeparam>
         /// <returns>Task.</returns>
-        Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, T data, CancellationToken cancellationToken);
+        Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken cancellationToken);
 
-        Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, Func<T> dataFn, CancellationToken cancellationToken);
+        Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, CancellationToken cancellationToken);
 
         /// <summary>
         /// Sends the message to user device sessions.
@@ -208,7 +208,7 @@ namespace MediaBrowser.Controller.Session
         /// <param name="data">The data.</param>
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>Task.</returns>
-        Task SendMessageToUserDeviceSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken);
+        Task SendMessageToUserDeviceSessions<T>(string deviceId, SessionMessageType name, T data, CancellationToken cancellationToken);
 
         /// <summary>
         /// Sends the restart required message.

+ 2 - 2
MediaBrowser.Controller/Session/SessionInfo.cs

@@ -230,8 +230,8 @@ namespace MediaBrowser.Controller.Session
         /// Gets or sets the supported commands.
         /// </summary>
         /// <value>The supported commands.</value>
-        public string[] SupportedCommands
-            => Capabilities == null ? Array.Empty<string>() : Capabilities.SupportedCommands;
+        public GeneralCommandType[] SupportedCommands
+            => Capabilities == null ? Array.Empty<GeneralCommandType>() : Capabilities.SupportedCommands;
 
         public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory)
         {

+ 21 - 31
MediaBrowser.Controller/SyncPlay/GroupInfo.cs

@@ -14,12 +14,12 @@ namespace MediaBrowser.Controller.SyncPlay
     public class GroupInfo
     {
         /// <summary>
-        /// Gets the default ping value used for sessions.
+        /// The default ping value used for sessions.
         /// </summary>
-        public long DefaultPing { get; } = 500;
+        public const long DefaultPing = 500;
 
         /// <summary>
-        /// Gets or sets the group identifier.
+        /// Gets the group identifier.
         /// </summary>
         /// <value>The group identifier.</value>
         public Guid GroupId { get; } = Guid.NewGuid();
@@ -58,7 +58,8 @@ namespace MediaBrowser.Controller.SyncPlay
         /// <summary>
         /// Checks if a session is in this group.
         /// </summary>
-        /// <value><c>true</c> if the session is in this group; <c>false</c> otherwise.</value>
+        /// <param name="sessionId">The session id to check.</param>
+        /// <returns><c>true</c> if the session is in this group; <c>false</c> otherwise.</returns>
         public bool ContainsSession(string sessionId)
         {
             return Participants.ContainsKey(sessionId);
@@ -70,16 +71,14 @@ namespace MediaBrowser.Controller.SyncPlay
         /// <param name="session">The session.</param>
         public void AddSession(SessionInfo session)
         {
-            if (ContainsSession(session.Id))
-            {
-                return;
-            }
-
-            var member = new GroupMember();
-            member.Session = session;
-            member.Ping = DefaultPing;
-            member.IsBuffering = false;
-            Participants[session.Id] = member;
+            Participants.TryAdd(
+                session.Id,
+                new GroupMember
+                {
+                    Session = session,
+                    Ping = DefaultPing,
+                    IsBuffering = false
+                });
         }
 
         /// <summary>
@@ -88,12 +87,7 @@ namespace MediaBrowser.Controller.SyncPlay
         /// <param name="session">The session.</param>
         public void RemoveSession(SessionInfo session)
         {
-            if (!ContainsSession(session.Id))
-            {
-                return;
-            }
-
-            Participants.Remove(session.Id, out _);
+            Participants.Remove(session.Id);
         }
 
         /// <summary>
@@ -103,18 +97,16 @@ namespace MediaBrowser.Controller.SyncPlay
         /// <param name="ping">The ping.</param>
         public void UpdatePing(SessionInfo session, long ping)
         {
-            if (!ContainsSession(session.Id))
+            if (Participants.TryGetValue(session.Id, out GroupMember value))
             {
-                return;
+                value.Ping = ping;
             }
-
-            Participants[session.Id].Ping = ping;
         }
 
         /// <summary>
         /// Gets the highest ping in the group.
         /// </summary>
-        /// <value name="session">The highest ping in the group.</value>
+        /// <returns>The highest ping in the group.</returns>
         public long GetHighestPing()
         {
             long max = long.MinValue;
@@ -133,18 +125,16 @@ namespace MediaBrowser.Controller.SyncPlay
         /// <param name="isBuffering">The state.</param>
         public void SetBuffering(SessionInfo session, bool isBuffering)
         {
-            if (!ContainsSession(session.Id))
+            if (Participants.TryGetValue(session.Id, out GroupMember value))
             {
-                return;
+                value.IsBuffering = isBuffering;
             }
-
-            Participants[session.Id].IsBuffering = isBuffering;
         }
 
         /// <summary>
         /// Gets the group buffering state.
         /// </summary>
-        /// <value><c>true</c> if there is a session buffering in the group; <c>false</c> otherwise.</value>
+        /// <returns><c>true</c> if there is a session buffering in the group; <c>false</c> otherwise.</returns>
         public bool IsBuffering()
         {
             foreach (var session in Participants.Values)
@@ -161,7 +151,7 @@ namespace MediaBrowser.Controller.SyncPlay
         /// <summary>
         /// Checks if the group is empty.
         /// </summary>
-        /// <value><c>true</c> if the group is empty; <c>false</c> otherwise.</value>
+        /// <returns><c>true</c> if the group is empty; <c>false</c> otherwise.</returns>
         public bool IsEmpty()
         {
             return Participants.Count == 0;

+ 0 - 2
MediaBrowser.Model/Configuration/LibraryOptions.cs

@@ -25,8 +25,6 @@ namespace MediaBrowser.Model.Configuration
 
         public bool EnableInternetProviders { get; set; }
 
-        public bool ImportMissingEpisodes { get; set; }
-
         public bool EnableAutomaticSeriesGrouping { get; set; }
 
         public bool EnableEmbeddedTitles { get; set; }

+ 6 - 4
MediaBrowser.Model/Dlna/StreamBuilder.cs

@@ -455,9 +455,10 @@ namespace MediaBrowser.Model.Dlna
 
             if (directPlayProfile == null)
             {
-                _logger.LogInformation("Profile: {0}, No direct play profiles found for Path: {1}",
+                _logger.LogInformation("Profile: {0}, No audio direct play profiles found for {1} with codec {2}",
                     options.Profile.Name ?? "Unknown Profile",
-                    item.Path ?? "Unknown path");
+                    item.Path ?? "Unknown path",
+                    audioStream.Codec ?? "Unknown codec");
 
                 return (Enumerable.Empty<PlayMethod>(), GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles));
             }
@@ -972,9 +973,10 @@ namespace MediaBrowser.Model.Dlna
 
             if (directPlay == null)
             {
-                _logger.LogInformation("Profile: {0}, No direct play profiles found for Path: {1}",
+                _logger.LogInformation("Profile: {0}, No video direct play profiles found for {1} with codec {2}",
                     profile.Name ?? "Unknown Profile",
-                    mediaSource.Path ?? "Unknown path");
+                    mediaSource.Path ?? "Unknown path",
+                    videoStream.Codec ?? "Unknown codec");
 
                 return (null, GetTranscodeReasonsFromDirectPlayProfile(mediaSource, videoStream, audioStream, profile.DirectPlayProfiles));
             }

+ 46 - 0
MediaBrowser.Model/Extensions/EnumerableExtensions.cs

@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MediaBrowser.Model.Providers;
+
+namespace MediaBrowser.Model.Extensions
+{
+    /// <summary>
+    /// Extension methods for <see cref="IEnumerable{T}"/>.
+    /// </summary>
+    public static class EnumerableExtensions
+    {
+        /// <summary>
+        /// Orders <see cref="RemoteImageInfo"/> by requested language in descending order, prioritizing "en" over other non-matches.
+        /// </summary>
+        /// <param name="remoteImageInfos">The remote image infos.</param>
+        /// <param name="requestedLanguage">The requested language for the images.</param>
+        /// <returns>The ordered remote image infos.</returns>
+        public static IEnumerable<RemoteImageInfo> OrderByLanguageDescending(this IEnumerable<RemoteImageInfo> remoteImageInfos, string requestedLanguage)
+        {
+            var isRequestedLanguageEn = string.Equals(requestedLanguage, "en", StringComparison.OrdinalIgnoreCase);
+
+            return remoteImageInfos.OrderByDescending(i =>
+                {
+                    if (string.Equals(requestedLanguage, i.Language, StringComparison.OrdinalIgnoreCase))
+                    {
+                        return 3;
+                    }
+
+                    if (!isRequestedLanguageEn && string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
+                    {
+                        return 2;
+                    }
+
+                    if (string.IsNullOrEmpty(i.Language))
+                    {
+                        return isRequestedLanguageEn ? 3 : 2;
+                    }
+
+                    return 0;
+                })
+                .ThenByDescending(i => i.CommunityRating ?? 0)
+                .ThenByDescending(i => i.VoteCount ?? 0);
+        }
+    }
+}

+ 2 - 1
MediaBrowser.Model/Net/WebSocketMessage.cs

@@ -2,6 +2,7 @@
 #pragma warning disable CS1591
 
 using System;
+using MediaBrowser.Model.Session;
 
 namespace MediaBrowser.Model.Net
 {
@@ -15,7 +16,7 @@ namespace MediaBrowser.Model.Net
         /// Gets or sets the type of the message.
         /// </summary>
         /// <value>The type of the message.</value>
-        public string MessageType { get; set; }
+        public SessionMessageType MessageType { get; set; }
 
         public Guid MessageId { get; set; }
 

+ 2 - 2
MediaBrowser.Model/Session/ClientCapabilities.cs

@@ -10,7 +10,7 @@ namespace MediaBrowser.Model.Session
     {
         public string[] PlayableMediaTypes { get; set; }
 
-        public string[] SupportedCommands { get; set; }
+        public GeneralCommandType[] SupportedCommands { get; set; }
 
         public bool SupportsMediaControl { get; set; }
 
@@ -31,7 +31,7 @@ namespace MediaBrowser.Model.Session
         public ClientCapabilities()
         {
             PlayableMediaTypes = Array.Empty<string>();
-            SupportedCommands = Array.Empty<string>();
+            SupportedCommands = Array.Empty<GeneralCommandType>();
             SupportsPersistentIdentifier = true;
         }
     }

+ 50 - 0
MediaBrowser.Model/Session/SessionMessageType.cs

@@ -0,0 +1,50 @@
+#pragma warning disable CS1591
+
+namespace MediaBrowser.Model.Session
+{
+    /// <summary>
+    /// The different kinds of messages that are used in the WebSocket api.
+    /// </summary>
+    public enum SessionMessageType
+    {
+        // Server -> Client
+        ForceKeepAlive,
+        GeneralCommand,
+        UserDataChanged,
+        Sessions,
+        Play,
+        SyncPlayCommand,
+        SyncPlayGroupUpdate,
+        PlayState,
+        RestartRequired,
+        ServerShuttingDown,
+        ServerRestarting,
+        LibraryChanged,
+        UserDeleted,
+        UserUpdated,
+        SeriesTimerCreated,
+        TimerCreated,
+        SeriesTimerCancelled,
+        TimerCancelled,
+        RefreshProgress,
+        ScheduledTaskEnded,
+        PackageInstallationCancelled,
+        PackageInstallationFailed,
+        PackageInstallationCompleted,
+        PackageInstalling,
+        PackageUninstalled,
+        ActivityLogEntry,
+        ScheduledTasksInfo,
+
+        // Client -> Server
+        ActivityLogEntryStart,
+        ActivityLogEntryStop,
+        SessionsStart,
+        SessionsStop,
+        ScheduledTasksInfoStart,
+        ScheduledTasksInfoStop,
+
+        // Shared
+        KeepAlive,
+    }
+}

+ 4 - 0
MediaBrowser.Model/Users/UserPolicy.cs

@@ -92,6 +92,8 @@ namespace MediaBrowser.Model.Users
 
         public int LoginAttemptsBeforeLockout { get; set; }
 
+        public int MaxActiveSessions { get; set; }
+
         public bool EnablePublicSharing { get; set; }
 
         public Guid[] BlockedMediaFolders { get; set; }
@@ -144,6 +146,8 @@ namespace MediaBrowser.Model.Users
 
             LoginAttemptsBeforeLockout = -1;
 
+            MaxActiveSessions = 0;
+
             EnableAllChannels = true;
             EnabledChannels = Array.Empty<Guid>();
 

+ 8 - 0
MediaBrowser.Providers/Manager/ProviderManager.cs

@@ -158,6 +158,14 @@ namespace MediaBrowser.Providers.Manager
             var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
             using var response = await httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
 
+            if (response.StatusCode != HttpStatusCode.OK)
+            {
+                throw new HttpException("Invalid image received.")
+                {
+                    StatusCode = response.StatusCode
+                };
+            }
+
             var contentType = response.Content.Headers.ContentType.MediaType;
 
             // Workaround for tvheadend channel icons

+ 1 - 0
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -21,6 +21,7 @@
     <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.8" />
     <PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" />
     <PackageReference Include="PlaylistsNET" Version="1.1.2" />
+    <PackageReference Include="TMDbLib" Version="1.7.3-alpha" />
     <PackageReference Include="TvDbSharper" Version="3.2.2" />
   </ItemGroup>
 

+ 0 - 26
MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs

@@ -80,32 +80,6 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
             return TryGetValue(cacheKey, language, () => TvDbClient.Episodes.GetAsync(episodeTvdbId, cancellationToken));
         }
 
-        public async Task<List<EpisodeRecord>> GetAllEpisodesAsync(int tvdbId, string language,
-            CancellationToken cancellationToken)
-        {
-            // Traverse all episode pages and join them together
-            var episodes = new List<EpisodeRecord>();
-            var episodePage = await GetEpisodesPageAsync(tvdbId, new EpisodeQuery(), language, cancellationToken)
-                .ConfigureAwait(false);
-            episodes.AddRange(episodePage.Data);
-            if (!episodePage.Links.Next.HasValue || !episodePage.Links.Last.HasValue)
-            {
-                return episodes;
-            }
-
-            int next = episodePage.Links.Next.Value;
-            int last = episodePage.Links.Last.Value;
-
-            for (var page = next; page <= last; ++page)
-            {
-                episodePage = await GetEpisodesPageAsync(tvdbId, page, new EpisodeQuery(), language, cancellationToken)
-                    .ConfigureAwait(false);
-                episodes.AddRange(episodePage.Data);
-            }
-
-            return episodes;
-        }
-
         public Task<TvDbResponse<SeriesSearchResult[]>> GetSeriesByImdbIdAsync(
             string imdbId,
             string language,

+ 46 - 97
MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs

@@ -2,6 +2,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Globalization;
 using System.Linq;
 using System.Net.Http;
 using System.Threading;
@@ -12,25 +13,25 @@ using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
 using MediaBrowser.Model.Providers;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.Collections;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-using MediaBrowser.Providers.Plugins.Tmdb.Movies;
 
 namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
 {
     public class TmdbBoxSetImageProvider : IRemoteImageProvider, IHasOrder
     {
         private readonly IHttpClientFactory _httpClientFactory;
+        private readonly TmdbClientManager _tmdbClientManager;
 
-        public TmdbBoxSetImageProvider(IHttpClientFactory httpClientFactory)
+        public TmdbBoxSetImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
         {
             _httpClientFactory = httpClientFactory;
+            _tmdbClientManager = tmdbClientManager;
         }
 
-        public string Name => ProviderName;
+        public string Name => TmdbUtils.ProviderName;
 
-        public static string ProviderName => TmdbUtils.ProviderName;
+        public int Order => 0;
 
         public bool Supports(BaseItem item)
         {
@@ -48,112 +49,60 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
 
         public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
         {
-            var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
+            var tmdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
 
-            if (!string.IsNullOrEmpty(tmdbId))
+            if (tmdbId <= 0)
             {
-                var language = item.GetPreferredMetadataLanguage();
-
-                var mainResult = await TmdbBoxSetProvider.Current.GetMovieDbResult(tmdbId, null, cancellationToken).ConfigureAwait(false);
+                return Enumerable.Empty<RemoteImageInfo>();
+            }
 
-                if (mainResult != null)
-                {
-                    var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
+            var language = item.GetPreferredMetadataLanguage();
 
-                    var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
+            var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false);
 
-                    return GetImages(mainResult, language, tmdbImageUrl);
-                }
+            if (collection?.Images == null)
+            {
+                return Enumerable.Empty<RemoteImageInfo>();
             }
 
-            return new List<RemoteImageInfo>();
-        }
+            var remoteImages = new List<RemoteImageInfo>();
 
-        private IEnumerable<RemoteImageInfo> GetImages(CollectionResult obj, string language, string baseUrl)
-        {
-            var list = new List<RemoteImageInfo>();
-
-            var images = obj.Images ?? new CollectionImages();
-
-            list.AddRange(GetPosters(images).Select(i => new RemoteImageInfo
-            {
-                Url = baseUrl + i.File_Path,
-                CommunityRating = i.Vote_Average,
-                VoteCount = i.Vote_Count,
-                Width = i.Width,
-                Height = i.Height,
-                Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language),
-                ProviderName = Name,
-                Type = ImageType.Primary,
-                RatingType = RatingType.Score
-            }));
-
-            list.AddRange(GetBackdrops(images).Select(i => new RemoteImageInfo
+            for (var i = 0; i < collection.Images.Posters.Count; i++)
             {
-                Url = baseUrl + i.File_Path,
-                CommunityRating = i.Vote_Average,
-                VoteCount = i.Vote_Count,
-                Width = i.Width,
-                Height = i.Height,
-                ProviderName = Name,
-                Type = ImageType.Backdrop,
-                RatingType = RatingType.Score
-            }));
-
-            var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
-
-            return list.OrderByDescending(i =>
-            {
-                if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase))
-                {
-                    return 3;
-                }
-
-                if (!isLanguageEn)
+                var poster = collection.Images.Posters[i];
+                remoteImages.Add(new RemoteImageInfo
                 {
-                    if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
-                    {
-                        return 2;
-                    }
-                }
+                    Url = _tmdbClientManager.GetPosterUrl(poster.FilePath),
+                    CommunityRating = poster.VoteAverage,
+                    VoteCount = poster.VoteCount,
+                    Width = poster.Width,
+                    Height = poster.Height,
+                    Language = TmdbUtils.AdjustImageLanguage(poster.Iso_639_1, language),
+                    ProviderName = Name,
+                    Type = ImageType.Primary,
+                    RatingType = RatingType.Score
+                });
+            }
 
-                if (string.IsNullOrEmpty(i.Language))
+            for (var i = 0; i < collection.Images.Backdrops.Count; i++)
+            {
+                var backdrop = collection.Images.Backdrops[i];
+                remoteImages.Add(new RemoteImageInfo
                 {
-                    return isLanguageEn ? 3 : 2;
-                }
-
-                return 0;
-            })
-                .ThenByDescending(i => i.CommunityRating ?? 0)
-                .ThenByDescending(i => i.VoteCount ?? 0);
-        }
-
-        /// <summary>
-        /// Gets the posters.
-        /// </summary>
-        /// <param name="images">The images.</param>
-        /// <returns>IEnumerable{MovieDbProvider.Poster}.</returns>
-        private IEnumerable<Poster> GetPosters(CollectionImages images)
-        {
-            return images.Posters ?? new List<Poster>();
-        }
-
-        /// <summary>
-        /// Gets the backdrops.
-        /// </summary>
-        /// <param name="images">The images.</param>
-        /// <returns>IEnumerable{MovieDbProvider.Backdrop}.</returns>
-        private IEnumerable<Backdrop> GetBackdrops(CollectionImages images)
-        {
-            var eligibleBackdrops = images.Backdrops == null ? new List<Backdrop>() :
-                images.Backdrops;
+                    Url = _tmdbClientManager.GetBackdropUrl(backdrop.FilePath),
+                    CommunityRating = backdrop.VoteAverage,
+                    VoteCount = backdrop.VoteCount,
+                    Width = backdrop.Width,
+                    Height = backdrop.Height,
+                    ProviderName = Name,
+                    Type = ImageType.Backdrop,
+                    RatingType = RatingType.Score
+                });
+            }
 
-            return eligibleBackdrops.OrderByDescending(i => i.Vote_Average)
-                .ThenByDescending(i => i.Vote_Count);
+            return remoteImages.OrderByLanguageDescending(language);
         }
 
-        public int Order => 0;
-
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
         {
             return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);

+ 51 - 203
MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs

@@ -3,268 +3,116 @@
 using System;
 using System.Collections.Generic;
 using System.Globalization;
-using System.IO;
 using System.Linq;
 using System.Net.Http;
-using System.Net.Http.Headers;
 using System.Threading;
 using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.Collections;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-using MediaBrowser.Providers.Plugins.Tmdb.Movies;
-using Microsoft.Extensions.Logging;
 
 namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets
 {
     public class TmdbBoxSetProvider : IRemoteMetadataProvider<BoxSet, BoxSetInfo>
     {
-        private const string GetCollectionInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/collection/{0}?api_key={1}&append_to_response=images";
-
-        internal static TmdbBoxSetProvider Current;
-
-        private readonly ILogger<TmdbBoxSetProvider> _logger;
-        private readonly IJsonSerializer _json;
-        private readonly IServerConfigurationManager _config;
-        private readonly IFileSystem _fileSystem;
         private readonly IHttpClientFactory _httpClientFactory;
-        private readonly ILibraryManager _libraryManager;
+        private readonly TmdbClientManager _tmdbClientManager;
 
-        public TmdbBoxSetProvider(
-            ILogger<TmdbBoxSetProvider> logger,
-            IJsonSerializer json,
-            IServerConfigurationManager config,
-            IFileSystem fileSystem,
-            IHttpClientFactory httpClientFactory,
-            ILibraryManager libraryManager)
+        public TmdbBoxSetProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
         {
-            _logger = logger;
-            _json = json;
-            _config = config;
-            _fileSystem = fileSystem;
             _httpClientFactory = httpClientFactory;
-            _libraryManager = libraryManager;
-            Current = this;
+            _tmdbClientManager = tmdbClientManager;
         }
 
-        private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+        public string Name => TmdbUtils.ProviderName;
 
         public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(BoxSetInfo searchInfo, CancellationToken cancellationToken)
         {
-            var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb);
+            var tmdbId = Convert.ToInt32(searchInfo.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
+            var language = searchInfo.MetadataLanguage;
 
-            if (!string.IsNullOrEmpty(tmdbId))
+            if (tmdbId > 0)
             {
-                await EnsureInfo(tmdbId, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
-
-                var dataFilePath = GetDataFilePath(_config.ApplicationPaths, tmdbId, searchInfo.MetadataLanguage);
-                var info = _json.DeserializeFromFile<CollectionResult>(dataFilePath);
-
-                var images = (info.Images ?? new CollectionImages()).Posters ?? new List<Poster>();
-
-                var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
+                var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false);
 
-                var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
+                if (collection == null)
+                {
+                    return Enumerable.Empty<RemoteSearchResult>();
+                }
 
                 var result = new RemoteSearchResult
                 {
-                    Name = info.Name,
-                    SearchProviderName = Name,
-                    ImageUrl = images.Count == 0 ? null : (tmdbImageUrl + images[0].File_Path)
+                    Name = collection.Name,
+                    SearchProviderName = Name
                 };
 
-                result.SetProviderId(MetadataProvider.Tmdb, info.Id.ToString(_usCulture));
-
-                return new[] { result };
-            }
-
-            return await new TmdbSearch(_logger, _json, _libraryManager).GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false);
-        }
-
-        public async Task<MetadataResult<BoxSet>> GetMetadata(BoxSetInfo id, CancellationToken cancellationToken)
-        {
-            var tmdbId = id.GetProviderId(MetadataProvider.Tmdb);
-
-            // We don't already have an Id, need to fetch it
-            if (string.IsNullOrEmpty(tmdbId))
-            {
-                var searchResults = await new TmdbSearch(_logger, _json, _libraryManager).GetSearchResults(id, cancellationToken).ConfigureAwait(false);
-
-                var searchResult = searchResults.FirstOrDefault();
-
-                if (searchResult != null)
-                {
-                    tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb);
-                }
-            }
-
-            var result = new MetadataResult<BoxSet>();
-
-            if (!string.IsNullOrEmpty(tmdbId))
-            {
-                var mainResult = await GetMovieDbResult(tmdbId, id.MetadataLanguage, cancellationToken).ConfigureAwait(false);
-
-                if (mainResult != null)
+                if (collection.Images != null)
                 {
-                    result.HasMetadata = true;
-                    result.Item = GetItem(mainResult);
+                    result.ImageUrl = _tmdbClientManager.GetPosterUrl(collection.PosterPath);
                 }
-            }
 
-            return result;
-        }
+                result.SetProviderId(MetadataProvider.Tmdb, collection.Id.ToString(CultureInfo.InvariantCulture));
 
-        internal async Task<CollectionResult> GetMovieDbResult(string tmdbId, string language, CancellationToken cancellationToken)
-        {
-            if (string.IsNullOrEmpty(tmdbId))
-            {
-                throw new ArgumentNullException(nameof(tmdbId));
+                return new[] { result };
             }
 
-            await EnsureInfo(tmdbId, language, cancellationToken).ConfigureAwait(false);
-
-            var dataFilePath = GetDataFilePath(_config.ApplicationPaths, tmdbId, language);
+            var collectionSearchResults = await _tmdbClientManager.SearchCollectionAsync(searchInfo.Name, language, cancellationToken).ConfigureAwait(false);
 
-            if (!string.IsNullOrEmpty(dataFilePath))
+            var collections = new List<RemoteSearchResult>();
+            for (var i = 0; i < collectionSearchResults.Count; i++)
             {
-                return _json.DeserializeFromFile<CollectionResult>(dataFilePath);
-            }
-
-            return null;
-        }
-
-        private BoxSet GetItem(CollectionResult obj)
-        {
-            var item = new BoxSet
-            {
-                Name = obj.Name,
-                Overview = obj.Overview
-            };
-
-            item.SetProviderId(MetadataProvider.Tmdb, obj.Id.ToString(_usCulture));
-
-            return item;
-        }
-
-        private async Task DownloadInfo(string tmdbId, string preferredMetadataLanguage, CancellationToken cancellationToken)
-        {
-            var mainResult = await FetchMainResult(tmdbId, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
+                var collection = new RemoteSearchResult
+                {
+                    Name = collectionSearchResults[i].Name,
+                    SearchProviderName = Name
+                };
+                collection.SetProviderId(MetadataProvider.Tmdb, collectionSearchResults[i].Id.ToString(CultureInfo.InvariantCulture));
 
-            if (mainResult == null)
-            {
-                return;
+                collections.Add(collection);
             }
 
-            var dataFilePath = GetDataFilePath(_config.ApplicationPaths, tmdbId, preferredMetadataLanguage);
-
-            Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
-
-            _json.SerializeToFile(mainResult, dataFilePath);
+            return collections;
         }
 
-        private async Task<CollectionResult> FetchMainResult(string id, string language, CancellationToken cancellationToken)
+        public async Task<MetadataResult<BoxSet>> GetMetadata(BoxSetInfo id, CancellationToken cancellationToken)
         {
-            var url = string.Format(CultureInfo.InvariantCulture, GetCollectionInfo3, id, TmdbUtils.ApiKey);
-
-            if (!string.IsNullOrEmpty(language))
+            var tmdbId = Convert.ToInt32(id.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
+            var language = id.MetadataLanguage;
+            // We don't already have an Id, need to fetch it
+            if (tmdbId <= 0)
             {
-                url += string.Format(CultureInfo.InvariantCulture, "&language={0}", TmdbMovieProvider.NormalizeLanguage(language));
+                var searchResults = await _tmdbClientManager.SearchCollectionAsync(id.Name, language, cancellationToken).ConfigureAwait(false);
 
-                // Get images in english and with no language
-                url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language);
+                if (searchResults != null && searchResults.Count > 0)
+                {
+                    tmdbId = searchResults[0].Id;
+                }
             }
 
-            cancellationToken.ThrowIfCancellationRequested();
+            var result = new MetadataResult<BoxSet>();
 
-            using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
-            foreach (var header in TmdbUtils.AcceptHeaders)
+            if (tmdbId > 0)
             {
-                requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
-            }
-
-            using var mainResponse = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
-            await using var stream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
-            var mainResult = await _json.DeserializeFromStreamAsync<CollectionResult>(stream).ConfigureAwait(false);
+                var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false);
 
-            cancellationToken.ThrowIfCancellationRequested();
-
-            if (mainResult != null && string.IsNullOrEmpty(mainResult.Name))
-            {
-                if (!string.IsNullOrEmpty(language) && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
+                if (collection != null)
                 {
-                    url = string.Format(CultureInfo.InvariantCulture, GetCollectionInfo3, id, TmdbUtils.ApiKey) + "&language=en";
-
-                    if (!string.IsNullOrEmpty(language))
+                    var item = new BoxSet
                     {
-                        // Get images in english and with no language
-                        url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language);
-                    }
-
-                    using var langRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);
-                    foreach (var header in TmdbUtils.AcceptHeaders)
-                    {
-                        langRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
-                    }
-
-                    await using var langStream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
-                    mainResult = await _json.DeserializeFromStreamAsync<CollectionResult>(langStream).ConfigureAwait(false);
-                }
-            }
+                        Name = collection.Name,
+                        Overview = collection.Overview
+                    };
 
-            return mainResult;
-        }
-
-        internal Task EnsureInfo(string tmdbId, string preferredMetadataLanguage, CancellationToken cancellationToken)
-        {
-            var path = GetDataFilePath(_config.ApplicationPaths, tmdbId, preferredMetadataLanguage);
+                    item.SetProviderId(MetadataProvider.Tmdb, collection.Id.ToString(CultureInfo.InvariantCulture));
 
-            var fileInfo = _fileSystem.GetFileSystemInfo(path);
-
-            if (fileInfo.Exists)
-            {
-                // If it's recent or automatic updates are enabled, don't re-download
-                if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
-                {
-                    return Task.CompletedTask;
+                    result.HasMetadata = true;
+                    result.Item = item;
                 }
             }
 
-            return DownloadInfo(tmdbId, preferredMetadataLanguage, cancellationToken);
-        }
-
-        public string Name => TmdbUtils.ProviderName;
-
-        private static string GetDataFilePath(IApplicationPaths appPaths, string tmdbId, string preferredLanguage)
-        {
-            var path = GetDataPath(appPaths, tmdbId);
-
-            var filename = string.Format(CultureInfo.InvariantCulture, "all-{0}.json", preferredLanguage ?? string.Empty);
-
-            return Path.Combine(path, filename);
-        }
-
-        private static string GetDataPath(IApplicationPaths appPaths, string tmdbId)
-        {
-            var dataPath = GetCollectionsDataPath(appPaths);
-
-            return Path.Combine(dataPath, tmdbId);
-        }
-
-        private static string GetCollectionsDataPath(IApplicationPaths appPaths)
-        {
-            var dataPath = Path.Combine(appPaths.CachePath, "tmdb-collections");
-
-            return dataPath;
+            return result;
         }
 
         public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)

+ 0 - 14
MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionImages.cs

@@ -1,14 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Collections
-{
-    public class CollectionImages
-    {
-        public List<Backdrop> Backdrops { get; set; }
-
-        public List<Poster> Posters { get; set; }
-    }
-}

+ 0 - 23
MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionResult.cs

@@ -1,23 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Collections
-{
-    public class CollectionResult
-    {
-        public int Id { get; set; }
-
-        public string Name { get; set; }
-
-        public string Overview { get; set; }
-
-        public string Poster_Path { get; set; }
-
-        public string Backdrop_Path { get; set; }
-
-        public List<Part> Parts { get; set; }
-
-        public CollectionImages Images { get; set; }
-    }
-}

+ 0 - 17
MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/Part.cs

@@ -1,17 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Collections
-{
-    public class Part
-    {
-        public string Title { get; set; }
-
-        public int Id { get; set; }
-
-        public string Release_Date { get; set; }
-
-        public string Poster_Path { get; set; }
-
-        public string Backdrop_Path { get; set; }
-    }
-}

+ 0 - 21
MediaBrowser.Providers/Plugins/Tmdb/Models/General/Backdrop.cs

@@ -1,21 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
-    public class Backdrop
-    {
-        public double Aspect_Ratio { get; set; }
-
-        public string File_Path { get; set; }
-
-        public int Height { get; set; }
-
-        public string Iso_639_1 { get; set; }
-
-        public double Vote_Average { get; set; }
-
-        public int Vote_Count { get; set; }
-
-        public int Width { get; set; }
-    }
-}

+ 0 - 19
MediaBrowser.Providers/Plugins/Tmdb/Models/General/Crew.cs

@@ -1,19 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
-    public class Crew
-    {
-        public int Id { get; set; }
-
-        public string Credit_Id { get; set; }
-
-        public string Name { get; set; }
-
-        public string Department { get; set; }
-
-        public string Job { get; set; }
-
-        public string Profile_Path { get; set; }
-    }
-}

+ 0 - 17
MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs

@@ -1,17 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
-    public class ExternalIds
-    {
-        public string Imdb_Id { get; set; }
-
-        public object Freebase_Id { get; set; }
-
-        public string Freebase_Mid { get; set; }
-
-        public int? Tvdb_Id { get; set; }
-
-        public int? Tvrage_Id { get; set; }
-    }
-}

+ 0 - 11
MediaBrowser.Providers/Plugins/Tmdb/Models/General/Genre.cs

@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
-    public class Genre
-    {
-        public int Id { get; set; }
-
-        public string Name { get; set; }
-    }
-}

+ 0 - 13
MediaBrowser.Providers/Plugins/Tmdb/Models/General/Images.cs

@@ -1,13 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
-    public class Images
-    {
-        public List<Backdrop> Backdrops { get; set; }
-
-        public List<Poster> Posters { get; set; }
-    }
-}

+ 0 - 11
MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keyword.cs

@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
-    public class Keyword
-    {
-        public int Id { get; set; }
-
-        public string Name { get; set; }
-    }
-}

+ 0 - 11
MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keywords.cs

@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
-    public class Keywords
-    {
-        public List<Keyword> Results { get; set; }
-    }
-}

+ 0 - 21
MediaBrowser.Providers/Plugins/Tmdb/Models/General/Poster.cs

@@ -1,21 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
-    public class Poster
-    {
-        public double Aspect_Ratio { get; set; }
-
-        public string File_Path { get; set; }
-
-        public int Height { get; set; }
-
-        public string Iso_639_1 { get; set; }
-
-        public double Vote_Average { get; set; }
-
-        public int Vote_Count { get; set; }
-
-        public int Width { get; set; }
-    }
-}

+ 0 - 17
MediaBrowser.Providers/Plugins/Tmdb/Models/General/Profile.cs

@@ -1,17 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
-    public class Profile
-    {
-        public string File_Path { get; set; }
-
-        public int Width { get; set; }
-
-        public int Height { get; set; }
-
-        public object Iso_639_1 { get; set; }
-
-        public double Aspect_Ratio { get; set; }
-    }
-}

+ 0 - 23
MediaBrowser.Providers/Plugins/Tmdb/Models/General/Still.cs

@@ -1,23 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
-    public class Still
-    {
-        public double Aspect_Ratio { get; set; }
-
-        public string File_Path { get; set; }
-
-        public int Height { get; set; }
-
-        public string Id { get; set; }
-
-        public string Iso_639_1 { get; set; }
-
-        public double Vote_Average { get; set; }
-
-        public int Vote_Count { get; set; }
-
-        public int Width { get; set; }
-    }
-}

+ 0 - 11
MediaBrowser.Providers/Plugins/Tmdb/Models/General/StillImages.cs

@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
-    public class StillImages
-    {
-        public List<Still> Stills { get; set; }
-    }
-}

+ 0 - 23
MediaBrowser.Providers/Plugins/Tmdb/Models/General/Video.cs

@@ -1,23 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
-    public class Video
-    {
-        public string Id { get; set; }
-
-        public string Iso_639_1 { get; set; }
-
-        public string Iso_3166_1 { get; set; }
-
-        public string Key { get; set; }
-
-        public string Name { get; set; }
-
-        public string Site { get; set; }
-
-        public string Size { get; set; }
-
-        public string Type { get; set; }
-    }
-}

+ 0 - 11
MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs

@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.General
-{
-    public class Videos
-    {
-        public IReadOnlyList<Video> Results { get; set; }
-    }
-}

+ 0 - 15
MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/BelongsToCollection.cs

@@ -1,15 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
-{
-    public class BelongsToCollection
-    {
-        public int Id { get; set; }
-
-        public string Name { get; set; }
-
-        public string Poster_Path { get; set; }
-
-        public string Backdrop_Path { get; set; }
-    }
-}

+ 0 - 19
MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Cast.cs

@@ -1,19 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
-{
-    public class Cast
-    {
-        public int Id { get; set; }
-
-        public string Name { get; set; }
-
-        public string Character { get; set; }
-
-        public int Order { get; set; }
-
-        public int Cast_Id { get; set; }
-
-        public string Profile_Path { get; set; }
-    }
-}

+ 0 - 14
MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Casts.cs

@@ -1,14 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
-{
-    public class Casts
-    {
-        public List<Cast> Cast { get; set; }
-
-        public List<Crew> Crew { get; set; }
-    }
-}

+ 0 - 15
MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Country.cs

@@ -1,15 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
-{
-    public class Country
-    {
-        public string Iso_3166_1 { get; set; }
-
-        public string Certification { get; set; }
-
-        public DateTime Release_Date { get; set; }
-    }
-}

+ 0 - 80
MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs

@@ -1,80 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
-{
-    public class MovieResult
-    {
-        public bool Adult { get; set; }
-
-        public string Backdrop_Path { get; set; }
-
-        public BelongsToCollection Belongs_To_Collection { get; set; }
-
-        public long Budget { get; set; }
-
-        public List<Genre> Genres { get; set; }
-
-        public string Homepage { get; set; }
-
-        public int Id { get; set; }
-
-        public string Imdb_Id { get; set; }
-
-        public string Original_Title { get; set; }
-
-        public string Original_Name { get; set; }
-
-        public string Overview { get; set; }
-
-        public double Popularity { get; set; }
-
-        public string Poster_Path { get; set; }
-
-        public List<ProductionCompany> Production_Companies { get; set; }
-
-        public List<ProductionCountry> Production_Countries { get; set; }
-
-        public string Release_Date { get; set; }
-
-        public long Revenue { get; set; }
-
-        public int Runtime { get; set; }
-
-        public List<SpokenLanguage> Spoken_Languages { get; set; }
-
-        public string Status { get; set; }
-
-        public string Tagline { get; set; }
-
-        public string Title { get; set; }
-
-        public string Name { get; set; }
-
-        public double Vote_Average { get; set; }
-
-        public int Vote_Count { get; set; }
-
-        public Casts Casts { get; set; }
-
-        public Releases Releases { get; set; }
-
-        public Images Images { get; set; }
-
-        public Keywords Keywords { get; set; }
-
-        public Trailers Trailers { get; set; }
-
-        public string GetOriginalTitle()
-        {
-            return Original_Name ?? Original_Title;
-        }
-
-        public string GetTitle()
-        {
-            return Name ?? Title ?? GetOriginalTitle();
-        }
-    }
-}

+ 0 - 11
MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCompany.cs

@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
-{
-    public class ProductionCompany
-    {
-        public string Name { get; set; }
-
-        public int Id { get; set; }
-    }
-}

+ 0 - 11
MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCountry.cs

@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
-{
-    public class ProductionCountry
-    {
-        public string Iso_3166_1 { get; set; }
-
-        public string Name { get; set; }
-    }
-}

+ 0 - 11
MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Releases.cs

@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
-{
-    public class Releases
-    {
-        public List<Country> Countries { get; set; }
-    }
-}

+ 0 - 11
MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/SpokenLanguage.cs

@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
-{
-    public class SpokenLanguage
-    {
-        public string Iso_639_1 { get; set; }
-
-        public string Name { get; set; }
-    }
-}

+ 0 - 11
MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs

@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
-{
-    public class Trailers
-    {
-        public IReadOnlyList<Youtube> Youtube { get; set; }
-    }
-}

+ 0 - 13
MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Youtube.cs

@@ -1,13 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
-{
-    public class Youtube
-    {
-        public string Name { get; set; }
-
-        public string Size { get; set; }
-
-        public string Source { get; set; }
-    }
-}

+ 0 - 12
MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs

@@ -1,12 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.People
-{
-    public class PersonImages
-    {
-        public IReadOnlyList<Profile> Profiles { get; set; }
-    }
-}

+ 0 - 38
MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonResult.cs

@@ -1,38 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.People
-{
-    public class PersonResult
-    {
-        public bool Adult { get; set; }
-
-        public List<string> Also_Known_As { get; set; }
-
-        public string Biography { get; set; }
-
-        public string Birthday { get; set; }
-
-        public string Deathday { get; set; }
-
-        public string Homepage { get; set; }
-
-        public int Id { get; set; }
-
-        public string Imdb_Id { get; set; }
-
-        public string Name { get; set; }
-
-        public string Place_Of_Birth { get; set; }
-
-        public double Popularity { get; set; }
-
-        public string Profile_Path { get; set; }
-
-        public PersonImages Images { get; set; }
-
-        public ExternalIds External_Ids { get; set; }
-    }
-}

+ 0 - 11
MediaBrowser.Providers/Plugins/Tmdb/Models/Search/ExternalIdLookupResult.cs

@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search
-{
-    public class ExternalIdLookupResult
-    {
-        public List<TvResult> Tv_Results { get; set; }
-    }
-}

+ 0 - 78
MediaBrowser.Providers/Plugins/Tmdb/Models/Search/MovieResult.cs

@@ -1,78 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search
-{
-    public class MovieResult
-    {
-        /// <summary>
-        /// Gets or sets a value indicating whether this <see cref="MovieResult" /> is adult.
-        /// </summary>
-        /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value>
-        public bool Adult { get; set; }
-
-        /// <summary>
-        /// Gets or sets the backdrop_path.
-        /// </summary>
-        /// <value>The backdrop_path.</value>
-        public string Backdrop_Path { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        public int Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets the original_title.
-        /// </summary>
-        /// <value>The original_title.</value>
-        public string Original_Title { get; set; }
-
-        /// <summary>
-        /// Gets or sets the original_name.
-        /// </summary>
-        /// <value>The original_name.</value>
-        public string Original_Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the release_date.
-        /// </summary>
-        /// <value>The release_date.</value>
-        public string Release_Date { get; set; }
-
-        /// <summary>
-        /// Gets or sets the poster_path.
-        /// </summary>
-        /// <value>The poster_path.</value>
-        public string Poster_Path { get; set; }
-
-        /// <summary>
-        /// Gets or sets the popularity.
-        /// </summary>
-        /// <value>The popularity.</value>
-        public double Popularity { get; set; }
-
-        /// <summary>
-        /// Gets or sets the title.
-        /// </summary>
-        /// <value>The title.</value>
-        public string Title { get; set; }
-
-        /// <summary>
-        /// Gets or sets the vote_average.
-        /// </summary>
-        /// <value>The vote_average.</value>
-        public double Vote_Average { get; set; }
-
-        /// <summary>
-        /// For collection search results.
-        /// </summary>
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the vote_count.
-        /// </summary>
-        /// <value>The vote_count.</value>
-        public int Vote_Count { get; set; }
-    }
-}

+ 0 - 31
MediaBrowser.Providers/Plugins/Tmdb/Models/Search/PersonSearchResult.cs

@@ -1,31 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search
-{
-    public class PersonSearchResult
-    {
-        /// <summary>
-        /// Gets or sets a value indicating whether this <see cref="PersonSearchResult" /> is adult.
-        /// </summary>
-        /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value>
-        public bool Adult { get; set; }
-
-        /// <summary>
-        /// Gets or sets the id.
-        /// </summary>
-        /// <value>The id.</value>
-        public int Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the profile_ path.
-        /// </summary>
-        /// <value>The profile_ path.</value>
-        public string Profile_Path { get; set; }
-    }
-}

+ 0 - 33
MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TmdbSearchResult.cs

@@ -1,33 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search
-{
-    public class TmdbSearchResult<T>
-    {
-        /// <summary>
-        /// Gets or sets the page.
-        /// </summary>
-        /// <value>The page.</value>
-        public int Page { get; set; }
-
-        /// <summary>
-        /// Gets or sets the results.
-        /// </summary>
-        /// <value>The results.</value>
-        public List<T> Results { get; set; }
-
-        /// <summary>
-        /// Gets or sets the total_pages.
-        /// </summary>
-        /// <value>The total_pages.</value>
-        public int Total_Pages { get; set; }
-
-        /// <summary>
-        /// Gets or sets the total_results.
-        /// </summary>
-        /// <value>The total_results.</value>
-        public int Total_Results { get; set; }
-    }
-}

+ 0 - 25
MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TvResult.cs

@@ -1,25 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Search
-{
-    public class TvResult
-    {
-        public string Backdrop_Path { get; set; }
-
-        public string First_Air_Date { get; set; }
-
-        public int Id { get; set; }
-
-        public string Original_Name { get; set; }
-
-        public string Poster_Path { get; set; }
-
-        public double Popularity { get; set; }
-
-        public string Name { get; set; }
-
-        public double Vote_Average { get; set; }
-
-        public int Vote_Count { get; set; }
-    }
-}

+ 0 - 19
MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Cast.cs

@@ -1,19 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
-    public class Cast
-    {
-        public string Character { get; set; }
-
-        public string Credit_Id { get; set; }
-
-        public int Id { get; set; }
-
-        public string Name { get; set; }
-
-        public string Profile_Path { get; set; }
-
-        public int Order { get; set; }
-    }
-}

+ 0 - 11
MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRating.cs

@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
-    public class ContentRating
-    {
-        public string Iso_3166_1 { get; set; }
-
-        public string Rating { get; set; }
-    }
-}

+ 0 - 11
MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRatings.cs

@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
-    public class ContentRatings
-    {
-        public List<ContentRating> Results { get; set; }
-    }
-}

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