aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.ci/azure-pipelines-api-client.yml28
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--Emby.Dlna/PlayTo/PlayToController.cs8
-rw-r--r--Emby.Dlna/PlayTo/PlayToManager.cs18
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs2
-rw-r--r--Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs7
-rw-r--r--Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs11
-rw-r--r--Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs2
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketConnection.cs5
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-PT.json2
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs2
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs36
-rw-r--r--Emby.Server.Implementations/Session/SessionWebSocketListener.cs3
-rw-r--r--Emby.Server.Implementations/Session/WebSocketController.cs3
-rw-r--r--Emby.Server.Implementations/SyncPlay/SyncPlayController.cs8
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs38
-rw-r--r--Jellyfin.Api/Controllers/SessionController.cs5
-rw-r--r--Jellyfin.Api/Helpers/MediaInfoHelper.cs2
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs5
-rw-r--r--Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs64
-rw-r--r--Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinderProvider.cs29
-rw-r--r--Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs14
-rw-r--r--Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs14
-rw-r--r--Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs9
-rw-r--r--Jellyfin.Data/Entities/User.cs5
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedNotifier.cs3
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationCancelledNotifier.cs3
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedNotifier.cs3
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledNotifier.cs3
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallingNotifier.cs3
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledNotifier.cs3
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedNotifier.cs3
-rw-r--r--Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs3
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs464
-rw-r--r--Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs28
-rw-r--r--Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs5
-rw-r--r--Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs2
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs4
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs4
-rw-r--r--Jellyfin.Server/Filters/WebsocketModelFilter.cs30
-rw-r--r--Jellyfin.Server/Middleware/ExceptionMiddleware.cs4
-rw-r--r--MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs27
-rw-r--r--MediaBrowser.Controller/Session/ISessionController.cs3
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs8
-rw-r--r--MediaBrowser.Controller/Session/SessionInfo.cs4
-rw-r--r--MediaBrowser.Controller/SyncPlay/GroupInfo.cs52
-rw-r--r--MediaBrowser.Model/Configuration/LibraryOptions.cs2
-rw-r--r--MediaBrowser.Model/Dlna/StreamBuilder.cs10
-rw-r--r--MediaBrowser.Model/Extensions/EnumerableExtensions.cs46
-rw-r--r--MediaBrowser.Model/Net/WebSocketMessage.cs3
-rw-r--r--MediaBrowser.Model/Session/ClientCapabilities.cs4
-rw-r--r--MediaBrowser.Model/Session/SessionMessageType.cs50
-rw-r--r--MediaBrowser.Model/Users/UserPolicy.cs4
-rw-r--r--MediaBrowser.Providers/Manager/ProviderManager.cs8
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj1
-rw-r--r--MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs26
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs143
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs254
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionImages.cs14
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionResult.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/Part.cs17
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Backdrop.cs21
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Crew.cs19
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs17
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Genre.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Images.cs13
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keyword.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keywords.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Poster.cs21
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Profile.cs17
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Still.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/StillImages.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Video.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/BelongsToCollection.cs15
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Cast.cs19
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Casts.cs14
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Country.cs15
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs80
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCompany.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCountry.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Releases.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/SpokenLanguage.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Youtube.cs13
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs12
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonResult.cs38
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Search/ExternalIdLookupResult.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Search/MovieResult.cs78
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Search/PersonSearchResult.cs31
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TmdbSearchResult.cs33
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TvResult.cs25
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Cast.cs19
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRating.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRatings.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/CreatedBy.cs13
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Credits.cs14
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Episode.cs23
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeCredits.cs16
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeResult.cs38
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/GuestStar.cs19
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Network.cs11
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Season.cs17
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonImages.cs12
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonResult.cs33
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeriesResult.cs71
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs309
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs212
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageSettings.cs22
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs3
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs128
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs457
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs302
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettingsResult.cs9
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs34
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs102
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs247
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs111
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs240
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs156
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs106
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs241
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs159
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs523
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs469
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs118
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs43
-rw-r--r--MediaBrowser.Providers/TV/DummySeasonProvider.cs229
-rw-r--r--MediaBrowser.Providers/TV/MissingEpisodeProvider.cs404
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs41
-rw-r--r--apiclient/templates/typescript/axios/generate.sh19
-rw-r--r--apiclient/templates/typescript/axios/stable.sh9
-rw-r--r--apiclient/templates/typescript/axios/unstable.sh9
-rwxr-xr-xdeployment/build.windows.amd644
-rw-r--r--fedora/jellyfin.spec10
-rw-r--r--tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj2
-rw-r--r--tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs229
-rw-r--r--tests/Jellyfin.Api.Tests/ModelBinders/TestType.cs17
-rw-r--r--tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs32
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj2
140 files changed, 2790 insertions, 4749 deletions
diff --git a/.ci/azure-pipelines-api-client.yml b/.ci/azure-pipelines-api-client.yml
index 7f428aec1..fc89b90d4 100644
--- a/.ci/azure-pipelines-api-client.yml
+++ b/.ci/azure-pipelines-api-client.yml
@@ -28,30 +28,40 @@ jobs:
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
+## 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/unstable.sh'
+ script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory) $(Build.BuildNumber)"
+# 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)"
+
+## Run npm install
+ - task: Npm@1
+ displayName: 'Install npm dependencies'
+ inputs:
+ command: install
+ workingDir: ./apiclient/generated/typescript/axios
+
+## Publish npm packages
+# Unstable
- task: Npm@1
displayName: 'Publish unstable typescript axios client'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
inputs:
command: publish
publishRegistry: useFeed
- publishFeed: unstable
+ publishFeed: 'jellyfin/unstable'
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/stable.sh'
-
- task: Npm@1
displayName: 'Publish stable typescript axios client'
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 99060d0b0..7b4772730 100644
--- a/CONTRIBUTORS.md
+++ b/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
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 63fd8ce5a..a5b8e2b3c 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/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);
}
diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs
index 21877f121..e93aef304 100644
--- a/Emby.Dlna/PlayTo/PlayToManager.cs
+++ b/Emby.Dlna/PlayTo/PlayToManager.cs
@@ -217,15 +217,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
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 7a46fdf2e..0c8b0339b 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -97,6 +97,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.Mvc;
@@ -537,6 +538,7 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton(_fileSystemManager);
ServiceCollection.AddSingleton<TvdbClientManager>();
+ ServiceCollection.AddSingleton<TmdbClientManager>();
ServiceCollection.AddSingleton(_networkManager);
diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index c9d21d963..ff64e217a 100644
--- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
+++ b/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)
{
diff --git a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
index 44d2580d6..824bb85f4 100644
--- a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs
+++ b/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();
diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
index 1da717e75..1989e9ed2 100644
--- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
+++ b/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)
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index 7eae4e764..fed2addf8 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/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);
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json
index c1fb65743..a7c17ecb6 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-PT.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json
@@ -26,7 +26,7 @@
"HeaderLiveTV": "TV em Direto",
"HeaderNextUp": "A Seguir",
"HeaderRecordingGroups": "Grupos de Gravação",
- "HomeVideos": "Videos caseiros",
+ "HomeVideos": "Vídeos Caseiros",
"Inherit": "Herdar",
"ItemAddedWithName": "{0} foi adicionado à biblioteca",
"ItemRemovedWithName": "{0} foi removido da biblioteca",
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
index 691408167..26ef19354 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
+++ b/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;
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index e42d47853..b1ab20da2 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/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();
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index 15c2af220..a5f847953 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/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);
diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs
index 94604ca1e..b986ffa1c 100644
--- a/Emby.Server.Implementations/Session/WebSocketController.cs
+++ b/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)
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
index 80b977731..538479512 100644
--- a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
+++ b/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 />
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 7afec1219..05efe2355 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -333,7 +333,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
- /// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
+ /// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
@@ -364,7 +364,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? quality,
[FromQuery] string? tag,
[FromQuery] bool? cropWhitespace,
- [FromQuery] string? format,
+ [FromQuery] ImageFormat? format,
[FromQuery] bool? addPlayedIndicator,
[FromQuery] double? percentPlayed,
[FromQuery] int? unplayedCount,
@@ -443,7 +443,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? quality,
[FromRoute, Required] string tag,
[FromQuery] bool? cropWhitespace,
- [FromRoute, Required] string format,
+ [FromRoute, Required] ImageFormat format,
[FromQuery] bool? addPlayedIndicator,
[FromRoute, Required] double percentPlayed,
[FromRoute, Required] int unplayedCount,
@@ -516,7 +516,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromQuery] string tag,
- [FromQuery] string format,
+ [FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] double? percentPlayed,
@@ -595,7 +595,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromQuery] string tag,
- [FromQuery] string format,
+ [FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] double? percentPlayed,
@@ -674,7 +674,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromQuery] string tag,
- [FromQuery] string format,
+ [FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] double? percentPlayed,
@@ -753,7 +753,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromQuery] string tag,
- [FromQuery] string format,
+ [FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] double? percentPlayed,
@@ -832,7 +832,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] string name,
[FromRoute, Required] ImageType imageType,
[FromRoute, Required] string tag,
- [FromRoute, Required] string format,
+ [FromRoute, Required] ImageFormat format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] double? percentPlayed,
@@ -911,7 +911,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid userId,
[FromRoute, Required] ImageType imageType,
[FromQuery] string? tag,
- [FromQuery] string? format,
+ [FromQuery] ImageFormat? format,
[FromQuery] int? maxWidth,
[FromQuery] int? maxHeight,
[FromQuery] double? percentPlayed,
@@ -1038,7 +1038,7 @@ namespace Jellyfin.Api.Controllers
ImageType imageType,
int? imageIndex,
string? tag,
- string? format,
+ ImageFormat? format,
int? maxWidth,
int? maxHeight,
double? percentPlayed,
@@ -1128,12 +1128,11 @@ namespace Jellyfin.Api.Controllers
isHeadRequest).ConfigureAwait(false);
}
- private ImageFormat[] GetOutputFormats(string? format)
+ private ImageFormat[] GetOutputFormats(ImageFormat? format)
{
- if (!string.IsNullOrWhiteSpace(format)
- && Enum.TryParse(format, true, out ImageFormat parsedFormat))
+ if (format.HasValue)
{
- return new[] { parsedFormat };
+ return new[] { format.Value };
}
return GetClientSupportedFormats();
@@ -1157,7 +1156,7 @@ namespace Jellyfin.Api.Controllers
var acceptParam = Request.Query[HeaderNames.Accept];
- var supportsWebP = SupportsFormat(supportedFormats, acceptParam, "webp", false);
+ var supportsWebP = SupportsFormat(supportedFormats, acceptParam, ImageFormat.Webp, false);
if (!supportsWebP)
{
@@ -1179,7 +1178,7 @@ namespace Jellyfin.Api.Controllers
formats.Add(ImageFormat.Jpg);
formats.Add(ImageFormat.Png);
- if (SupportsFormat(supportedFormats, acceptParam, "gif", true))
+ if (SupportsFormat(supportedFormats, acceptParam, ImageFormat.Gif, true))
{
formats.Add(ImageFormat.Gif);
}
@@ -1187,9 +1186,10 @@ namespace Jellyfin.Api.Controllers
return formats.ToArray();
}
- private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string acceptParam, string format, bool acceptAll)
+ private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string acceptParam, ImageFormat format, bool acceptAll)
{
- var mimeType = "image/" + format;
+ var normalized = format.ToString().ToLowerInvariant();
+ var mimeType = "image/" + normalized;
if (requestAcceptTypes.Contains(mimeType))
{
@@ -1201,7 +1201,7 @@ namespace Jellyfin.Api.Controllers
return true;
}
- return string.Equals(acceptParam, format, StringComparison.OrdinalIgnoreCase);
+ return string.Equals(acceptParam, normalized, StringComparison.OrdinalIgnoreCase);
}
private async Task<ActionResult> GetImageResult(
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index 39bf6e6dc..565670962 100644
--- a/Jellyfin.Api/Controllers/SessionController.cs
+++ b/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
diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index 1207fb513..e78f63b25 100644
--- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs
+++ b/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)
{
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 64d1227f7..0db1fabff 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/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();
diff --git a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs
new file mode 100644
index 000000000..13469194a
--- /dev/null
+++ b/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;
+ }
+ }
+}
diff --git a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinderProvider.cs b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinderProvider.cs
new file mode 100644
index 000000000..b9785a73b
--- /dev/null
+++ b/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;
+ }
+ }
+}
diff --git a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
index 849b3b709..77d55828d 100644
--- a/Jellyfin.Api/WebSocketListeners/ActivityLogWebSocketListener.cs
+++ b/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.
diff --git a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
index 8a966c137..80314b923 100644
--- a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
+++ b/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.
diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
index 1fb5dc412..1cf43a005 100644
--- a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
+++ b/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.
diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs
index f7ab57a1b..6d4681914 100644
--- a/Jellyfin.Data/Entities/User.cs
+++ b/Jellyfin.Data/Entities/User.cs
@@ -189,6 +189,11 @@ namespace Jellyfin.Data.Entities
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>
/// <remarks>
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedNotifier.cs
index 80ed56cd8..0993c6df7 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedNotifier.cs
+++ b/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);
}
}
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationCancelledNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationCancelledNotifier.cs
index 1c600683a..1d790da6b 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationCancelledNotifier.cs
+++ b/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);
}
}
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedNotifier.cs
index ea0c878d4..a1faf18fc 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallationFailedNotifier.cs
+++ b/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);
}
}
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledNotifier.cs
index 3dda5a04c..bd1a71404 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstalledNotifier.cs
+++ b/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);
}
}
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallingNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallingNotifier.cs
index f691d11a7..b513ac64a 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginInstallingNotifier.cs
+++ b/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);
}
}
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledNotifier.cs
index 709692f6b..1fd7b9adf 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Updates/PluginUninstalledNotifier.cs
+++ b/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);
}
}
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedNotifier.cs
index 10367a939..303e88621 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserDeletedNotifier.cs
+++ b/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);
}
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs
index 6081dd044..a14911b94 100644
--- a/Jellyfin.Server.Implementations/Events/Consumers/Users/UserUpdatedNotifier.cs
+++ b/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);
}
diff --git a/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs b/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs
new file mode 100644
index 000000000..e5c326a32
--- /dev/null
+++ b/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
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs b/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.cs
new file mode 100644
index 000000000..10acb4def
--- /dev/null
+++ b/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");
+ }
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs b/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
index ccfcf96b1..16d62f482 100644
--- a/Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
+++ b/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");
diff --git a/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs b/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs
index e38cd07f0..5f32479e1 100644
--- a/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs
+++ b/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)
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 8f6a0496a..437833aa3 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -380,6 +380,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),
@@ -702,6 +703,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);
@@ -800,7 +802,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))
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 5bcf6d5f0..d7b9da5c2 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/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
@@ -260,6 +263,7 @@ namespace Jellyfin.Server.Extensions
c.AddSwaggerTypeMappings();
c.OperationFilter<FileResponseFilter>();
+ c.DocumentFilter<WebsocketModelFilter>();
});
}
diff --git a/Jellyfin.Server/Filters/WebsocketModelFilter.cs b/Jellyfin.Server/Filters/WebsocketModelFilter.cs
new file mode 100644
index 000000000..248802857
--- /dev/null
+++ b/Jellyfin.Server/Filters/WebsocketModelFilter.cs
@@ -0,0 +1,30 @@
+using MediaBrowser.Common.Plugins;
+using MediaBrowser.Controller.LiveTv;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Session;
+using MediaBrowser.Model.SyncPlay;
+using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.SwaggerGen;
+
+namespace Jellyfin.Server.Filters
+{
+ /// <summary>
+ /// Add models used in websocket messaging.
+ /// </summary>
+ public class WebsocketModelFilter : IDocumentFilter
+ {
+ /// <inheritdoc />
+ public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
+ {
+ context.SchemaGenerator.GenerateSchema(typeof(LibraryUpdateInfo), context.SchemaRepository);
+ context.SchemaGenerator.GenerateSchema(typeof(IPlugin), context.SchemaRepository);
+ context.SchemaGenerator.GenerateSchema(typeof(PlayRequest), context.SchemaRepository);
+ context.SchemaGenerator.GenerateSchema(typeof(PlaystateRequest), context.SchemaRepository);
+ context.SchemaGenerator.GenerateSchema(typeof(TimerEventInfo), context.SchemaRepository);
+ context.SchemaGenerator.GenerateSchema(typeof(SendCommand), context.SchemaRepository);
+ context.SchemaGenerator.GenerateSchema(typeof(GeneralCommandType), context.SchemaRepository);
+
+ context.SchemaGenerator.GenerateSchema(typeof(GroupUpdate<object>), context.SchemaRepository);
+ }
+ }
+}
diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
index fb1ee3b2b..f6c76e4d9 100644
--- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs
+++ b/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;
diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
index 916dea58b..28227603b 100644
--- a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
+++ b/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);
}
}
diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs
index 22d6e2a04..bc4ccd44c 100644
--- a/MediaBrowser.Controller/Session/ISessionController.cs
+++ b/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);
}
}
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index 228b2331d..04c3004ee 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/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.
diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs
index 55e44c19d..ce58a60b9 100644
--- a/MediaBrowser.Controller/Session/SessionInfo.cs
+++ b/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)
{
diff --git a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs
index e742df517..a1cada25c 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs
+++ b/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;
diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs
index 890469d36..54ef49ea6 100644
--- a/MediaBrowser.Model/Configuration/LibraryOptions.cs
+++ b/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; }
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index d9e7e4fbb..fc0aad072 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/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));
}
diff --git a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs
new file mode 100644
index 000000000..712fa381e
--- /dev/null
+++ b/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);
+ }
+ }
+}
diff --git a/MediaBrowser.Model/Net/WebSocketMessage.cs b/MediaBrowser.Model/Net/WebSocketMessage.cs
index 660eebeda..bffbbe612 100644
--- a/MediaBrowser.Model/Net/WebSocketMessage.cs
+++ b/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; }
diff --git a/MediaBrowser.Model/Session/ClientCapabilities.cs b/MediaBrowser.Model/Session/ClientCapabilities.cs
index d3878ca30..a85e6ff2a 100644
--- a/MediaBrowser.Model/Session/ClientCapabilities.cs
+++ b/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;
}
}
diff --git a/MediaBrowser.Model/Session/SessionMessageType.cs b/MediaBrowser.Model/Session/SessionMessageType.cs
new file mode 100644
index 000000000..23c41026d
--- /dev/null
+++ b/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,
+ }
+}
diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs
index a1f01f7e8..363b2633f 100644
--- a/MediaBrowser.Model/Users/UserPolicy.cs
+++ b/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>();
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index b6fb4267f..a0c7d4ad0 100644
--- a/MediaBrowser.Providers/Manager/ProviderManager.cs
+++ b/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
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 813dd441f..794490cc5 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/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>
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs
index f22d484ab..5e9a4a225 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs
+++ b/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,
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
index f6592afe4..df1e12240 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
+++ b/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);
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
index e7328b553..fcd8e614c 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
+++ b/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)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionImages.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionImages.cs
deleted file mode 100644
index 0a8994d54..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionImages.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionResult.cs
deleted file mode 100644
index c6b851c23..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/CollectionResult.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/Part.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/Part.cs
deleted file mode 100644
index a48124b3e..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Collections/Part.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Backdrop.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Backdrop.cs
deleted file mode 100644
index 5b7627f6e..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Backdrop.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Crew.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Crew.cs
deleted file mode 100644
index 339ecb628..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Crew.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs
deleted file mode 100644
index aac4420e8..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/ExternalIds.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Genre.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Genre.cs
deleted file mode 100644
index 9ba1c15c6..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Genre.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Images.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Images.cs
deleted file mode 100644
index 0538cf174..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Images.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keyword.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keyword.cs
deleted file mode 100644
index fff86931b..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keyword.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keywords.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keywords.cs
deleted file mode 100644
index 235ecb568..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Keywords.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Poster.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Poster.cs
deleted file mode 100644
index 4f61e978b..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Poster.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Profile.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Profile.cs
deleted file mode 100644
index 0a1f8843e..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Profile.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Still.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Still.cs
deleted file mode 100644
index 61de819b9..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Still.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/StillImages.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/StillImages.cs
deleted file mode 100644
index 59ab18b7b..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/StillImages.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Video.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Video.cs
deleted file mode 100644
index ebd5c7ace..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Video.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs
deleted file mode 100644
index 1c673fdbd..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/General/Videos.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/BelongsToCollection.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/BelongsToCollection.cs
deleted file mode 100644
index e8745be14..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/BelongsToCollection.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Cast.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Cast.cs
deleted file mode 100644
index 937cfb8f6..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Cast.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Casts.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Casts.cs
deleted file mode 100644
index 37547640f..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Casts.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Country.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Country.cs
deleted file mode 100644
index edd656a46..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Country.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs
deleted file mode 100644
index 704ebcd5a..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/MovieResult.cs
+++ /dev/null
@@ -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();
- }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCompany.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCompany.cs
deleted file mode 100644
index 2788731b2..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCompany.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCountry.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCountry.cs
deleted file mode 100644
index 1b6f2cc67..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/ProductionCountry.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Releases.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Releases.cs
deleted file mode 100644
index 276fbaaf5..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Releases.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/SpokenLanguage.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/SpokenLanguage.cs
deleted file mode 100644
index 67231d219..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/SpokenLanguage.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs
deleted file mode 100644
index 057177294..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Trailers.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Youtube.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Youtube.cs
deleted file mode 100644
index 6885b7dab..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Movies/Youtube.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs
deleted file mode 100644
index d82e0fc6d..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonImages.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonResult.cs
deleted file mode 100644
index 460ced49a..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/People/PersonResult.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/ExternalIdLookupResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/ExternalIdLookupResult.cs
deleted file mode 100644
index 87c2a723d..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/ExternalIdLookupResult.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/MovieResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/MovieResult.cs
deleted file mode 100644
index 401c75c31..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/MovieResult.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/PersonSearchResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/PersonSearchResult.cs
deleted file mode 100644
index 4cff45ca6..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/PersonSearchResult.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TmdbSearchResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TmdbSearchResult.cs
deleted file mode 100644
index 3b9257b62..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TmdbSearchResult.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TvResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TvResult.cs
deleted file mode 100644
index b2bb068b5..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/Search/TvResult.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Cast.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Cast.cs
deleted file mode 100644
index 4ce26c65e..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Cast.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRating.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRating.cs
deleted file mode 100644
index aef4e2863..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRating.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRatings.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRatings.cs
deleted file mode 100644
index ae1b5668d..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/ContentRatings.cs
+++ /dev/null
@@ -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; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/CreatedBy.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/CreatedBy.cs
deleted file mode 100644
index ba36632e0..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/CreatedBy.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
- public class CreatedBy
- {
- public int Id { get; set; }
-
- public string Name { get; set; }
-
- public string Profile_Path { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Credits.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Credits.cs
deleted file mode 100644
index 47205d875..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Credits.cs
+++ /dev/null
@@ -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.TV
-{
- public class Credits
- {
- public List<Cast> Cast { get; set; }
-
- public List<Crew> Crew { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Episode.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Episode.cs
deleted file mode 100644
index 53e3c2695..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Episode.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
- public class Episode
- {
- public string Air_Date { get; set; }
-
- public int Episode_Number { get; set; }
-
- public int Id { get; set; }
-
- public string Name { get; set; }
-
- public string Overview { get; set; }
-
- public string Still_Path { get; set; }
-
- public double Vote_Average { get; set; }
-
- public int Vote_Count { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeCredits.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeCredits.cs
deleted file mode 100644
index 9707e4bf4..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeCredits.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
- public class EpisodeCredits
- {
- public List<Cast> Cast { get; set; }
-
- public List<Crew> Crew { get; set; }
-
- public List<GuestStar> Guest_Stars { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeResult.cs
deleted file mode 100644
index 4458bad36..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/EpisodeResult.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
- public class EpisodeResult
- {
- public DateTime Air_Date { get; set; }
-
- public int Episode_Number { get; set; }
-
- public string Name { get; set; }
-
- public string Overview { get; set; }
-
- public int Id { get; set; }
-
- public object Production_Code { get; set; }
-
- public int Season_Number { get; set; }
-
- public string Still_Path { get; set; }
-
- public double Vote_Average { get; set; }
-
- public int Vote_Count { get; set; }
-
- public StillImages Images { get; set; }
-
- public ExternalIds External_Ids { get; set; }
-
- public EpisodeCredits Credits { get; set; }
-
- public Tmdb.Models.General.Videos Videos { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/GuestStar.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/GuestStar.cs
deleted file mode 100644
index 8f3988641..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/GuestStar.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
- public class GuestStar
- {
- public int Id { get; set; }
-
- public string Name { get; set; }
-
- public string Credit_Id { get; set; }
-
- public string Character { get; set; }
-
- public int Order { get; set; }
-
- public string Profile_Path { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Network.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Network.cs
deleted file mode 100644
index 3dc310d33..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Network.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
- public class Network
- {
- public int Id { get; set; }
-
- public string Name { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Season.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Season.cs
deleted file mode 100644
index 9cbd283a9..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/Season.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
- public class Season
- {
- public string Air_Date { get; set; }
-
- public int Episode_Count { get; set; }
-
- public int Id { get; set; }
-
- public string Poster_Path { get; set; }
-
- public int Season_Number { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonImages.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonImages.cs
deleted file mode 100644
index f364d4921..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonImages.cs
+++ /dev/null
@@ -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.TV
-{
- public class SeasonImages
- {
- public List<Poster> Posters { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonResult.cs
deleted file mode 100644
index e98048eac..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeasonResult.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
- public class SeasonResult
- {
- public DateTime Air_Date { get; set; }
-
- public List<Episode> Episodes { get; set; }
-
- public string Name { get; set; }
-
- public string Overview { get; set; }
-
- public int Id { get; set; }
-
- public string Poster_Path { get; set; }
-
- public int Season_Number { get; set; }
-
- public Credits Credits { get; set; }
-
- public SeasonImages Images { get; set; }
-
- public ExternalIds External_Ids { get; set; }
-
- public General.Videos Videos { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeriesResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeriesResult.cs
deleted file mode 100644
index 331cd59fa..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Models/TV/SeriesResult.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Models.TV
-{
- public class SeriesResult
- {
- public string Backdrop_Path { get; set; }
-
- public List<CreatedBy> Created_By { get; set; }
-
- public List<int> Episode_Run_Time { get; set; }
-
- public DateTime First_Air_Date { get; set; }
-
- public List<Genre> Genres { get; set; }
-
- public string Homepage { get; set; }
-
- public int Id { get; set; }
-
- public bool In_Production { get; set; }
-
- public List<string> Languages { get; set; }
-
- public DateTime Last_Air_Date { get; set; }
-
- public string Name { get; set; }
-
- public List<Network> Networks { get; set; }
-
- public int Number_Of_Episodes { get; set; }
-
- public int Number_Of_Seasons { get; set; }
-
- public string Original_Name { get; set; }
-
- public List<string> Origin_Country { get; set; }
-
- public string Overview { get; set; }
-
- public string Popularity { get; set; }
-
- public string Poster_Path { get; set; }
-
- public List<Season> Seasons { get; set; }
-
- public string Status { get; set; }
-
- public double Vote_Average { get; set; }
-
- public int Vote_Count { get; set; }
-
- public Credits Credits { get; set; }
-
- public Images Images { get; set; }
-
- public Keywords Keywords { get; set; }
-
- public ExternalIds External_Ids { get; set; }
-
- public General.Videos Videos { get; set; }
-
- public ContentRatings Content_Ratings { get; set; }
-
- public string ResultLanguage { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs
deleted file mode 100644
index 3c626f9eb..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/GenericTmdbMovieInfo.cs
+++ /dev/null
@@ -1,309 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Net;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.Movies;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
-{
- public class GenericTmdbMovieInfo<T>
- where T : BaseItem, new()
- {
- private readonly ILogger _logger;
- private readonly IJsonSerializer _jsonSerializer;
- private readonly ILibraryManager _libraryManager;
- private readonly IFileSystem _fileSystem;
-
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
- public GenericTmdbMovieInfo(ILogger logger, IJsonSerializer jsonSerializer, ILibraryManager libraryManager, IFileSystem fileSystem)
- {
- _logger = logger;
- _jsonSerializer = jsonSerializer;
- _libraryManager = libraryManager;
- _fileSystem = fileSystem;
- }
-
- public async Task<MetadataResult<T>> GetMetadata(ItemLookupInfo itemId, CancellationToken cancellationToken)
- {
- var tmdbId = itemId.GetProviderId(MetadataProvider.Tmdb);
- var imdbId = itemId.GetProviderId(MetadataProvider.Imdb);
-
- // Don't search for music video id's because it is very easy to misidentify.
- if (string.IsNullOrEmpty(tmdbId) && string.IsNullOrEmpty(imdbId) && typeof(T) != typeof(MusicVideo))
- {
- var searchResults = await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetMovieSearchResults(itemId, cancellationToken).ConfigureAwait(false);
-
- var searchResult = searchResults.FirstOrDefault();
-
- if (searchResult != null)
- {
- tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb);
- }
- }
-
- if (!string.IsNullOrEmpty(tmdbId) || !string.IsNullOrEmpty(imdbId))
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- return await FetchMovieData(tmdbId, imdbId, itemId.MetadataLanguage, itemId.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
- }
-
- return new MetadataResult<T>();
- }
-
- /// <summary>
- /// Fetches the movie data.
- /// </summary>
- /// <param name="tmdbId">The TMDB identifier.</param>
- /// <param name="imdbId">The imdb identifier.</param>
- /// <param name="language">The language.</param>
- /// <param name="preferredCountryCode">The preferred country code.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task{`0}.</returns>
- private async Task<MetadataResult<T>> FetchMovieData(string tmdbId, string imdbId, string language, string preferredCountryCode, CancellationToken cancellationToken)
- {
- var item = new MetadataResult<T>
- {
- Item = new T()
- };
-
- string dataFilePath = null;
- MovieResult movieInfo = null;
-
- // Id could be ImdbId or TmdbId
- if (string.IsNullOrEmpty(tmdbId))
- {
- movieInfo = await TmdbMovieProvider.Current.FetchMainResult(imdbId, false, language, cancellationToken).ConfigureAwait(false);
- if (movieInfo != null)
- {
- tmdbId = movieInfo.Id.ToString(_usCulture);
-
- dataFilePath = TmdbMovieProvider.Current.GetDataFilePath(tmdbId, language);
- Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
- _jsonSerializer.SerializeToFile(movieInfo, dataFilePath);
- }
- }
-
- if (!string.IsNullOrWhiteSpace(tmdbId))
- {
- await TmdbMovieProvider.Current.EnsureMovieInfo(tmdbId, language, cancellationToken).ConfigureAwait(false);
-
- dataFilePath = dataFilePath ?? TmdbMovieProvider.Current.GetDataFilePath(tmdbId, language);
- movieInfo = movieInfo ?? _jsonSerializer.DeserializeFromFile<MovieResult>(dataFilePath);
-
- var settings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
-
- ProcessMainInfo(item, settings, preferredCountryCode, movieInfo);
- item.HasMetadata = true;
- }
-
- return item;
- }
-
- /// <summary>
- /// Processes the main info.
- /// </summary>
- /// <param name="resultItem">The result item.</param>
- /// <param name="settings">The settings.</param>
- /// <param name="preferredCountryCode">The preferred country code.</param>
- /// <param name="movieData">The movie data.</param>
- private void ProcessMainInfo(MetadataResult<T> resultItem, TmdbSettingsResult settings, string preferredCountryCode, MovieResult movieData)
- {
- var movie = resultItem.Item;
-
- movie.Name = movieData.GetTitle() ?? movie.Name;
-
- movie.OriginalTitle = movieData.GetOriginalTitle();
-
- movie.Overview = string.IsNullOrWhiteSpace(movieData.Overview) ? null : WebUtility.HtmlDecode(movieData.Overview);
- movie.Overview = movie.Overview != null ? movie.Overview.Replace("\n\n", "\n") : null;
-
- // movie.HomePageUrl = movieData.homepage;
-
- if (!string.IsNullOrEmpty(movieData.Tagline))
- {
- movie.Tagline = movieData.Tagline;
- }
-
- if (movieData.Production_Countries != null)
- {
- movie.ProductionLocations = movieData
- .Production_Countries
- .Select(i => i.Name)
- .ToArray();
- }
-
- movie.SetProviderId(MetadataProvider.Tmdb, movieData.Id.ToString(_usCulture));
- movie.SetProviderId(MetadataProvider.Imdb, movieData.Imdb_Id);
-
- if (movieData.Belongs_To_Collection != null)
- {
- movie.SetProviderId(MetadataProvider.TmdbCollection,
- movieData.Belongs_To_Collection.Id.ToString(CultureInfo.InvariantCulture));
-
- if (movie is Movie movieItem)
- {
- movieItem.CollectionName = movieData.Belongs_To_Collection.Name;
- }
- }
-
- string voteAvg = movieData.Vote_Average.ToString(CultureInfo.InvariantCulture);
-
- if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var rating))
- {
- movie.CommunityRating = rating;
- }
-
- // movie.VoteCount = movieData.vote_count;
-
- if (movieData.Releases != null && movieData.Releases.Countries != null)
- {
- var releases = movieData.Releases.Countries.Where(i => !string.IsNullOrWhiteSpace(i.Certification)).ToList();
-
- var ourRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, preferredCountryCode, StringComparison.OrdinalIgnoreCase));
- var usRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase));
-
- if (ourRelease != null)
- {
- var ratingPrefix = string.Equals(preferredCountryCode, "us", StringComparison.OrdinalIgnoreCase) ? "" : preferredCountryCode + "-";
- var newRating = ratingPrefix + ourRelease.Certification;
-
- newRating = newRating.Replace("de-", "FSK-", StringComparison.OrdinalIgnoreCase);
-
- movie.OfficialRating = newRating;
- }
- else if (usRelease != null)
- {
- movie.OfficialRating = usRelease.Certification;
- }
- }
-
- if (!string.IsNullOrWhiteSpace(movieData.Release_Date))
- {
- // These dates are always in this exact format
- if (DateTime.TryParse(movieData.Release_Date, _usCulture, DateTimeStyles.None, out var r))
- {
- movie.PremiereDate = r.ToUniversalTime();
- movie.ProductionYear = movie.PremiereDate.Value.Year;
- }
- }
-
- // studios
- if (movieData.Production_Companies != null)
- {
- movie.SetStudios(movieData.Production_Companies.Select(c => c.Name));
- }
-
- // genres
- // Movies get this from imdb
- var genres = movieData.Genres ?? new List<Tmdb.Models.General.Genre>();
-
- foreach (var genre in genres.Select(g => g.Name))
- {
- movie.AddGenre(genre);
- }
-
- resultItem.ResetPeople();
- var tmdbImageUrl = settings.images.GetImageUrl("original");
-
- // Actors, Directors, Writers - all in People
- // actors come from cast
- if (movieData.Casts != null && movieData.Casts.Cast != null)
- {
- foreach (var actor in movieData.Casts.Cast.OrderBy(a => a.Order))
- {
- var personInfo = new PersonInfo
- {
- Name = actor.Name.Trim(),
- Role = actor.Character,
- Type = PersonType.Actor,
- SortOrder = actor.Order
- };
-
- if (!string.IsNullOrWhiteSpace(actor.Profile_Path))
- {
- personInfo.ImageUrl = tmdbImageUrl + actor.Profile_Path;
- }
-
- if (actor.Id > 0)
- {
- personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
- }
-
- resultItem.AddPerson(personInfo);
- }
- }
-
- // and the rest from crew
- if (movieData.Casts?.Crew != null)
- {
- var keepTypes = new[]
- {
- PersonType.Director,
- PersonType.Writer,
- PersonType.Producer
- };
-
- foreach (var person in movieData.Casts.Crew)
- {
- // Normalize this
- var type = TmdbUtils.MapCrewToPersonType(person);
-
- if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) &&
- !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
- {
- continue;
- }
-
- var personInfo = new PersonInfo
- {
- Name = person.Name.Trim(),
- Role = person.Job,
- Type = type
- };
-
- if (!string.IsNullOrWhiteSpace(person.Profile_Path))
- {
- personInfo.ImageUrl = tmdbImageUrl + person.Profile_Path;
- }
-
- if (person.Id > 0)
- {
- personInfo.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
- }
-
- resultItem.AddPerson(personInfo);
- }
- }
-
- // if (movieData.keywords != null && movieData.keywords.keywords != null)
- //{
- // movie.Keywords = movieData.keywords.keywords.Select(i => i.name).ToList();
- //}
-
- if (movieData.Trailers != null && movieData.Trailers.Youtube != null)
- {
- movie.RemoteTrailers = movieData.Trailers.Youtube.Select(i => new MediaUrl
- {
- Url = string.Format(CultureInfo.InvariantCulture, "https://www.youtube.com/watch?v={0}", i.Source),
- Name = i.Name
- }).ToArray();
- }
- }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs
deleted file mode 100644
index 9db7e0997..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageProvider.cs
+++ /dev/null
@@ -1,212 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Dto;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.Movies;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
-{
- public class TmdbImageProvider : IRemoteImageProvider, IHasOrder
- {
- private readonly IJsonSerializer _jsonSerializer;
- private readonly IHttpClientFactory _httpClientFactory;
- private readonly IFileSystem _fileSystem;
-
- public TmdbImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory, IFileSystem fileSystem)
- {
- _jsonSerializer = jsonSerializer;
- _httpClientFactory = httpClientFactory;
- _fileSystem = fileSystem;
- }
-
- public string Name => ProviderName;
-
- public static string ProviderName => TmdbUtils.ProviderName;
-
- /// <inheritdoc />
- public int Order => 0;
-
- public bool Supports(BaseItem item)
- {
- return item is Movie || item is MusicVideo || item is Trailer;
- }
-
- public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
- {
- return new List<ImageType>
- {
- ImageType.Primary,
- ImageType.Backdrop
- };
- }
-
- public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
- {
- var list = new List<RemoteImageInfo>();
-
- var language = item.GetPreferredMetadataLanguage();
-
- var results = await FetchImages(item, null, _jsonSerializer, cancellationToken).ConfigureAwait(false);
-
- if (results == null)
- {
- return list;
- }
-
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
-
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
-
- var supportedImages = GetSupportedImages(item).ToList();
-
- if (supportedImages.Contains(ImageType.Primary))
- {
- list.AddRange(GetPosters(results).Select(i => new RemoteImageInfo
- {
- Url = tmdbImageUrl + 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
- }));
- }
-
- if (supportedImages.Contains(ImageType.Backdrop))
- {
- list.AddRange(GetBackdrops(results).Select(i => new RemoteImageInfo
- {
- Url = tmdbImageUrl + 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)
- {
- if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
- {
- return 2;
- }
- }
-
- if (string.IsNullOrEmpty(i.Language))
- {
- 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(Images 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(Images images)
- {
- var eligibleBackdrops = images.Backdrops == null ? new List<Backdrop>() :
- images.Backdrops;
-
- return eligibleBackdrops.OrderByDescending(i => i.Vote_Average)
- .ThenByDescending(i => i.Vote_Count);
- }
-
- /// <summary>
- /// Fetches the images.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <param name="language">The language.</param>
- /// <param name="jsonSerializer">The json serializer.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task{MovieImages}.</returns>
- private async Task<Images> FetchImages(BaseItem item, string language, IJsonSerializer jsonSerializer, CancellationToken cancellationToken)
- {
- var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
-
- if (string.IsNullOrWhiteSpace(tmdbId))
- {
- var imdbId = item.GetProviderId(MetadataProvider.Imdb);
- if (!string.IsNullOrWhiteSpace(imdbId))
- {
- var movieInfo = await TmdbMovieProvider.Current.FetchMainResult(imdbId, false, language, cancellationToken).ConfigureAwait(false);
- if (movieInfo != null)
- {
- tmdbId = movieInfo.Id.ToString(CultureInfo.InvariantCulture);
- }
- }
- }
-
- if (string.IsNullOrWhiteSpace(tmdbId))
- {
- return null;
- }
-
- await TmdbMovieProvider.Current.EnsureMovieInfo(tmdbId, language, cancellationToken).ConfigureAwait(false);
-
- var path = TmdbMovieProvider.Current.GetDataFilePath(tmdbId, language);
-
- if (!string.IsNullOrEmpty(path))
- {
- var fileInfo = _fileSystem.GetFileInfo(path);
-
- if (fileInfo.Exists)
- {
- return jsonSerializer.DeserializeFromFile<MovieResult>(path).Images;
- }
- }
-
- return null;
- }
-
- public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
- {
- return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
- }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageSettings.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageSettings.cs
deleted file mode 100644
index 1ba8f9072..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbImageSettings.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
-{
- internal class TmdbImageSettings
- {
- public IReadOnlyList<string> backdrop_sizes { get; set; }
-
- public string secure_base_url { get; set; }
-
- public IReadOnlyList<string> poster_sizes { get; set; }
-
- public IReadOnlyList<string> profile_sizes { get; set; }
-
- public string GetImageUrl(string image)
- {
- return secure_base_url + image;
- }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
index 9610e4058..f1a1b65d8 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs
@@ -1,4 +1,3 @@
-using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Providers;
@@ -33,7 +32,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
return true;
}
- return item is Movie || item is MusicVideo || item is Trailer;
+ return item is Movie;
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
new file mode 100644
index 000000000..dac9e961c
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
@@ -0,0 +1,128 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller.Entities;
+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 TMDbLib.Objects.Find;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
+{
+ public class TmdbMovieImageProvider : IRemoteImageProvider, IHasOrder
+ {
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly TmdbClientManager _tmdbClientManager;
+
+ public TmdbMovieImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
+ {
+ _httpClientFactory = httpClientFactory;
+ _tmdbClientManager = tmdbClientManager;
+ }
+
+ public int Order => 0;
+
+ public string Name => TmdbUtils.ProviderName;
+
+ public bool Supports(BaseItem item)
+ {
+ return item is Movie || item is Trailer;
+ }
+
+ public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
+ {
+ return new List<ImageType>
+ {
+ ImageType.Primary,
+ ImageType.Backdrop
+ };
+ }
+
+ public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
+ {
+ var language = item.GetPreferredMetadataLanguage();
+
+ var movieTmdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
+ if (movieTmdbId <= 0)
+ {
+ var movieImdbId = item.GetProviderId(MetadataProvider.Imdb);
+ if (string.IsNullOrEmpty(movieImdbId))
+ {
+ return Enumerable.Empty<RemoteImageInfo>();
+ }
+
+ var movieResult = await _tmdbClientManager.FindByExternalIdAsync(movieImdbId, FindExternalSource.Imdb, language, cancellationToken).ConfigureAwait(false);
+ if (movieResult?.MovieResults != null && movieResult.MovieResults.Count > 0)
+ {
+ movieTmdbId = movieResult.MovieResults[0].Id;
+ }
+ }
+
+ if (movieTmdbId <= 0)
+ {
+ return Enumerable.Empty<RemoteImageInfo>();
+ }
+
+ var movie = await _tmdbClientManager
+ .GetMovieAsync(movieTmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken)
+ .ConfigureAwait(false);
+
+ if (movie?.Images == null)
+ {
+ return Enumerable.Empty<RemoteImageInfo>();
+ }
+
+ var remoteImages = new List<RemoteImageInfo>();
+
+ for (var i = 0; i < movie.Images.Posters.Count; i++)
+ {
+ var poster = movie.Images.Posters[i];
+ remoteImages.Add(new RemoteImageInfo
+ {
+ 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
+ });
+ }
+
+ for (var i = 0; i < movie.Images.Backdrops.Count; i++)
+ {
+ var backdrop = movie.Images.Backdrops[i];
+ remoteImages.Add(new RemoteImageInfo
+ {
+ Url = _tmdbClientManager.GetPosterUrl(backdrop.FilePath),
+ CommunityRating = backdrop.VoteAverage,
+ VoteCount = backdrop.VoteCount,
+ Width = backdrop.Width,
+ Height = backdrop.Height,
+ ProviderName = Name,
+ Type = ImageType.Backdrop,
+ RatingType = RatingType.Score
+ });
+ }
+
+ return remoteImages.OrderByLanguageDescending(language);
+ }
+
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
+ {
+ return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
index 31cfd8649..3984e4953 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
@@ -3,26 +3,17 @@
using System;
using System.Collections.Generic;
using System.Globalization;
-using System.IO;
-using System.Net;
+using System.Linq;
using System.Net.Http;
-using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common;
-using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.Movies;
-using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
{
@@ -31,365 +22,273 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
/// </summary>
public class TmdbMovieProvider : IRemoteMetadataProvider<Movie, MovieInfo>, IHasOrder
{
- private const string TmdbConfigUrl = TmdbUtils.BaseTmdbApiUrl + "3/configuration?api_key={0}";
- private const string GetMovieInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/movie/{0}?api_key={1}&append_to_response=casts,releases,images,keywords,trailers";
-
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
- private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IFileSystem _fileSystem;
- private readonly IServerConfigurationManager _configurationManager;
- private readonly ILogger<TmdbMovieProvider> _logger;
private readonly ILibraryManager _libraryManager;
- private readonly IApplicationHost _appHost;
-
- /// <summary>
- /// The _TMDB settings task.
- /// </summary>
- private TmdbSettingsResult _tmdbSettings;
+ private readonly TmdbClientManager _tmdbClientManager;
public TmdbMovieProvider(
- IJsonSerializer jsonSerializer,
- IHttpClientFactory httpClientFactory,
- IFileSystem fileSystem,
- IServerConfigurationManager configurationManager,
- ILogger<TmdbMovieProvider> logger,
ILibraryManager libraryManager,
- IApplicationHost appHost)
+ TmdbClientManager tmdbClientManager,
+ IHttpClientFactory httpClientFactory)
{
- _jsonSerializer = jsonSerializer;
- _httpClientFactory = httpClientFactory;
- _fileSystem = fileSystem;
- _configurationManager = configurationManager;
- _logger = logger;
_libraryManager = libraryManager;
- _appHost = appHost;
- Current = this;
+ _tmdbClientManager = tmdbClientManager;
+ _httpClientFactory = httpClientFactory;
}
- internal static TmdbMovieProvider Current { get; private set; }
-
- /// <inheritdoc />
public string Name => TmdbUtils.ProviderName;
/// <inheritdoc />
public int Order => 1;
- public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MovieInfo searchInfo, CancellationToken cancellationToken)
+ public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MovieInfo searchInfo, CancellationToken cancellationToken)
{
- return GetMovieSearchResults(searchInfo, cancellationToken);
- }
-
- public async Task<IEnumerable<RemoteSearchResult>> GetMovieSearchResults(ItemLookupInfo searchInfo, CancellationToken cancellationToken)
- {
- var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb);
+ var tmdbId = Convert.ToInt32(searchInfo.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
- if (!string.IsNullOrEmpty(tmdbId))
+ if (tmdbId == 0)
{
- cancellationToken.ThrowIfCancellationRequested();
-
- await EnsureMovieInfo(tmdbId, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
-
- var dataFilePath = GetDataFilePath(tmdbId, searchInfo.MetadataLanguage);
-
- var obj = _jsonSerializer.DeserializeFromFile<MovieResult>(dataFilePath);
-
- var tmdbSettings = await GetTmdbSettings(cancellationToken).ConfigureAwait(false);
-
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
-
- var remoteResult = new RemoteSearchResult
+ var movieResults = await _tmdbClientManager
+ .SearchMovieAsync(searchInfo.Name, searchInfo.MetadataLanguage, cancellationToken)
+ .ConfigureAwait(false);
+ var remoteSearchResults = new List<RemoteSearchResult>();
+ for (var i = 0; i < movieResults.Count; i++)
{
- Name = obj.GetTitle(),
- SearchProviderName = Name,
- ImageUrl = string.IsNullOrWhiteSpace(obj.Poster_Path) ? null : tmdbImageUrl + obj.Poster_Path
- };
-
- if (!string.IsNullOrWhiteSpace(obj.Release_Date))
- {
- // These dates are always in this exact format
- if (DateTime.TryParse(obj.Release_Date, _usCulture, DateTimeStyles.None, out var r))
+ var movieResult = movieResults[i];
+ var remoteSearchResult = new RemoteSearchResult
{
- remoteResult.PremiereDate = r.ToUniversalTime();
- remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
- }
- }
-
- remoteResult.SetProviderId(MetadataProvider.Tmdb, obj.Id.ToString(_usCulture));
-
- if (!string.IsNullOrWhiteSpace(obj.Imdb_Id))
- {
- remoteResult.SetProviderId(MetadataProvider.Imdb, obj.Imdb_Id);
+ Name = movieResult.Title ?? movieResult.OriginalTitle,
+ ImageUrl = _tmdbClientManager.GetPosterUrl(movieResult.PosterPath),
+ Overview = movieResult.Overview,
+ SearchProviderName = Name
+ };
+
+ var releaseDate = movieResult.ReleaseDate?.ToUniversalTime();
+ remoteSearchResult.PremiereDate = releaseDate;
+ remoteSearchResult.ProductionYear = releaseDate?.Year;
+
+ remoteSearchResult.SetProviderId(MetadataProvider.Tmdb, movieResult.Id.ToString(CultureInfo.InvariantCulture));
+ remoteSearchResults.Add(remoteSearchResult);
}
- return new[] { remoteResult };
+ return remoteSearchResults;
}
- return await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetMovieSearchResults(searchInfo, cancellationToken).ConfigureAwait(false);
- }
-
- public Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken)
- {
- return GetItemMetadata<Movie>(info, cancellationToken);
- }
+ var movie = await _tmdbClientManager
+ .GetMovieAsync(tmdbId, searchInfo.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage), cancellationToken)
+ .ConfigureAwait(false);
- public Task<MetadataResult<T>> GetItemMetadata<T>(ItemLookupInfo id, CancellationToken cancellationToken)
- where T : BaseItem, new()
- {
- var movieDb = new GenericTmdbMovieInfo<T>(_logger, _jsonSerializer, _libraryManager, _fileSystem);
-
- return movieDb.GetMetadata(id, cancellationToken);
- }
-
- /// <summary>
- /// Gets the TMDB settings.
- /// </summary>
- /// <returns>Task{TmdbSettingsResult}.</returns>
- internal async Task<TmdbSettingsResult> GetTmdbSettings(CancellationToken cancellationToken)
- {
- if (_tmdbSettings != null)
+ var remoteResult = new RemoteSearchResult
{
- return _tmdbSettings;
- }
+ Name = movie.Title ?? movie.OriginalTitle,
+ SearchProviderName = Name,
+ ImageUrl = _tmdbClientManager.GetPosterUrl(movie.PosterPath),
+ Overview = movie.Overview
+ };
- using var requestMessage = new HttpRequestMessage(HttpMethod.Get, string.Format(CultureInfo.InvariantCulture, TmdbConfigUrl, TmdbUtils.ApiKey));
- foreach (var header in TmdbUtils.AcceptHeaders)
+ if (movie.ReleaseDate != null)
{
- requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
+ var releaseDate = movie.ReleaseDate.Value.ToUniversalTime();
+ remoteResult.PremiereDate = releaseDate;
+ remoteResult.ProductionYear = releaseDate.Year;
}
- using var response = await GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
- _tmdbSettings = await _jsonSerializer.DeserializeFromStreamAsync<TmdbSettingsResult>(stream).ConfigureAwait(false);
- return _tmdbSettings;
- }
-
- /// <summary>
- /// Gets the movie data path.
- /// </summary>
- /// <param name="appPaths">The app paths.</param>
- /// <param name="tmdbId">The TMDB id.</param>
- /// <returns>System.String.</returns>
- internal static string GetMovieDataPath(IApplicationPaths appPaths, string tmdbId)
- {
- var dataPath = GetMoviesDataPath(appPaths);
-
- return Path.Combine(dataPath, tmdbId);
- }
+ remoteResult.SetProviderId(MetadataProvider.Tmdb, movie.Id.ToString(CultureInfo.InvariantCulture));
- internal static string GetMoviesDataPath(IApplicationPaths appPaths)
- {
- var dataPath = Path.Combine(appPaths.CachePath, "tmdb-movies2");
-
- return dataPath;
- }
-
- /// <summary>
- /// Downloads the movie info.
- /// </summary>
- /// <param name="id">The id.</param>
- /// <param name="preferredMetadataLanguage">The preferred metadata language.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- internal async Task DownloadMovieInfo(string id, string preferredMetadataLanguage, CancellationToken cancellationToken)
- {
- var mainResult = await FetchMainResult(id, true, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
-
- if (mainResult == null)
+ if (!string.IsNullOrWhiteSpace(movie.ImdbId))
{
- return;
+ remoteResult.SetProviderId(MetadataProvider.Imdb, movie.ImdbId);
}
- var dataFilePath = GetDataFilePath(id, preferredMetadataLanguage);
-
- Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
-
- _jsonSerializer.SerializeToFile(mainResult, dataFilePath);
+ return new[] { remoteResult };
}
- internal Task EnsureMovieInfo(string tmdbId, string language, CancellationToken cancellationToken)
+ public async Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken)
{
- if (string.IsNullOrEmpty(tmdbId))
- {
- throw new ArgumentNullException(nameof(tmdbId));
- }
-
- var path = GetDataFilePath(tmdbId, language);
-
- var fileInfo = _fileSystem.GetFileSystemInfo(path);
+ var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
+ var imdbId = info.GetProviderId(MetadataProvider.Imdb);
- if (fileInfo.Exists)
+ if (string.IsNullOrEmpty(tmdbId) && string.IsNullOrEmpty(imdbId))
{
- // If it's recent or automatic updates are enabled, don't re-download
- if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
+ // ParseName is required here.
+ // Caller provides the filename with extension stripped and NOT the parsed filename
+ var parsedName = _libraryManager.ParseName(info.Name);
+ var searchResults = await _tmdbClientManager.SearchMovieAsync(parsedName.Name, parsedName.Year ?? 0, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
+
+ if (searchResults.Count > 0)
{
- return Task.CompletedTask;
+ tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture);
}
}
- return DownloadMovieInfo(tmdbId, language, cancellationToken);
- }
-
- internal string GetDataFilePath(string tmdbId, string preferredLanguage)
- {
if (string.IsNullOrEmpty(tmdbId))
{
- throw new ArgumentNullException(nameof(tmdbId));
+ return new MetadataResult<Movie>();
}
- var path = GetMovieDataPath(_configurationManager.ApplicationPaths, tmdbId);
+ var movieResult = await _tmdbClientManager
+ .GetMovieAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
+ .ConfigureAwait(false);
- if (string.IsNullOrWhiteSpace(preferredLanguage))
+ var movie = new Movie
+ {
+ Name = movieResult.Title ?? movieResult.OriginalTitle,
+ Overview = movieResult.Overview?.Replace("\n\n", "\n", StringComparison.InvariantCulture),
+ Tagline = movieResult.Tagline,
+ ProductionLocations = movieResult.ProductionCountries.Select(pc => pc.Name).ToArray()
+ };
+ var metadataResult = new MetadataResult<Movie>
{
- preferredLanguage = "alllang";
+ HasMetadata = true,
+ ResultLanguage = info.MetadataLanguage,
+ Item = movie
+ };
+
+ movie.SetProviderId(MetadataProvider.Tmdb, tmdbId);
+ movie.SetProviderId(MetadataProvider.Imdb, movieResult.ImdbId);
+ if (movieResult.BelongsToCollection != null)
+ {
+ movie.SetProviderId(MetadataProvider.TmdbCollection, movieResult.BelongsToCollection.Id.ToString(CultureInfo.InvariantCulture));
+ movie.CollectionName = movieResult.BelongsToCollection.Name;
}
- var filename = string.Format(CultureInfo.InvariantCulture, "all-{0}.json", preferredLanguage);
+ movie.CommunityRating = Convert.ToSingle(movieResult.VoteAverage);
- return Path.Combine(path, filename);
- }
+ if (movieResult.Releases?.Countries != null)
+ {
+ var releases = movieResult.Releases.Countries.Where(i => !string.IsNullOrWhiteSpace(i.Certification)).ToList();
- public static string GetImageLanguagesParam(string preferredLanguage)
- {
- var languages = new List<string>();
+ var ourRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, info.MetadataCountryCode, StringComparison.OrdinalIgnoreCase));
+ var usRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase));
- if (!string.IsNullOrEmpty(preferredLanguage))
- {
- preferredLanguage = NormalizeLanguage(preferredLanguage);
+ if (ourRelease != null)
+ {
+ var ratingPrefix = string.Equals(info.MetadataCountryCode, "us", StringComparison.OrdinalIgnoreCase) ? string.Empty : info.MetadataCountryCode + "-";
+ var newRating = ratingPrefix + ourRelease.Certification;
- languages.Add(preferredLanguage);
+ newRating = newRating.Replace("de-", "FSK-", StringComparison.OrdinalIgnoreCase);
- if (preferredLanguage.Length == 5) // like en-US
+ movie.OfficialRating = newRating;
+ }
+ else if (usRelease != null)
{
- // Currenty, TMDB supports 2-letter language codes only
- // They are planning to change this in the future, thus we're
- // supplying both codes if we're having a 5-letter code.
- languages.Add(preferredLanguage.Substring(0, 2));
+ movie.OfficialRating = usRelease.Certification;
}
}
- languages.Add("null");
+ movie.PremiereDate = movieResult.ReleaseDate;
+ movie.ProductionYear = movieResult.ReleaseDate?.Year;
- if (!string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase))
+ if (movieResult.ProductionCompanies != null)
{
- languages.Add("en");
+ movie.SetStudios(movieResult.ProductionCompanies.Select(c => c.Name));
}
- return string.Join(',', languages);
- }
+ var genres = movieResult.Genres;
- public static string NormalizeLanguage(string language)
- {
- if (!string.IsNullOrEmpty(language))
+ foreach (var genre in genres.Select(g => g.Name))
{
- // They require this to be uppercase
- // Everything after the hyphen must be written in uppercase due to a way TMDB wrote their api.
- // See here: https://www.themoviedb.org/talk/5119221d760ee36c642af4ad?page=3#56e372a0c3a3685a9e0019ab
- var parts = language.Split('-');
+ movie.AddGenre(genre);
+ }
- if (parts.Length == 2)
+ if (movieResult.Keywords?.Keywords != null)
+ {
+ for (var i = 0; i < movieResult.Keywords.Keywords.Count; i++)
{
- language = parts[0] + "-" + parts[1].ToUpperInvariant();
+ movie.AddTag(movieResult.Keywords.Keywords[i].Name);
}
}
- return language;
- }
-
- public static string AdjustImageLanguage(string imageLanguage, string requestLanguage)
- {
- if (!string.IsNullOrEmpty(imageLanguage)
- && !string.IsNullOrEmpty(requestLanguage)
- && requestLanguage.Length > 2
- && imageLanguage.Length == 2
- && requestLanguage.StartsWith(imageLanguage, StringComparison.OrdinalIgnoreCase))
+ if (movieResult.Credits?.Cast != null)
{
- return requestLanguage;
- }
-
- return imageLanguage;
- }
+ // TODO configurable
+ foreach (var actor in movieResult.Credits.Cast.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers))
+ {
+ var personInfo = new PersonInfo
+ {
+ Name = actor.Name.Trim(),
+ Role = actor.Character,
+ Type = PersonType.Actor,
+ SortOrder = actor.Order
+ };
- /// <summary>
- /// Fetches the main result.
- /// </summary>
- /// <param name="id">The id.</param>
- /// <param name="isTmdbId">if set to <c>true</c> [is TMDB identifier].</param>
- /// <param name="language">The language.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task{CompleteMovieData}.</returns>
- internal async Task<MovieResult> FetchMainResult(string id, bool isTmdbId, string language, CancellationToken cancellationToken)
- {
- var url = string.Format(CultureInfo.InvariantCulture, GetMovieInfo3, id, TmdbUtils.ApiKey);
+ if (!string.IsNullOrWhiteSpace(actor.ProfilePath))
+ {
+ personInfo.ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath);
+ }
- if (!string.IsNullOrEmpty(language))
- {
- url += string.Format(CultureInfo.InvariantCulture, "&language={0}", NormalizeLanguage(language));
+ if (actor.Id > 0)
+ {
+ personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
+ }
- // Get images in english and with no language
- url += "&include_image_language=" + GetImageLanguagesParam(language);
+ metadataResult.AddPerson(personInfo);
+ }
}
- cancellationToken.ThrowIfCancellationRequested();
-
- using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
- foreach (var header in TmdbUtils.AcceptHeaders)
+ if (movieResult.Credits?.Crew != null)
{
- requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
+ var keepTypes = new[]
+ {
+ PersonType.Director,
+ PersonType.Writer,
+ PersonType.Producer
+ };
- using var mainResponse = await GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
- if (mainResponse.StatusCode == HttpStatusCode.NotFound)
- {
- return null;
- }
+ foreach (var person in movieResult.Credits.Crew)
+ {
+ // Normalize this
+ var type = TmdbUtils.MapCrewToPersonType(person);
- await using var stream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
- var mainResult = await _jsonSerializer.DeserializeFromStreamAsync<MovieResult>(stream).ConfigureAwait(false);
+ if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) &&
+ !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ {
+ continue;
+ }
- cancellationToken.ThrowIfCancellationRequested();
+ var personInfo = new PersonInfo
+ {
+ Name = person.Name.Trim(),
+ Role = person.Job,
+ Type = type
+ };
- // If the language preference isn't english, then have the overview fallback to english if it's blank
- if (mainResult != null &&
- string.IsNullOrEmpty(mainResult.Overview) &&
- !string.IsNullOrEmpty(language) &&
- !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
- {
- _logger.LogInformation("MovieDbProvider couldn't find meta for language " + language + ". Trying English...");
+ if (!string.IsNullOrWhiteSpace(person.ProfilePath))
+ {
+ personInfo.ImageUrl = _tmdbClientManager.GetPosterUrl(person.ProfilePath);
+ }
- url = string.Format(CultureInfo.InvariantCulture, GetMovieInfo3, id, TmdbUtils.ApiKey) + "&language=en";
+ if (person.Id > 0)
+ {
+ personInfo.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
+ }
- if (!string.IsNullOrEmpty(language))
- {
- // Get images in english and with no language
- url += "&include_image_language=" + GetImageLanguagesParam(language);
+ metadataResult.AddPerson(personInfo);
}
+ }
+
- using var langRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);
- foreach (var header in TmdbUtils.AcceptHeaders)
+ if (movieResult.Videos?.Results != null)
+ {
+ var trailers = new List<MediaUrl>();
+ for (var i = 0; i < movieResult.Videos.Results.Count; i++)
{
- langRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
+ var video = movieResult.Videos.Results[0];
+ if (!TmdbUtils.IsTrailerType(video))
+ {
+ continue;
+ }
- using var langResponse = await GetMovieDbResponse(langRequestMessage, cancellationToken).ConfigureAwait(false);
+ trailers.Add(new MediaUrl
+ {
+ Url = string.Format(CultureInfo.InvariantCulture, "https://www.youtube.com/watch?v={0}", video.Key),
+ Name = video.Name
+ });
+ }
- await using var langStream = await langResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
- var langResult = await _jsonSerializer.DeserializeFromStreamAsync<MovieResult>(stream).ConfigureAwait(false);
- mainResult.Overview = langResult.Overview;
+ movie.RemoteTrailers = trailers;
}
- return mainResult;
- }
-
- /// <summary>
- /// Gets the movie db response.
- /// </summary>
- /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
- internal Task<HttpResponseMessage> GetMovieDbResponse(HttpRequestMessage message, CancellationToken cancellationToken = default)
- {
- message.Headers.UserAgent.ParseAdd(_appHost.ApplicationUserAgent);
- return _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(message, cancellationToken);
+ return metadataResult;
}
/// <inheritdoc />
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs
deleted file mode 100644
index 36a4eef8a..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs
+++ /dev/null
@@ -1,302 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Net;
-using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Text.RegularExpressions;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.Search;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
-{
- public class TmdbSearch
- {
- private const string SearchUrl = TmdbUtils.BaseTmdbApiUrl + @"3/search/{3}?api_key={1}&query={0}&language={2}";
- private const string SearchUrlTvWithYear = TmdbUtils.BaseTmdbApiUrl + @"3/search/tv?api_key={1}&query={0}&language={2}&first_air_date_year={3}";
- private const string SearchUrlMovieWithYear = TmdbUtils.BaseTmdbApiUrl + @"3/search/movie?api_key={1}&query={0}&language={2}&primary_release_year={3}";
-
- private static readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
- private static readonly Regex _cleanEnclosed = new Regex(@"\p{Ps}.*\p{Pe}", RegexOptions.Compiled);
- private static readonly Regex _cleanNonWord = new Regex(@"[\W_]+", RegexOptions.Compiled);
- private static readonly Regex _cleanStopWords = new Regex(
- @"\b( # Start at word boundary
- 19[0-9]{2}|20[0-9]{2}| # 1900-2099
- S[0-9]{2}| # Season
- E[0-9]{2}| # Episode
- (2160|1080|720|576|480)[ip]?| # Resolution
- [xh]?264| # Encoding
- (web|dvd|bd|hdtv|hd)rip| # *Rip
- web|hdtv|mp4|bluray|ktr|dl|single|imageset|internal|doku|dubbed|retail|xxx|flac
- ).* # Match rest of string",
- RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace | RegexOptions.IgnoreCase);
-
- private readonly ILogger _logger;
- private readonly IJsonSerializer _json;
- private readonly ILibraryManager _libraryManager;
-
- public TmdbSearch(ILogger logger, IJsonSerializer json, ILibraryManager libraryManager)
- {
- _logger = logger;
- _json = json;
- _libraryManager = libraryManager;
- }
-
- public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo idInfo, CancellationToken cancellationToken)
- {
- return GetSearchResults(idInfo, "tv", cancellationToken);
- }
-
- public Task<IEnumerable<RemoteSearchResult>> GetMovieSearchResults(ItemLookupInfo idInfo, CancellationToken cancellationToken)
- {
- return GetSearchResults(idInfo, "movie", cancellationToken);
- }
-
- public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(BoxSetInfo idInfo, CancellationToken cancellationToken)
- {
- return GetSearchResults(idInfo, "collection", cancellationToken);
- }
-
- private async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ItemLookupInfo idInfo, string searchType, CancellationToken cancellationToken)
- {
- var name = idInfo.Name;
- var year = idInfo.Year;
-
- if (string.IsNullOrWhiteSpace(name))
- {
- return new List<RemoteSearchResult>();
- }
-
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
-
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
-
- // ParseName is required here.
- // Caller provides the filename with extension stripped and NOT the parsed filename
- var parsedName = _libraryManager.ParseName(name);
- var yearInName = parsedName.Year;
- name = parsedName.Name;
- year ??= yearInName;
-
- var language = idInfo.MetadataLanguage.ToLowerInvariant();
-
- // Replace sequences of non-word characters with space
- // TMDB expects a space separated list of words make sure that is the case
- name = _cleanNonWord.Replace(name, " ").Trim();
-
- _logger.LogInformation("TmdbSearch: Finding id for item: {0} ({1})", name, year);
- var results = await GetSearchResults(name, searchType, year, language, tmdbImageUrl, cancellationToken).ConfigureAwait(false);
-
- if (results.Count == 0)
- {
- // try in english if wasn't before
- if (!string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
- {
- results = await GetSearchResults(name, searchType, year, "en", tmdbImageUrl, cancellationToken).ConfigureAwait(false);
- }
- }
-
- // TODO: retrying alternatives should be done outside the search
- // provider so that the retry logic can be common for all search
- // providers
- if (results.Count == 0)
- {
- var name2 = parsedName.Name;
-
- // Remove things enclosed in []{}() etc
- name2 = _cleanEnclosed.Replace(name2, string.Empty);
-
- // Replace sequences of non-word characters with space
- name2 = _cleanNonWord.Replace(name2, " ");
-
- // Clean based on common stop words / tokens
- name2 = _cleanStopWords.Replace(name2, string.Empty);
-
- // Trim whitespace
- name2 = name2.Trim();
-
- // Search again if the new name is different
- if (!string.Equals(name2, name, StringComparison.Ordinal) && !string.IsNullOrWhiteSpace(name2))
- {
- _logger.LogInformation("TmdbSearch: Finding id for item: {0} ({1})", name2, year);
- results = await GetSearchResults(name2, searchType, year, language, tmdbImageUrl, cancellationToken).ConfigureAwait(false);
-
- if (results.Count == 0 && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
- {
- // one more time, in english
- results = await GetSearchResults(name2, searchType, year, "en", tmdbImageUrl, cancellationToken).ConfigureAwait(false);
- }
- }
- }
-
- return results.Where(i =>
- {
- if (year.HasValue && i.ProductionYear.HasValue)
- {
- // Allow one year tolerance
- return Math.Abs(year.Value - i.ProductionYear.Value) <= 1;
- }
-
- return true;
- });
- }
-
- private Task<List<RemoteSearchResult>> GetSearchResults(string name, string type, int? year, string language, string baseImageUrl, CancellationToken cancellationToken)
- {
- switch (type)
- {
- case "tv":
- return GetSearchResultsTv(name, year, language, baseImageUrl, cancellationToken);
- default:
- return GetSearchResultsGeneric(name, type, year, language, baseImageUrl, cancellationToken);
- }
- }
-
- private async Task<List<RemoteSearchResult>> GetSearchResultsGeneric(string name, string type, int? year, string language, string baseImageUrl, CancellationToken cancellationToken)
- {
- if (string.IsNullOrWhiteSpace(name))
- {
- throw new ArgumentException("String can't be null or empty.", nameof(name));
- }
-
- string url3;
- if (year != null && string.Equals(type, "movie", StringComparison.OrdinalIgnoreCase))
- {
- url3 = string.Format(
- CultureInfo.InvariantCulture,
- SearchUrlMovieWithYear,
- WebUtility.UrlEncode(name),
- TmdbUtils.ApiKey,
- language,
- year);
- }
- else
- {
- url3 = string.Format(
- CultureInfo.InvariantCulture,
- SearchUrl,
- WebUtility.UrlEncode(name),
- TmdbUtils.ApiKey,
- language,
- type);
- }
-
- using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url3);
- foreach (var header in TmdbUtils.AcceptHeaders)
- {
- requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
-
- using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
- var searchResults = await _json.DeserializeFromStreamAsync<TmdbSearchResult<MovieResult>>(stream).ConfigureAwait(false);
-
- var results = searchResults.Results ?? new List<MovieResult>();
-
- return results
- .Select(i =>
- {
- var remoteResult = new RemoteSearchResult
- {
- SearchProviderName = TmdbMovieProvider.Current.Name,
- Name = i.Title ?? i.Name ?? i.Original_Title,
- ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path
- };
-
- if (!string.IsNullOrWhiteSpace(i.Release_Date))
- {
- // These dates are always in this exact format
- if (DateTime.TryParseExact(i.Release_Date, "yyyy-MM-dd", _usCulture, DateTimeStyles.None, out var r))
- {
- remoteResult.PremiereDate = r.ToUniversalTime();
- remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
- }
- }
-
- remoteResult.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture));
-
- return remoteResult;
- })
- .ToList();
- }
-
- private async Task<List<RemoteSearchResult>> GetSearchResultsTv(string name, int? year, string language, string baseImageUrl, CancellationToken cancellationToken)
- {
- if (string.IsNullOrWhiteSpace(name))
- {
- throw new ArgumentException("String can't be null or empty.", nameof(name));
- }
-
- string url3;
- if (year == null)
- {
- url3 = string.Format(
- CultureInfo.InvariantCulture,
- SearchUrl,
- WebUtility.UrlEncode(name),
- TmdbUtils.ApiKey,
- language,
- "tv");
- }
- else
- {
- url3 = string.Format(
- CultureInfo.InvariantCulture,
- SearchUrlTvWithYear,
- WebUtility.UrlEncode(name),
- TmdbUtils.ApiKey,
- language,
- year);
- }
-
- using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url3);
- foreach (var header in TmdbUtils.AcceptHeaders)
- {
- requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
-
- using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
- var searchResults = await _json.DeserializeFromStreamAsync<TmdbSearchResult<TvResult>>(stream).ConfigureAwait(false);
-
- var results = searchResults.Results ?? new List<TvResult>();
-
- return results
- .Select(i =>
- {
- var remoteResult = new RemoteSearchResult
- {
- SearchProviderName = TmdbMovieProvider.Current.Name,
- Name = i.Name ?? i.Original_Name,
- ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path
- };
-
- if (!string.IsNullOrWhiteSpace(i.First_Air_Date))
- {
- // These dates are always in this exact format
- if (DateTime.TryParseExact(i.First_Air_Date, "yyyy-MM-dd", _usCulture, DateTimeStyles.None, out var r))
- {
- remoteResult.PremiereDate = r.ToUniversalTime();
- remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
- }
- }
-
- remoteResult.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture));
-
- return remoteResult;
- })
- .ToList();
- }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettingsResult.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettingsResult.cs
deleted file mode 100644
index c7ba97438..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSettingsResult.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
-{
- internal class TmdbSettingsResult
- {
- public TmdbImageSettings images { get; set; }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs
deleted file mode 100644
index b88ecce87..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Music/TmdbMusicVideoProvider.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Providers;
-using MediaBrowser.Providers.Plugins.Tmdb.Movies;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Music
-{
- public class TmdbMusicVideoProvider : IRemoteMetadataProvider<MusicVideo, MusicVideoInfo>
- {
- public string Name => TmdbMovieProvider.Current.Name;
-
- public Task<MetadataResult<MusicVideo>> GetMetadata(MusicVideoInfo info, CancellationToken cancellationToken)
- {
- return TmdbMovieProvider.Current.GetItemMetadata<MusicVideo>(info, cancellationToken);
- }
-
- public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MusicVideoInfo searchInfo, CancellationToken cancellationToken)
- {
- return Task.FromResult((IEnumerable<RemoteSearchResult>)new List<RemoteSearchResult>());
- }
-
- public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
- {
- throw new NotImplementedException();
- }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
index f2d2c8120..3f57c4bc4 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs
@@ -2,40 +2,33 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.People;
-using MediaBrowser.Providers.Plugins.Tmdb.Movies;
namespace MediaBrowser.Providers.Plugins.Tmdb.People
{
public class TmdbPersonImageProvider : IRemoteImageProvider, IHasOrder
{
- private readonly IServerConfigurationManager _config;
- private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClientFactory _httpClientFactory;
+ private readonly TmdbClientManager _tmdbClientManager;
- public TmdbPersonImageProvider(IServerConfigurationManager config, IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory)
+ public TmdbPersonImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
{
- _config = config;
- _jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
+ _tmdbClientManager = tmdbClientManager;
}
- public static string ProviderName => TmdbUtils.ProviderName;
-
/// <inheritdoc />
- public string Name => ProviderName;
+ public string Name => TmdbUtils.ProviderName;
/// <inheritdoc />
public int Order => 0;
@@ -56,78 +49,37 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
var person = (Person)item;
- var id = person.GetProviderId(MetadataProvider.Tmdb);
-
- if (!string.IsNullOrEmpty(id))
- {
- await TmdbPersonProvider.Current.EnsurePersonInfo(id, cancellationToken).ConfigureAwait(false);
-
- var dataFilePath = TmdbPersonProvider.GetPersonDataFilePath(_config.ApplicationPaths, id);
-
- var result = _jsonSerializer.DeserializeFromFile<PersonResult>(dataFilePath);
-
- var images = result.Images ?? new PersonImages();
-
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
-
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
-
- return GetImages(images, item.GetPreferredMetadataLanguage(), tmdbImageUrl);
- }
-
- return new List<RemoteImageInfo>();
- }
-
- private IEnumerable<RemoteImageInfo> GetImages(PersonImages images, string preferredLanguage, string baseImageUrl)
- {
- var list = new List<RemoteImageInfo>();
+ var personTmdbId = Convert.ToInt32(person.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
- if (images.Profiles != null)
+ if (personTmdbId > 0)
{
- list.AddRange(images.Profiles.Select(i => new RemoteImageInfo
+ var personResult = await _tmdbClientManager.GetPersonAsync(personTmdbId, cancellationToken).ConfigureAwait(false);
+ if (personResult?.Images?.Profiles == null)
{
- ProviderName = Name,
- Type = ImageType.Primary,
- Width = i.Width,
- Height = i.Height,
- Language = GetLanguage(i),
- Url = baseImageUrl + i.File_Path
- }));
- }
-
- var language = preferredLanguage;
-
- var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
-
- return list.OrderByDescending(i =>
- {
- if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase))
- {
- return 3;
+ return Enumerable.Empty<RemoteImageInfo>();
}
- if (!isLanguageEn)
- {
- if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
- {
- return 2;
- }
- }
+ var remoteImages = new List<RemoteImageInfo>();
+ var language = item.GetPreferredMetadataLanguage();
- if (string.IsNullOrEmpty(i.Language))
+ for (var i = 0; i < personResult.Images.Profiles.Count; i++)
{
- return isLanguageEn ? 3 : 2;
+ var image = personResult.Images.Profiles[i];
+ remoteImages.Add(new RemoteImageInfo
+ {
+ ProviderName = Name,
+ Type = ImageType.Primary,
+ Width = image.Width,
+ Height = image.Height,
+ Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language),
+ Url = _tmdbClientManager.GetProfileUrl(image.FilePath)
+ });
}
- return 0;
- })
- .ThenByDescending(i => i.CommunityRating ?? 0)
- .ThenByDescending(i => i.VoteCount ?? 0);
- }
+ return remoteImages.OrderByLanguageDescending(language);
+ }
- private string GetLanguage(Profile profile)
- {
- return profile.Iso_639_1?.ToString();
+ return Enumerable.Empty<RemoteImageInfo>();
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
index 777ebce49..4384c203e 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
@@ -3,198 +3,130 @@
using System;
using System.Collections.Generic;
using System.Globalization;
-using System.IO;
using System.Linq;
-using System.Net;
using System.Net.Http;
-using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.People;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.Search;
-using MediaBrowser.Providers.Plugins.Tmdb.Movies;
-using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Plugins.Tmdb.People
{
public class TmdbPersonProvider : IRemoteMetadataProvider<Person, PersonLookupInfo>
{
- private const string DataFileName = "info.json";
-
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
-
- private readonly IJsonSerializer _jsonSerializer;
- private readonly IFileSystem _fileSystem;
- private readonly IServerConfigurationManager _configurationManager;
private readonly IHttpClientFactory _httpClientFactory;
+ private readonly TmdbClientManager _tmdbClientManager;
- public TmdbPersonProvider(
- IFileSystem fileSystem,
- IServerConfigurationManager configurationManager,
- IJsonSerializer jsonSerializer,
- IHttpClientFactory httpClientFactory)
+ public TmdbPersonProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
{
- _fileSystem = fileSystem;
- _configurationManager = configurationManager;
- _jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
- Current = this;
+ _tmdbClientManager = tmdbClientManager;
}
- internal static TmdbPersonProvider Current { get; private set; }
-
public string Name => TmdbUtils.ProviderName;
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(PersonLookupInfo searchInfo, CancellationToken cancellationToken)
{
- var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb);
-
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
-
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
+ var personTmdbId = Convert.ToInt32(searchInfo.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
- if (!string.IsNullOrEmpty(tmdbId))
+ if (personTmdbId <= 0)
{
- await EnsurePersonInfo(tmdbId, cancellationToken).ConfigureAwait(false);
+ var personResult = await _tmdbClientManager.GetPersonAsync(personTmdbId, cancellationToken).ConfigureAwait(false);
- var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, tmdbId);
- var info = _jsonSerializer.DeserializeFromFile<PersonResult>(dataFilePath);
-
- IReadOnlyList<Profile> images = info.Images?.Profiles ?? Array.Empty<Profile>();
-
- var result = new RemoteSearchResult
+ if (personResult != null)
{
- Name = info.Name,
-
- SearchProviderName = Name,
+ var result = new RemoteSearchResult
+ {
+ Name = personResult.Name,
+ SearchProviderName = Name,
+ Overview = personResult.Biography
+ };
- ImageUrl = images.Count == 0 ? null : (tmdbImageUrl + images[0].File_Path)
- };
+ if (personResult.Images?.Profiles != null && personResult.Images.Profiles.Count > 0)
+ {
+ result.ImageUrl = _tmdbClientManager.GetProfileUrl(personResult.Images.Profiles[0].FilePath);
+ }
- result.SetProviderId(MetadataProvider.Tmdb, info.Id.ToString(_usCulture));
- result.SetProviderId(MetadataProvider.Imdb, info.Imdb_Id);
+ result.SetProviderId(MetadataProvider.Tmdb, personResult.Id.ToString(CultureInfo.InvariantCulture));
+ result.SetProviderId(MetadataProvider.Imdb, personResult.ExternalIds.ImdbId);
- return new[] { result };
+ return new[] { result };
+ }
}
+ // TODO why? Because of the old rate limit?
if (searchInfo.IsAutomated)
{
// Don't hammer moviedb searching by name
- return Array.Empty<RemoteSearchResult>();
+ return Enumerable.Empty<RemoteSearchResult>();
}
- var url = string.Format(
- CultureInfo.InvariantCulture,
- TmdbUtils.BaseTmdbApiUrl + @"3/search/person?api_key={1}&query={0}",
- WebUtility.UrlEncode(searchInfo.Name),
- TmdbUtils.ApiKey);
+ var personSearchResult = await _tmdbClientManager.SearchPersonAsync(searchInfo.Name, cancellationToken).ConfigureAwait(false);
- using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
- foreach (var header in TmdbUtils.AcceptHeaders)
+ var remoteSearchResults = new List<RemoteSearchResult>();
+ for (var i = 0; i < personSearchResult.Count; i++)
{
- requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
-
- var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
-
- var result2 = await _jsonSerializer.DeserializeFromStreamAsync<TmdbSearchResult<PersonSearchResult>>(stream).ConfigureAwait(false)
- ?? new TmdbSearchResult<PersonSearchResult>();
-
- return result2.Results.Select(i => GetSearchResult(i, tmdbImageUrl));
- }
-
- private RemoteSearchResult GetSearchResult(PersonSearchResult i, string baseImageUrl)
- {
- var result = new RemoteSearchResult
- {
- SearchProviderName = Name,
-
- Name = i.Name,
-
- ImageUrl = string.IsNullOrEmpty(i.Profile_Path) ? null : baseImageUrl + i.Profile_Path
- };
+ var person = personSearchResult[i];
+ var remoteSearchResult = new RemoteSearchResult
+ {
+ SearchProviderName = Name,
+ Name = person.Name,
+ ImageUrl = _tmdbClientManager.GetProfileUrl(person.ProfilePath)
+ };
- result.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture));
+ remoteSearchResult.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
+ remoteSearchResults.Add(remoteSearchResult);
+ }
- return result;
+ return remoteSearchResults;
}
public async Task<MetadataResult<Person>> GetMetadata(PersonLookupInfo id, CancellationToken cancellationToken)
{
- var tmdbId = id.GetProviderId(MetadataProvider.Tmdb);
+ var personTmdbId = Convert.ToInt32(id.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
// We don't already have an Id, need to fetch it
- if (string.IsNullOrEmpty(tmdbId))
+ if (personTmdbId <= 0)
{
- tmdbId = await GetTmdbId(id, cancellationToken).ConfigureAwait(false);
+ var personSearchResults = await _tmdbClientManager.SearchPersonAsync(id.Name, cancellationToken).ConfigureAwait(false);
+ if (personSearchResults.Count > 0)
+ {
+ personTmdbId = personSearchResults[0].Id;
+ }
}
var result = new MetadataResult<Person>();
- if (!string.IsNullOrEmpty(tmdbId))
+ if (personTmdbId > 0)
{
- try
- {
- await EnsurePersonInfo(tmdbId, cancellationToken).ConfigureAwait(false);
- }
- catch (HttpException ex)
- {
- if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
- {
- return result;
- }
-
- throw;
- }
-
- var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, tmdbId);
-
- var info = _jsonSerializer.DeserializeFromFile<PersonResult>(dataFilePath);
+ var person = await _tmdbClientManager.GetPersonAsync(personTmdbId, cancellationToken).ConfigureAwait(false);
- var item = new Person();
result.HasMetadata = true;
- // Take name from incoming info, don't rename the person
- // TODO: This should go in PersonMetadataService, not each person provider
- item.Name = id.Name;
-
- // item.HomePageUrl = info.homepage;
-
- if (!string.IsNullOrWhiteSpace(info.Place_Of_Birth))
+ var item = new Person
{
- item.ProductionLocations = new string[] { info.Place_Of_Birth };
- }
-
- item.Overview = info.Biography;
-
- if (DateTime.TryParseExact(info.Birthday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out var date))
- {
- item.PremiereDate = date.ToUniversalTime();
- }
+ // Take name from incoming info, don't rename the person
+ // TODO: This should go in PersonMetadataService, not each person provider
+ Name = id.Name,
+ HomePageUrl = person.Homepage,
+ Overview = person.Biography,
+ PremiereDate = person.Birthday?.ToUniversalTime(),
+ EndDate = person.Deathday?.ToUniversalTime()
+ };
- if (DateTime.TryParseExact(info.Deathday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out date))
+ if (!string.IsNullOrWhiteSpace(person.PlaceOfBirth))
{
- item.EndDate = date.ToUniversalTime();
+ item.ProductionLocations = new[] { person.PlaceOfBirth };
}
- item.SetProviderId(MetadataProvider.Tmdb, info.Id.ToString(_usCulture));
+ item.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
- if (!string.IsNullOrEmpty(info.Imdb_Id))
+ if (!string.IsNullOrEmpty(person.ImdbId))
{
- item.SetProviderId(MetadataProvider.Imdb, info.Imdb_Id);
+ item.SetProviderId(MetadataProvider.Imdb, person.ImdbId);
}
result.HasMetadata = true;
@@ -204,65 +136,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
return result;
}
- /// <summary>
- /// Gets the TMDB id.
- /// </summary>
- /// <param name="info">The information.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task{System.String}.</returns>
- private async Task<string> GetTmdbId(PersonLookupInfo info, CancellationToken cancellationToken)
- {
- var results = await GetSearchResults(info, cancellationToken).ConfigureAwait(false);
-
- return results.Select(i => i.GetProviderId(MetadataProvider.Tmdb)).FirstOrDefault();
- }
-
- internal async Task EnsurePersonInfo(string id, CancellationToken cancellationToken)
- {
- var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, id);
-
- var fileInfo = _fileSystem.GetFileSystemInfo(dataFilePath);
-
- if (fileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
- {
- return;
- }
-
- var url = string.Format(
- CultureInfo.InvariantCulture,
- TmdbUtils.BaseTmdbApiUrl + @"3/person/{1}?api_key={0}&append_to_response=credits,images,external_ids",
- TmdbUtils.ApiKey,
- id);
-
- using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
- foreach (var header in TmdbUtils.AcceptHeaders)
- {
- requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
-
- using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
- Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
- await using var fs = new FileStream(dataFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
- await response.Content.CopyToAsync(fs).ConfigureAwait(false);
- }
-
- private static string GetPersonDataPath(IApplicationPaths appPaths, string tmdbId)
- {
- var letter = tmdbId.GetMD5().ToString().AsSpan().Slice(0, 1);
-
- return Path.Join(GetPersonsDataPath(appPaths), letter, tmdbId);
- }
-
- internal static string GetPersonDataFilePath(IApplicationPaths appPaths, string tmdbId)
- {
- return Path.Combine(GetPersonDataPath(appPaths, tmdbId), DataFileName);
- }
-
- private static string GetPersonsDataPath(IApplicationPaths appPaths)
- {
- return Path.Combine(appPaths.CachePath, "tmdb-people");
- }
-
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
index c56774f8e..fe9e483c6 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeImageProvider.cs
@@ -2,40 +2,37 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-using MediaBrowser.Providers.Plugins.Tmdb.Movies;
-using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
- public class TmdbEpisodeImageProvider :
- TmdbEpisodeProviderBase,
- IRemoteImageProvider,
- IHasOrder
+ public class TmdbEpisodeImageProvider : IRemoteImageProvider, IHasOrder
{
- public TmdbEpisodeImageProvider(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
- : base(httpClientFactory, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory)
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly TmdbClientManager _tmdbClientManager;
+
+ public TmdbEpisodeImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
{
+ _httpClientFactory = httpClientFactory;
+ _tmdbClientManager = tmdbClientManager;
}
- public string Name => TmdbUtils.ProviderName;
-
// After TheTvDb
public int Order => 1;
+ public string Name => TmdbUtils.ProviderName;
+
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
return new List<ImageType>
@@ -49,13 +46,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var episode = (Controller.Entities.TV.Episode)item;
var series = episode.Series;
- var seriesId = series?.GetProviderId(MetadataProvider.Tmdb);
+ var seriesTmdbId = Convert.ToInt32(series?.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
- var list = new List<RemoteImageInfo>();
-
- if (string.IsNullOrEmpty(seriesId))
+ if (seriesTmdbId <= 0)
{
- return list;
+ return Enumerable.Empty<RemoteImageInfo>();
}
var seasonNumber = episode.ParentIndexNumber;
@@ -63,71 +58,45 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (!seasonNumber.HasValue || !episodeNumber.HasValue)
{
- return list;
+ return Enumerable.Empty<RemoteImageInfo>();
}
var language = item.GetPreferredMetadataLanguage();
- var response = await GetEpisodeInfo(
- seriesId,
- seasonNumber.Value,
- episodeNumber.Value,
- language,
- cancellationToken).ConfigureAwait(false);
-
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
+ var episodeResult = await _tmdbClientManager
+ .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken)
+ .ConfigureAwait(false);
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
-
- list.AddRange(GetPosters(response.Images).Select(i => new RemoteImageInfo
- {
- Url = tmdbImageUrl + 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
- }));
-
- var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
-
- return list.OrderByDescending(i =>
+ if (episodeResult?.Images?.Stills == null)
{
- if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase))
- {
- return 3;
- }
+ return Enumerable.Empty<RemoteImageInfo>();
+ }
- if (!isLanguageEn)
- {
- if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
- {
- return 2;
- }
- }
+ var remoteImages = new List<RemoteImageInfo>();
- if (string.IsNullOrEmpty(i.Language))
+ for (var i = 0; i < episodeResult.Images.Stills.Count; i++)
+ {
+ var image = episodeResult.Images.Stills[i];
+ remoteImages.Add(new RemoteImageInfo
{
- return isLanguageEn ? 3 : 2;
- }
-
- return 0;
- })
- .ThenByDescending(i => i.CommunityRating ?? 0)
- .ThenByDescending(i => i.VoteCount ?? 0);
- }
+ Url = _tmdbClientManager.GetStillUrl(image.FilePath),
+ CommunityRating = image.VoteAverage,
+ VoteCount = image.VoteCount,
+ Width = image.Width,
+ Height = image.Height,
+ Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language),
+ ProviderName = Name,
+ Type = ImageType.Primary,
+ RatingType = RatingType.Score
+ });
+ }
- private IEnumerable<Still> GetPosters(StillImages images)
- {
- return images.Stills ?? new List<Still>();
+ return remoteImages.OrderByLanguageDescending(language);
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return GetResponse(url, cancellationToken);
+ return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
}
public bool Supports(BaseItem item)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
index a7e3a03fe..55f1ba7dc 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
@@ -4,32 +4,27 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
-using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
- public class TmdbEpisodeProvider :
- TmdbEpisodeProviderBase,
- IRemoteMetadataProvider<Episode, EpisodeInfo>,
- IHasOrder
+ public class TmdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
{
- public TmdbEpisodeProvider(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
- : base(httpClientFactory, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory)
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly TmdbClientManager _tmdbClientManager;
+
+ public TmdbEpisodeProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
{
+ _httpClientFactory = httpClientFactory;
+ _tmdbClientManager = tmdbClientManager;
}
// After TheTvDb
@@ -39,51 +34,54 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
{
- var list = new List<RemoteSearchResult>();
-
// The search query must either provide an episode number or date
if (!searchInfo.IndexNumber.HasValue || !searchInfo.ParentIndexNumber.HasValue)
{
- return list;
+ return Enumerable.Empty<RemoteSearchResult>();
}
- var metadataResult = await GetMetadata(searchInfo, cancellationToken).ConfigureAwait(false);
+ var metadataResult = await GetMetadata(searchInfo, cancellationToken);
- if (metadataResult.HasMetadata)
+ if (!metadataResult.HasMetadata)
{
- var item = metadataResult.Item;
-
- list.Add(new RemoteSearchResult
- {
- IndexNumber = item.IndexNumber,
- Name = item.Name,
- ParentIndexNumber = item.ParentIndexNumber,
- PremiereDate = item.PremiereDate,
- ProductionYear = item.ProductionYear,
- ProviderIds = item.ProviderIds,
- SearchProviderName = Name,
- IndexNumberEnd = item.IndexNumberEnd
- });
+ return Enumerable.Empty<RemoteSearchResult>();
}
+ var list = new List<RemoteSearchResult>();
+
+ var item = metadataResult.Item;
+
+ list.Add(new RemoteSearchResult
+ {
+ IndexNumber = item.IndexNumber,
+ Name = item.Name,
+ ParentIndexNumber = item.ParentIndexNumber,
+ PremiereDate = item.PremiereDate,
+ ProductionYear = item.ProductionYear,
+ ProviderIds = item.ProviderIds,
+ SearchProviderName = Name,
+ IndexNumberEnd = item.IndexNumberEnd
+ });
+
return list;
}
public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken)
{
- var result = new MetadataResult<Episode>();
+ var metadataResult = new MetadataResult<Episode>();
// Allowing this will dramatically increase scan times
if (info.IsMissingEpisode)
{
- return result;
+ return metadataResult;
}
- info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out string seriesTmdbId);
+ info.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out string tmdbId);
- if (string.IsNullOrEmpty(seriesTmdbId))
+ var seriesTmdbId = Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture);
+ if (seriesTmdbId <= 0)
{
- return result;
+ return metadataResult;
}
var seasonNumber = info.ParentIndexNumber;
@@ -91,125 +89,121 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (!seasonNumber.HasValue || !episodeNumber.HasValue)
{
- return result;
+ return metadataResult;
}
- try
- {
- var response = await GetEpisodeInfo(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
+ var episodeResult = await _tmdbClientManager
+ .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
+ .ConfigureAwait(false);
- result.HasMetadata = true;
- result.QueriedById = true;
+ if (episodeResult == null)
+ {
+ return metadataResult;
+ }
- if (!string.IsNullOrEmpty(response.Overview))
- {
- // if overview is non-empty, we can assume that localized data was returned
- result.ResultLanguage = info.MetadataLanguage;
- }
+ metadataResult.HasMetadata = true;
+ metadataResult.QueriedById = true;
- var item = new Episode();
- result.Item = item;
+ if (!string.IsNullOrEmpty(episodeResult.Overview))
+ {
+ // if overview is non-empty, we can assume that localized data was returned
+ metadataResult.ResultLanguage = info.MetadataLanguage;
+ }
- item.Name = info.Name;
- item.IndexNumber = info.IndexNumber;
- item.ParentIndexNumber = info.ParentIndexNumber;
- item.IndexNumberEnd = info.IndexNumberEnd;
+ var item = new Episode
+ {
+ Name = info.Name,
+ IndexNumber = info.IndexNumber,
+ ParentIndexNumber = info.ParentIndexNumber,
+ IndexNumberEnd = info.IndexNumberEnd
+ };
- if (response.External_Ids != null && response.External_Ids.Tvdb_Id > 0)
- {
- item.SetProviderId(MetadataProvider.Tvdb, response.External_Ids.Tvdb_Id.Value.ToString(CultureInfo.InvariantCulture));
- }
+ if (!string.IsNullOrEmpty(episodeResult.ExternalIds?.TvdbId))
+ {
+ item.SetProviderId(MetadataProvider.Tvdb, episodeResult.ExternalIds.TvdbId);
+ }
- item.PremiereDate = response.Air_Date;
- item.ProductionYear = result.Item.PremiereDate.Value.Year;
+ item.PremiereDate = episodeResult.AirDate;
+ item.ProductionYear = episodeResult.AirDate?.Year;
- item.Name = response.Name;
- item.Overview = response.Overview;
+ item.Name = episodeResult.Name;
+ item.Overview = episodeResult.Overview;
- item.CommunityRating = (float)response.Vote_Average;
+ item.CommunityRating = Convert.ToSingle(episodeResult.VoteAverage);
- if (response.Videos?.Results != null)
+ if (episodeResult.Videos?.Results != null)
+ {
+ foreach (var video in episodeResult.Videos.Results)
{
- foreach (var video in response.Videos.Results)
+ if (TmdbUtils.IsTrailerType(video))
{
- if (video.Type.Equals("trailer", System.StringComparison.OrdinalIgnoreCase)
- || video.Type.Equals("clip", System.StringComparison.OrdinalIgnoreCase))
- {
- if (video.Site.Equals("youtube", System.StringComparison.OrdinalIgnoreCase))
- {
- var videoUrl = string.Format(CultureInfo.InvariantCulture, "http://www.youtube.com/watch?v={0}", video.Key);
- item.AddTrailerUrl(videoUrl);
- }
- }
+ var videoUrl = string.Format(CultureInfo.InvariantCulture, "http://www.youtube.com/watch?v={0}", video.Key);
+ item.AddTrailerUrl(videoUrl);
}
}
+ }
- result.ResetPeople();
+ var credits = episodeResult.Credits;
- var credits = response.Credits;
- if (credits != null)
+ if (credits?.Cast != null)
+ {
+ foreach (var actor in credits.Cast.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers))
{
- // Actors, Directors, Writers - all in People
- // actors come from cast
- if (credits.Cast != null)
+ metadataResult.AddPerson(new PersonInfo
{
- foreach (var actor in credits.Cast.OrderBy(a => a.Order))
- {
- result.AddPerson(new PersonInfo { Name = actor.Name.Trim(), Role = actor.Character, Type = PersonType.Actor, SortOrder = actor.Order });
- }
- }
-
- // guest stars
- if (credits.Guest_Stars != null)
- {
- foreach (var guest in credits.Guest_Stars.OrderBy(a => a.Order))
- {
- result.AddPerson(new PersonInfo { Name = guest.Name.Trim(), Role = guest.Character, Type = PersonType.GuestStar, SortOrder = guest.Order });
- }
- }
+ Name = actor.Name.Trim(),
+ Role = actor.Character,
+ Type = PersonType.Actor,
+ SortOrder = actor.Order
+ });
+ }
+ }
- // and the rest from crew
- if (credits.Crew != null)
+ if (credits?.GuestStars != null)
+ {
+ foreach (var guest in credits.GuestStars.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers))
+ {
+ metadataResult.AddPerson(new PersonInfo
{
- var keepTypes = new[]
- {
- PersonType.Director,
- PersonType.Writer,
- PersonType.Producer
- };
-
- foreach (var person in credits.Crew)
- {
- // Normalize this
- var type = TmdbUtils.MapCrewToPersonType(person);
-
- if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) &&
- !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
- {
- continue;
- }
-
- result.AddPerson(new PersonInfo { Name = person.Name.Trim(), Role = person.Job, Type = type });
- }
- }
+ Name = guest.Name.Trim(),
+ Role = guest.Character,
+ Type = PersonType.GuestStar,
+ SortOrder = guest.Order
+ });
}
}
- catch (HttpException ex)
+
+ // and the rest from crew
+ if (credits?.Crew != null)
{
- if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
+ foreach (var person in credits.Crew)
{
- return result;
- }
+ // Normalize this
+ var type = TmdbUtils.MapCrewToPersonType(person);
+
+ if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
+ && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ {
+ continue;
+ }
- throw;
+ metadataResult.AddPerson(new PersonInfo
+ {
+ Name = person.Name.Trim(),
+ Role = person.Job,
+ Type = type
+ });
+ }
}
- return result;
+ metadataResult.Item = item;
+
+ return metadataResult;
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- return GetResponse(url, cancellationToken);
+ return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs
deleted file mode 100644
index 34d2424a3..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProviderBase.cs
+++ /dev/null
@@ -1,156 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Globalization;
-using System.IO;
-using System.Net.Http;
-using System.Net.Http.Headers;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.TV;
-using MediaBrowser.Providers.Plugins.Tmdb.Movies;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.TV
-{
- public abstract class TmdbEpisodeProviderBase
- {
- private const string EpisodeUrlPattern = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}/season/{1}/episode/{2}?api_key={3}&append_to_response=images,external_ids,credits,videos";
-
- private readonly IHttpClientFactory _httpClientFactory;
- private readonly IServerConfigurationManager _configurationManager;
- private readonly IJsonSerializer _jsonSerializer;
- private readonly IFileSystem _fileSystem;
- private readonly ILogger<TmdbEpisodeProviderBase> _logger;
-
- protected TmdbEpisodeProviderBase(IHttpClientFactory httpClientFactory, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory)
- {
- _httpClientFactory = httpClientFactory;
- _configurationManager = configurationManager;
- _jsonSerializer = jsonSerializer;
- _fileSystem = fileSystem;
- _logger = loggerFactory.CreateLogger<TmdbEpisodeProviderBase>();
- }
-
- protected ILogger Logger => _logger;
-
- protected async Task<EpisodeResult> GetEpisodeInfo(
- string seriesTmdbId,
- int season,
- int episodeNumber,
- string preferredMetadataLanguage,
- CancellationToken cancellationToken)
- {
- await EnsureEpisodeInfo(seriesTmdbId, season, episodeNumber, preferredMetadataLanguage, cancellationToken)
- .ConfigureAwait(false);
-
- var dataFilePath = GetDataFilePath(seriesTmdbId, season, episodeNumber, preferredMetadataLanguage);
-
- return _jsonSerializer.DeserializeFromFile<EpisodeResult>(dataFilePath);
- }
-
- internal Task EnsureEpisodeInfo(string tmdbId, int seasonNumber, int episodeNumber, string language, CancellationToken cancellationToken)
- {
- if (string.IsNullOrEmpty(tmdbId))
- {
- throw new ArgumentNullException(nameof(tmdbId));
- }
-
- if (string.IsNullOrEmpty(language))
- {
- throw new ArgumentNullException(nameof(language));
- }
-
- var path = GetDataFilePath(tmdbId, seasonNumber, episodeNumber, language);
-
- 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;
- }
- }
-
- return DownloadEpisodeInfo(tmdbId, seasonNumber, episodeNumber, language, cancellationToken);
- }
-
- internal string GetDataFilePath(string tmdbId, int seasonNumber, int episodeNumber, string preferredLanguage)
- {
- if (string.IsNullOrEmpty(tmdbId))
- {
- throw new ArgumentNullException(nameof(tmdbId));
- }
-
- if (string.IsNullOrEmpty(preferredLanguage))
- {
- throw new ArgumentNullException(nameof(preferredLanguage));
- }
-
- var path = TmdbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId);
-
- var filename = string.Format(
- CultureInfo.InvariantCulture,
- "season-{0}-episode-{1}-{2}.json",
- seasonNumber.ToString(CultureInfo.InvariantCulture),
- episodeNumber.ToString(CultureInfo.InvariantCulture),
- preferredLanguage);
-
- return Path.Combine(path, filename);
- }
-
- internal async Task DownloadEpisodeInfo(string id, int seasonNumber, int episodeNumber, string preferredMetadataLanguage, CancellationToken cancellationToken)
- {
- var mainResult = await FetchMainResult(EpisodeUrlPattern, id, seasonNumber, episodeNumber, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
-
- var dataFilePath = GetDataFilePath(id, seasonNumber, episodeNumber, preferredMetadataLanguage);
-
- Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
- _jsonSerializer.SerializeToFile(mainResult, dataFilePath);
- }
-
- internal async Task<EpisodeResult> FetchMainResult(string urlPattern, string id, int seasonNumber, int episodeNumber, string language, CancellationToken cancellationToken)
- {
- var url = string.Format(
- CultureInfo.InvariantCulture,
- urlPattern,
- id,
- seasonNumber.ToString(CultureInfo.InvariantCulture),
- episodeNumber,
- TmdbUtils.ApiKey);
-
- if (!string.IsNullOrEmpty(language))
- {
- url += string.Format(CultureInfo.InvariantCulture, "&language={0}", language);
- }
-
- var includeImageLanguageParam = TmdbMovieProvider.GetImageLanguagesParam(language);
- // Get images in english and with no language
- url += "&include_image_language=" + includeImageLanguageParam;
-
- cancellationToken.ThrowIfCancellationRequested();
-
- using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
- foreach (var header in TmdbUtils.AcceptHeaders)
- {
- requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
-
- using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
- return await _jsonSerializer.DeserializeFromStreamAsync<EpisodeResult>(stream).ConfigureAwait(false);
- }
-
- protected Task<HttpResponseMessage> GetResponse(string url, CancellationToken cancellationToken)
- {
- return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
- }
- }
-}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
index dcc7f8700..0feaa5732 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonImageProvider.cs
@@ -2,7 +2,7 @@
using System;
using System.Collections.Generic;
-using System.IO;
+using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
@@ -13,29 +13,25 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-using MediaBrowser.Providers.Plugins.Tmdb.Movies;
namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
public class TmdbSeasonImageProvider : IRemoteImageProvider, IHasOrder
{
- private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClientFactory _httpClientFactory;
+ private readonly TmdbClientManager _tmdbClientManager;
- public TmdbSeasonImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory)
+ public TmdbSeasonImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
{
- _jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
+ _tmdbClientManager = tmdbClientManager;
}
public int Order => 1;
- public string Name => ProviderName;
-
- public static string ProviderName => TmdbUtils.ProviderName;
+ public string Name => TmdbUtils.ProviderName;
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
@@ -45,87 +41,45 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
var season = (Season)item;
- var series = season.Series;
-
- var seriesId = series?.GetProviderId(MetadataProvider.Tmdb);
-
- if (string.IsNullOrEmpty(seriesId))
- {
- return Enumerable.Empty<RemoteImageInfo>();
- }
+ var series = season?.Series;
- var seasonNumber = season.IndexNumber;
+ var seriesTmdbId = Convert.ToInt32(series?.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
- if (!seasonNumber.HasValue)
+ if (seriesTmdbId <= 0 || season?.IndexNumber == null)
{
return Enumerable.Empty<RemoteImageInfo>();
}
var language = item.GetPreferredMetadataLanguage();
- var results = await FetchImages(season, seriesId, language, cancellationToken).ConfigureAwait(false);
+ var seasonResult = await _tmdbClientManager
+ .GetSeasonAsync(seriesTmdbId, season.IndexNumber.Value, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken)
+ .ConfigureAwait(false);
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
-
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
-
- var list = results.Select(i => new RemoteImageInfo
- {
- Url = tmdbImageUrl + 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
- });
-
- var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
-
- return list.OrderByDescending(i =>
+ if (seasonResult?.Images?.Posters == null)
{
- if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase))
- {
- return 3;
- }
-
- if (!isLanguageEn)
- {
- if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
- {
- return 2;
- }
- }
-
- if (string.IsNullOrEmpty(i.Language))
- {
- return isLanguageEn ? 3 : 2;
- }
-
- return 0;
- })
- .ThenByDescending(i => i.CommunityRating ?? 0)
- .ThenByDescending(i => i.VoteCount ?? 0);
- }
-
- private async Task<List<Poster>> FetchImages(Season item, string tmdbId, string language, CancellationToken cancellationToken)
- {
- var seasonNumber = item.IndexNumber.GetValueOrDefault();
- await TmdbSeasonProvider.Current.EnsureSeasonInfo(tmdbId, seasonNumber, language, cancellationToken).ConfigureAwait(false);
-
- var path = TmdbSeasonProvider.Current.GetDataFilePath(tmdbId, seasonNumber, language);
+ return Enumerable.Empty<RemoteImageInfo>();
+ }
- if (!string.IsNullOrEmpty(path))
+ var remoteImages = new List<RemoteImageInfo>();
+ for (var i = 0; i < seasonResult.Images.Posters.Count; i++)
{
- if (File.Exists(path))
+ var image = seasonResult.Images.Posters[i];
+ remoteImages.Add(new RemoteImageInfo
{
- return _jsonSerializer.DeserializeFromFile<Models.TV.SeasonResult>(path).Images.Posters;
- }
+ Url = _tmdbClientManager.GetPosterUrl(image.FilePath),
+ CommunityRating = image.VoteAverage,
+ VoteCount = image.VoteCount,
+ Width = image.Width,
+ Height = image.Height,
+ Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language),
+ ProviderName = Name,
+ Type = ImageType.Primary,
+ RatingType = RatingType.Score
+ });
}
- return null;
+ return remoteImages.OrderByLanguageDescending(language);
}
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
index c9b257fcc..6ca462474 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
@@ -3,53 +3,28 @@
using System;
using System.Collections.Generic;
using System.Globalization;
-using System.IO;
-using System.Net;
+using System.Linq;
using System.Net.Http;
-using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.TV;
-using MediaBrowser.Providers.Plugins.Tmdb.Movies;
-using Microsoft.Extensions.Logging;
-using Season = MediaBrowser.Controller.Entities.TV.Season;
namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
public class TmdbSeasonProvider : IRemoteMetadataProvider<Season, SeasonInfo>
{
- private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}/season/{1}?api_key={2}&append_to_response=images,keywords,external_ids,credits,videos";
-
private readonly IHttpClientFactory _httpClientFactory;
- private readonly IServerConfigurationManager _configurationManager;
- private readonly IJsonSerializer _jsonSerializer;
- private readonly IFileSystem _fileSystem;
- private readonly ILogger<TmdbSeasonProvider> _logger;
-
- internal static TmdbSeasonProvider Current { get; private set; }
-
- public TmdbSeasonProvider(
- IHttpClientFactory httpClientFactory,
- IServerConfigurationManager configurationManager,
- IFileSystem fileSystem,
- IJsonSerializer jsonSerializer,
- ILogger<TmdbSeasonProvider> logger)
+ private readonly TmdbClientManager _tmdbClientManager;
+
+ public TmdbSeasonProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
{
_httpClientFactory = httpClientFactory;
- _configurationManager = configurationManager;
- _fileSystem = fileSystem;
- _jsonSerializer = jsonSerializer;
- _logger = logger;
- Current = this;
+ _tmdbClientManager = tmdbClientManager;
}
public string Name => TmdbUtils.ProviderName;
@@ -62,180 +37,86 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var seasonNumber = info.IndexNumber;
- if (!string.IsNullOrWhiteSpace(seriesTmdbId) && seasonNumber.HasValue)
+ if (string.IsNullOrWhiteSpace(seriesTmdbId) || !seasonNumber.HasValue)
{
- try
- {
- var seasonInfo = await GetSeasonInfo(seriesTmdbId, seasonNumber.Value, info.MetadataLanguage, cancellationToken)
- .ConfigureAwait(false);
-
- result.HasMetadata = true;
- result.Item = new Season();
-
- // Don't use moviedb season names for now until if/when we have field-level configuration
- // result.Item.Name = seasonInfo.name;
-
- result.Item.Name = info.Name;
-
- result.Item.IndexNumber = seasonNumber;
-
- result.Item.Overview = seasonInfo.Overview;
-
- if (seasonInfo.External_Ids != null && seasonInfo.External_Ids.Tvdb_Id > 0)
- {
- result.Item.SetProviderId(MetadataProvider.Tvdb, seasonInfo.External_Ids.Tvdb_Id.Value.ToString(CultureInfo.InvariantCulture));
- }
-
- var credits = seasonInfo.Credits;
- if (credits != null)
- {
- // Actors, Directors, Writers - all in People
- // actors come from cast
- if (credits.Cast != null)
- {
- // foreach (var actor in credits.cast.OrderBy(a => a.order)) result.Item.AddPerson(new PersonInfo { Name = actor.name.Trim(), Role = actor.character, Type = PersonType.Actor, SortOrder = actor.order });
- }
-
- // and the rest from crew
- if (credits.Crew != null)
- {
- // foreach (var person in credits.crew) result.Item.AddPerson(new PersonInfo { Name = person.name.Trim(), Role = person.job, Type = person.department });
- }
- }
-
- result.Item.PremiereDate = seasonInfo.Air_Date;
- result.Item.ProductionYear = result.Item.PremiereDate.Value.Year;
- }
- catch (HttpException ex)
- {
- _logger.LogError(ex, "No metadata found for {0}", seasonNumber.Value);
-
- if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
- {
- return result;
- }
-
- throw;
- }
+ return result;
}
- return result;
- }
-
- public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeasonInfo searchInfo, CancellationToken cancellationToken)
- {
- return Task.FromResult<IEnumerable<RemoteSearchResult>>(new List<RemoteSearchResult>());
- }
-
- public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
- {
- return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
- }
-
- private async Task<SeasonResult> GetSeasonInfo(
- string seriesTmdbId,
- int season,
- string preferredMetadataLanguage,
- CancellationToken cancellationToken)
- {
- await EnsureSeasonInfo(seriesTmdbId, season, preferredMetadataLanguage, cancellationToken)
- .ConfigureAwait(false);
-
- var dataFilePath = GetDataFilePath(seriesTmdbId, season, preferredMetadataLanguage);
-
- return _jsonSerializer.DeserializeFromFile<SeasonResult>(dataFilePath);
- }
+ var seasonResult = await _tmdbClientManager
+ .GetSeasonAsync(Convert.ToInt32(seriesTmdbId, CultureInfo.InvariantCulture), seasonNumber.Value, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
+ .ConfigureAwait(false);
- internal Task EnsureSeasonInfo(string tmdbId, int seasonNumber, string language, CancellationToken cancellationToken)
- {
- if (string.IsNullOrEmpty(tmdbId))
+ if (seasonResult == null)
{
- throw new ArgumentNullException(nameof(tmdbId));
+ return result;
}
- if (string.IsNullOrEmpty(language))
+ result.HasMetadata = true;
+ result.Item = new Season
{
- throw new ArgumentNullException(nameof(language));
- }
+ Name = info.Name,
+ IndexNumber = seasonNumber,
+ Overview = seasonResult?.Overview
+ };
- var path = GetDataFilePath(tmdbId, seasonNumber, language);
-
- var fileInfo = _fileSystem.GetFileSystemInfo(path);
+ if (!string.IsNullOrEmpty(seasonResult.ExternalIds?.TvdbId))
+ {
+ result.Item.SetProviderId(MetadataProvider.Tvdb, seasonResult.ExternalIds.TvdbId);
+ }
- if (fileInfo.Exists)
+ // TODO why was this disabled?
+ var credits = seasonResult.Credits;
+ if (credits?.Cast != null)
{
- // If it's recent or automatic updates are enabled, don't re-download
- if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2)
+ var cast = credits.Cast.OrderBy(c => c.Order).Take(TmdbUtils.MaxCastMembers).ToList();
+ for (var i = 0; i < cast.Count; i++)
{
- return Task.CompletedTask;
+ result.AddPerson(new PersonInfo
+ {
+ Name = cast[i].Name.Trim(),
+ Role = cast[i].Character,
+ Type = PersonType.Actor,
+ SortOrder = cast[i].Order
+ });
}
}
- return DownloadSeasonInfo(tmdbId, seasonNumber, language, cancellationToken);
- }
-
- internal string GetDataFilePath(string tmdbId, int seasonNumber, string preferredLanguage)
- {
- if (string.IsNullOrEmpty(tmdbId))
+ if (credits?.Crew != null)
{
- throw new ArgumentNullException(nameof(tmdbId));
- }
+ foreach (var person in credits.Crew)
+ {
+ // Normalize this
+ var type = TmdbUtils.MapCrewToPersonType(person);
- if (string.IsNullOrEmpty(preferredLanguage))
- {
- throw new ArgumentNullException(nameof(preferredLanguage));
- }
+ if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
+ && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ {
+ continue;
+ }
- var path = TmdbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId);
+ result.AddPerson(new PersonInfo
+ {
+ Name = person.Name.Trim(),
+ Role = person.Job,
+ Type = type
+ });
+ }
+ }
- var filename = string.Format(
- CultureInfo.InvariantCulture,
- "season-{0}-{1}.json",
- seasonNumber.ToString(CultureInfo.InvariantCulture),
- preferredLanguage);
+ result.Item.PremiereDate = seasonResult.AirDate;
+ result.Item.ProductionYear = seasonResult.AirDate?.Year;
- return Path.Combine(path, filename);
+ return result;
}
- internal async Task DownloadSeasonInfo(string id, int seasonNumber, string preferredMetadataLanguage, CancellationToken cancellationToken)
+ public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeasonInfo searchInfo, CancellationToken cancellationToken)
{
- var mainResult = await FetchMainResult(id, seasonNumber, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
-
- var dataFilePath = GetDataFilePath(id, seasonNumber, preferredMetadataLanguage);
-
- Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
- _jsonSerializer.SerializeToFile(mainResult, dataFilePath);
+ return Task.FromResult(Enumerable.Empty<RemoteSearchResult>());
}
- internal async Task<SeasonResult> FetchMainResult(string id, int seasonNumber, string language, CancellationToken cancellationToken)
+ public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
- var url = string.Format(
- CultureInfo.InvariantCulture,
- GetTvInfo3,
- id,
- seasonNumber.ToString(CultureInfo.InvariantCulture),
- TmdbUtils.ApiKey);
-
- if (!string.IsNullOrEmpty(language))
- {
- url += string.Format(CultureInfo.InvariantCulture, "&language={0}", TmdbMovieProvider.NormalizeLanguage(language));
- }
-
- var includeImageLanguageParam = TmdbMovieProvider.GetImageLanguagesParam(language);
- // Get images in english and with no language
- url += "&include_image_language=" + includeImageLanguageParam;
-
- cancellationToken.ThrowIfCancellationRequested();
-
- using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
- foreach (var header in TmdbUtils.AcceptHeaders)
- {
- requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
-
- using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
- return await _jsonSerializer.DeserializeFromStreamAsync<SeasonResult>(stream).ConfigureAwait(false);
+ return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
index 179ceb825..2fdec0196 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
@@ -2,7 +2,7 @@
using System;
using System.Collections.Generic;
-using System.IO;
+using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
@@ -13,28 +13,23 @@ using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.Extensions;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.TV;
-using MediaBrowser.Providers.Plugins.Tmdb.Movies;
namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
public class TmdbSeriesImageProvider : IRemoteImageProvider, IHasOrder
{
- private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClientFactory _httpClientFactory;
+ private readonly TmdbClientManager _tmdbClientManager;
- public TmdbSeriesImageProvider(IJsonSerializer jsonSerializer, IHttpClientFactory httpClientFactory)
+ public TmdbSeriesImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
{
- _jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
+ _tmdbClientManager = tmdbClientManager;
}
- public string Name => ProviderName;
-
- public static string ProviderName => TmdbUtils.ProviderName;
+ public string Name => TmdbUtils.ProviderName;
// After tvdb and fanart
public int Order => 2;
@@ -55,124 +50,60 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
- var list = new List<RemoteImageInfo>();
-
- var results = await FetchImages(item, null, cancellationToken).ConfigureAwait(false);
+ var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
- if (results == null)
+ if (string.IsNullOrEmpty(tmdbId))
{
- return list;
+ return null;
}
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
-
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
-
var language = item.GetPreferredMetadataLanguage();
- list.AddRange(GetPosters(results).Select(i => new RemoteImageInfo
- {
- Url = tmdbImageUrl + 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(results).Select(i => new RemoteImageInfo
- {
- Url = tmdbImageUrl + 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)
- {
- if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
- {
- return 2;
- }
- }
-
- if (string.IsNullOrEmpty(i.Language))
- {
- return isLanguageEn ? 3 : 2;
- }
-
- return 0;
- })
- .ThenByDescending(i => i.CommunityRating ?? 0)
- .ThenByDescending(i => i.VoteCount ?? 0);
- }
+ var series = await _tmdbClientManager
+ .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken)
+ .ConfigureAwait(false);
- /// <summary>
- /// Gets the posters.
- /// </summary>
- /// <param name="images">The images.</param>
- private IEnumerable<Poster> GetPosters(Images images)
- {
- return images.Posters ?? new List<Poster>();
- }
-
- /// <summary>
- /// Gets the backdrops.
- /// </summary>
- /// <param name="images">The images.</param>
- private IEnumerable<Backdrop> GetBackdrops(Images images)
- {
- var eligibleBackdrops = images.Backdrops ?? new List<Backdrop>();
-
- return eligibleBackdrops.OrderByDescending(i => i.Vote_Average)
- .ThenByDescending(i => i.Vote_Count);
- }
-
- /// <summary>
- /// Fetches the images.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <param name="language">The language.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task{MovieImages}.</returns>
- private async Task<Images> FetchImages(
- BaseItem item,
- string language,
- CancellationToken cancellationToken)
- {
- var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
-
- if (string.IsNullOrEmpty(tmdbId))
+ if (series?.Images == null)
{
- return null;
+ return Enumerable.Empty<RemoteImageInfo>();
}
- await TmdbSeriesProvider.Current.EnsureSeriesInfo(tmdbId, language, cancellationToken).ConfigureAwait(false);
+ var remoteImages = new List<RemoteImageInfo>();
- var path = TmdbSeriesProvider.Current.GetDataFilePath(tmdbId, language);
+ for (var i = 0; i < series.Images.Posters.Count; i++)
+ {
+ var poster = series.Images.Posters[i];
+ remoteImages.Add(new RemoteImageInfo
+ {
+ 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(path) && File.Exists(path))
+ for (var i = 0; i < series.Images.Backdrops.Count; i++)
{
- return _jsonSerializer.DeserializeFromFile<SeriesResult>(path).Images;
+ var backdrop = series.Images.Backdrops[i];
+ remoteImages.Add(new RemoteImageInfo
+ {
+ 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 null;
+ return remoteImages.OrderByLanguageDescending(language);
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
index 287ebca8c..7944dfe27 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
@@ -3,107 +3,80 @@
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;
using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.Search;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.TV;
-using MediaBrowser.Providers.Plugins.Tmdb.Movies;
-using Microsoft.Extensions.Logging;
+using TMDbLib.Objects.Find;
+using TMDbLib.Objects.Search;
+using TMDbLib.Objects.TvShows;
namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
public class TmdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder
{
- private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}?api_key={1}&append_to_response=credits,images,keywords,external_ids,videos,content_ratings";
-
- private readonly IJsonSerializer _jsonSerializer;
- private readonly IServerConfigurationManager _configurationManager;
- private readonly ILogger<TmdbSeriesProvider> _logger;
private readonly IHttpClientFactory _httpClientFactory;
- private readonly ILibraryManager _libraryManager;
-
- private readonly CultureInfo _usCulture = new CultureInfo("en-US");
+ private readonly TmdbClientManager _tmdbClientManager;
public TmdbSeriesProvider(
- IJsonSerializer jsonSerializer,
- IServerConfigurationManager configurationManager,
- ILogger<TmdbSeriesProvider> logger,
IHttpClientFactory httpClientFactory,
- ILibraryManager libraryManager)
+ TmdbClientManager tmdbClientManager)
{
- _jsonSerializer = jsonSerializer;
- _configurationManager = configurationManager;
- _logger = logger;
_httpClientFactory = httpClientFactory;
- _libraryManager = libraryManager;
+ _tmdbClientManager = tmdbClientManager;
Current = this;
}
- internal static TmdbSeriesProvider Current { get; private set; }
-
public string Name => TmdbUtils.ProviderName;
// After TheTVDB
public int Order => 1;
+ internal static TmdbSeriesProvider Current { get; private set; }
+
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
{
var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb);
if (!string.IsNullOrEmpty(tmdbId))
{
- cancellationToken.ThrowIfCancellationRequested();
-
- await EnsureSeriesInfo(tmdbId, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
-
- var dataFilePath = GetDataFilePath(tmdbId, searchInfo.MetadataLanguage);
-
- var obj = _jsonSerializer.DeserializeFromFile<SeriesResult>(dataFilePath);
+ var series = await _tmdbClientManager
+ .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), searchInfo.MetadataLanguage, searchInfo.MetadataLanguage, cancellationToken)
+ .ConfigureAwait(false);
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
-
- var remoteResult = new RemoteSearchResult
+ if (series != null)
{
- Name = obj.Name,
- SearchProviderName = Name,
- ImageUrl = string.IsNullOrWhiteSpace(obj.Poster_Path) ? null : tmdbImageUrl + obj.Poster_Path
- };
-
- remoteResult.SetProviderId(MetadataProvider.Tmdb, obj.Id.ToString(_usCulture));
- remoteResult.SetProviderId(MetadataProvider.Imdb, obj.External_Ids.Imdb_Id);
+ var remoteResult = MapTvShowToRemoteSearchResult(series);
- if (obj.External_Ids != null && obj.External_Ids.Tvdb_Id > 0)
- {
- remoteResult.SetProviderId(MetadataProvider.Tvdb, obj.External_Ids.Tvdb_Id.Value.ToString(_usCulture));
+ return new[] { remoteResult };
}
-
- return new[] { remoteResult };
}
var imdbId = searchInfo.GetProviderId(MetadataProvider.Imdb);
if (!string.IsNullOrEmpty(imdbId))
{
- var searchResult = await FindByExternalId(imdbId, "imdb_id", cancellationToken).ConfigureAwait(false);
+ var findResult = await _tmdbClientManager
+ .FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, searchInfo.MetadataLanguage, cancellationToken)
+ .ConfigureAwait(false);
- if (searchResult != null)
+ if (findResult?.TvResults != null)
{
- return new[] { searchResult };
+ var imdbIdResults = new List<RemoteSearchResult>();
+ for (var i = 0; i < findResult.TvResults.Count; i++)
+ {
+ var remoteResult = MapSearchTvToRemoteSearchResult(findResult.TvResults[i]);
+ remoteResult.SetProviderId(MetadataProvider.Imdb, imdbId);
+ imdbIdResults.Add(remoteResult);
+ }
+
+ return imdbIdResults;
}
}
@@ -111,15 +84,79 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (!string.IsNullOrEmpty(tvdbId))
{
- var searchResult = await FindByExternalId(tvdbId, "tvdb_id", cancellationToken).ConfigureAwait(false);
+ var findResult = await _tmdbClientManager
+ .FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, searchInfo.MetadataLanguage, cancellationToken)
+ .ConfigureAwait(false);
+
+ if (findResult?.TvResults != null)
+ {
+ var tvIdResults = new List<RemoteSearchResult>();
+ for (var i = 0; i < findResult.TvResults.Count; i++)
+ {
+ var remoteResult = MapSearchTvToRemoteSearchResult(findResult.TvResults[i]);
+ remoteResult.SetProviderId(MetadataProvider.Tvdb, tvdbId);
+ tvIdResults.Add(remoteResult);
+ }
+
+ return tvIdResults;
+ }
+ }
+
+ var tvSearchResults = await _tmdbClientManager.SearchSeriesAsync(searchInfo.Name, searchInfo.MetadataLanguage, cancellationToken)
+ .ConfigureAwait(false);
+
+ var remoteResults = new List<RemoteSearchResult>();
+ for (var i = 0; i < tvSearchResults.Count; i++)
+ {
+ remoteResults.Add(MapSearchTvToRemoteSearchResult(tvSearchResults[i]));
+ }
+
+ return remoteResults;
+ }
+
+ private RemoteSearchResult MapTvShowToRemoteSearchResult(TvShow series)
+ {
+ var remoteResult = new RemoteSearchResult
+ {
+ Name = series.Name ?? series.OriginalName,
+ SearchProviderName = Name,
+ ImageUrl = _tmdbClientManager.GetPosterUrl(series.PosterPath),
+ Overview = series.Overview
+ };
+
+ remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(CultureInfo.InvariantCulture));
+ if (series.ExternalIds != null)
+ {
+ if (!string.IsNullOrEmpty(series.ExternalIds.ImdbId))
+ {
+ remoteResult.SetProviderId(MetadataProvider.Imdb, series.ExternalIds.ImdbId);
+ }
- if (searchResult != null)
+ if (!string.IsNullOrEmpty(series.ExternalIds.TvdbId))
{
- return new[] { searchResult };
+ remoteResult.SetProviderId(MetadataProvider.Tvdb, series.ExternalIds.TvdbId);
}
}
- return await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false);
+ remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime();
+
+ return remoteResult;
+ }
+
+ private RemoteSearchResult MapSearchTvToRemoteSearchResult(SearchTv series)
+ {
+ var remoteResult = new RemoteSearchResult
+ {
+ Name = series.Name ?? series.OriginalName,
+ SearchProviderName = Name,
+ ImageUrl = _tmdbClientManager.GetPosterUrl(series.PosterPath),
+ Overview = series.Overview
+ };
+
+ remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(CultureInfo.InvariantCulture));
+ remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime();
+
+ return remoteResult;
}
public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken)
@@ -137,11 +174,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (!string.IsNullOrEmpty(imdbId))
{
- var searchResult = await FindByExternalId(imdbId, "imdb_id", cancellationToken).ConfigureAwait(false);
+ var searchResult = await _tmdbClientManager.FindByExternalIdAsync(imdbId, FindExternalSource.Imdb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
if (searchResult != null)
{
- tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb);
+ tmdbId = searchResult.TvResults.FirstOrDefault()?.Id.ToString(CultureInfo.InvariantCulture);
}
}
}
@@ -152,11 +189,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (!string.IsNullOrEmpty(tvdbId))
{
- var searchResult = await FindByExternalId(tvdbId, "tvdb_id", cancellationToken).ConfigureAwait(false);
+ var searchResult = await _tmdbClientManager.FindByExternalIdAsync(tvdbId, FindExternalSource.TvDb, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
if (searchResult != null)
{
- tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb);
+ tmdbId = searchResult.TvResults.FirstOrDefault()?.Id.ToString(CultureInfo.InvariantCulture);
}
}
}
@@ -164,13 +201,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (string.IsNullOrEmpty(tmdbId))
{
result.QueriedById = false;
- var searchResults = await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetSearchResults(info, cancellationToken).ConfigureAwait(false);
-
- var searchResult = searchResults.FirstOrDefault();
+ var searchResults = await _tmdbClientManager.SearchSeriesAsync(info.Name, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
- if (searchResult != null)
+ if (searchResults.Count > 0)
{
- tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb);
+ tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture);
}
}
@@ -178,107 +213,91 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
{
cancellationToken.ThrowIfCancellationRequested();
- result = await FetchMovieData(tmdbId, info.MetadataLanguage, info.MetadataCountryCode, cancellationToken).ConfigureAwait(false);
-
- result.HasMetadata = result.Item != null;
- }
+ var tvShow = await _tmdbClientManager
+ .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
+ .ConfigureAwait(false);
- return result;
- }
+ result = new MetadataResult<Series>
+ {
+ Item = MapTvShowToSeries(tvShow, info.MetadataCountryCode),
+ ResultLanguage = info.MetadataLanguage ?? tvShow.OriginalLanguage
+ };
- private async Task<MetadataResult<Series>> FetchMovieData(string tmdbId, string language, string preferredCountryCode, CancellationToken cancellationToken)
- {
- SeriesResult seriesInfo = await FetchMainResult(tmdbId, language, cancellationToken).ConfigureAwait(false);
+ foreach (var person in GetPersons(tvShow))
+ {
+ result.AddPerson(person);
+ }
- if (seriesInfo == null)
- {
- return null;
+ result.HasMetadata = result.Item != null;
}
- tmdbId = seriesInfo.Id.ToString(_usCulture);
-
- string dataFilePath = GetDataFilePath(tmdbId, language);
- Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
- _jsonSerializer.SerializeToFile(seriesInfo, dataFilePath);
-
- await EnsureSeriesInfo(tmdbId, language, cancellationToken).ConfigureAwait(false);
-
- var result = new MetadataResult<Series>
- {
- Item = new Series(),
- ResultLanguage = seriesInfo.ResultLanguage
- };
-
- var settings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
-
- ProcessMainInfo(result, seriesInfo, preferredCountryCode, settings);
-
return result;
}
- private void ProcessMainInfo(MetadataResult<Series> seriesResult, SeriesResult seriesInfo, string preferredCountryCode, TmdbSettingsResult settings)
+ private Series MapTvShowToSeries(TvShow seriesResult, string preferredCountryCode)
{
- var series = seriesResult.Item;
+ var series = new Series {Name = seriesResult.Name, OriginalTitle = seriesResult.OriginalName};
+
+ series.SetProviderId(MetadataProvider.Tmdb, seriesResult.Id.ToString(CultureInfo.InvariantCulture));
- series.Name = seriesInfo.Name;
- series.OriginalTitle = seriesInfo.Original_Name;
- series.SetProviderId(MetadataProvider.Tmdb, seriesInfo.Id.ToString(_usCulture));
+ series.CommunityRating = Convert.ToSingle(seriesResult.VoteAverage);
- string voteAvg = seriesInfo.Vote_Average.ToString(CultureInfo.InvariantCulture);
+ series.Overview = seriesResult.Overview;
- if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out float rating))
+ if (seriesResult.Networks != null)
{
- series.CommunityRating = rating;
+ series.Studios = seriesResult.Networks.Select(i => i.Name).ToArray();
}
- series.Overview = seriesInfo.Overview;
-
- if (seriesInfo.Networks != null)
+ if (seriesResult.Genres != null)
{
- series.Studios = seriesInfo.Networks.Select(i => i.Name).ToArray();
+ series.Genres = seriesResult.Genres.Select(i => i.Name).ToArray();
}
- if (seriesInfo.Genres != null)
+ if (seriesResult.Keywords?.Results != null)
{
- series.Genres = seriesInfo.Genres.Select(i => i.Name).ToArray();
+ for (var i = 0; i < seriesResult.Keywords.Results.Count; i++)
+ {
+ series.AddTag(seriesResult.Keywords.Results[i].Name);
+ }
}
- series.HomePageUrl = seriesInfo.Homepage;
+ series.HomePageUrl = seriesResult.Homepage;
- series.RunTimeTicks = seriesInfo.Episode_Run_Time.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault();
+ series.RunTimeTicks = seriesResult.EpisodeRunTime.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault();
- if (string.Equals(seriesInfo.Status, "Ended", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(seriesResult.Status, "Ended", StringComparison.OrdinalIgnoreCase))
{
series.Status = SeriesStatus.Ended;
- series.EndDate = seriesInfo.Last_Air_Date;
+ series.EndDate = seriesResult.LastAirDate;
}
else
{
series.Status = SeriesStatus.Continuing;
}
- series.PremiereDate = seriesInfo.First_Air_Date;
+ series.PremiereDate = seriesResult.FirstAirDate;
- var ids = seriesInfo.External_Ids;
+ var ids = seriesResult.ExternalIds;
if (ids != null)
{
- if (!string.IsNullOrWhiteSpace(ids.Imdb_Id))
+ if (!string.IsNullOrWhiteSpace(ids.ImdbId))
{
- series.SetProviderId(MetadataProvider.Imdb, ids.Imdb_Id);
+ series.SetProviderId(MetadataProvider.Imdb, ids.ImdbId);
}
- if (ids.Tvrage_Id > 0)
+ if (!string.IsNullOrEmpty(ids.TvrageId))
{
- series.SetProviderId(MetadataProvider.TvRage, ids.Tvrage_Id.Value.ToString(_usCulture));
+ series.SetProviderId(MetadataProvider.TvRage, ids.TvrageId);
}
- if (ids.Tvdb_Id > 0)
+ if (!string.IsNullOrEmpty(ids.TvdbId))
{
- series.SetProviderId(MetadataProvider.Tvdb, ids.Tvdb_Id.Value.ToString(_usCulture));
+ series.SetProviderId(MetadataProvider.Tvdb, ids.TvdbId);
}
}
- var contentRatings = (seriesInfo.Content_Ratings ?? new ContentRatings()).Results ?? new List<ContentRating>();
+ var contentRatings = seriesResult.ContentRatings.Results ?? new List<ContentRating>();
var ourRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, preferredCountryCode, StringComparison.OrdinalIgnoreCase));
var usRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase));
@@ -297,254 +316,72 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
series.OfficialRating = minimumRelease.Rating;
}
- if (seriesInfo.Videos != null && seriesInfo.Videos.Results != null)
+ if (seriesResult.Videos?.Results != null)
{
- foreach (var video in seriesInfo.Videos.Results)
+ foreach (var video in seriesResult.Videos.Results)
{
- if ((video.Type.Equals("trailer", StringComparison.OrdinalIgnoreCase)
- || video.Type.Equals("clip", StringComparison.OrdinalIgnoreCase))
- && video.Site.Equals("youtube", StringComparison.OrdinalIgnoreCase))
+ if (TmdbUtils.IsTrailerType(video))
{
series.AddTrailerUrl($"http://www.youtube.com/watch?v={video.Key}");
}
}
}
- seriesResult.ResetPeople();
- var tmdbImageUrl = settings.images.GetImageUrl("original");
+ return series;
+ }
- if (seriesInfo.Credits != null)
+ private IEnumerable<PersonInfo> GetPersons(TvShow seriesResult)
+ {
+ if (seriesResult.Credits?.Cast != null)
{
- if (seriesInfo.Credits.Cast != null)
- {
- foreach (var actor in seriesInfo.Credits.Cast.OrderBy(a => a.Order))
- {
- var personInfo = new PersonInfo
- {
- Name = actor.Name.Trim(),
- Role = actor.Character,
- Type = PersonType.Actor,
- SortOrder = actor.Order
- };
-
- if (!string.IsNullOrWhiteSpace(actor.Profile_Path))
- {
- personInfo.ImageUrl = tmdbImageUrl + actor.Profile_Path;
- }
-
- if (actor.Id > 0)
- {
- personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
- }
-
- seriesResult.AddPerson(personInfo);
- }
- }
-
- if (seriesInfo.Credits.Crew != null)
+ foreach (var actor in seriesResult.Credits.Cast.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers))
{
- var keepTypes = new[]
+ var personInfo = new PersonInfo
{
- PersonType.Director,
- PersonType.Writer,
- PersonType.Producer
+ Name = actor.Name.Trim(),
+ Role = actor.Character,
+ Type = PersonType.Actor,
+ SortOrder = actor.Order,
+ ImageUrl = _tmdbClientManager.GetPosterUrl(actor.ProfilePath)
};
- foreach (var person in seriesInfo.Credits.Crew)
+ if (actor.Id > 0)
{
- // Normalize this
- var type = TmdbUtils.MapCrewToPersonType(person);
-
- if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
- && !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
- {
- continue;
- }
-
- seriesResult.AddPerson(new PersonInfo
- {
- Name = person.Name.Trim(),
- Role = person.Job,
- Type = type
- });
+ personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
}
- }
- }
- }
-
- internal static string GetSeriesDataPath(IApplicationPaths appPaths, string tmdbId)
- {
- var dataPath = GetSeriesDataPath(appPaths);
-
- return Path.Combine(dataPath, tmdbId);
- }
-
- internal static string GetSeriesDataPath(IApplicationPaths appPaths)
- {
- var dataPath = Path.Combine(appPaths.CachePath, "tmdb-tv");
-
- return dataPath;
- }
-
- internal async Task DownloadSeriesInfo(string id, string preferredMetadataLanguage, CancellationToken cancellationToken)
- {
- SeriesResult mainResult = await FetchMainResult(id, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
-
- if (mainResult == null)
- {
- return;
- }
-
- var dataFilePath = GetDataFilePath(id, preferredMetadataLanguage);
-
- Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
-
- _jsonSerializer.SerializeToFile(mainResult, dataFilePath);
- }
- internal async Task<SeriesResult> FetchMainResult(string id, string language, CancellationToken cancellationToken)
- {
- var url = string.Format(CultureInfo.InvariantCulture, GetTvInfo3, id, TmdbUtils.ApiKey);
-
- if (!string.IsNullOrEmpty(language))
- {
- url += "&language=" + TmdbMovieProvider.NormalizeLanguage(language)
- + "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language); // Get images in english and with no language
- }
-
- cancellationToken.ThrowIfCancellationRequested();
-
- using var mainRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);
- foreach (var header in TmdbUtils.AcceptHeaders)
- {
- mainRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
-
- using var mainResponse = await TmdbMovieProvider.Current.GetMovieDbResponse(mainRequestMessage, cancellationToken).ConfigureAwait(false);
- await using var mainStream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
- var mainResult = await _jsonSerializer.DeserializeFromStreamAsync<SeriesResult>(mainStream).ConfigureAwait(false);
-
- if (!string.IsNullOrEmpty(language))
- {
- mainResult.ResultLanguage = language;
- }
-
- cancellationToken.ThrowIfCancellationRequested();
-
- // If the language preference isn't english, then have the overview fallback to english if it's blank
- if (mainResult != null &&
- string.IsNullOrEmpty(mainResult.Overview) &&
- !string.IsNullOrEmpty(language) &&
- !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
- {
- _logger.LogInformation("MovieDbSeriesProvider couldn't find meta for language {Language}. Trying English...", language);
-
- url = string.Format(CultureInfo.InvariantCulture, GetTvInfo3, id, TmdbUtils.ApiKey) + "&language=en";
-
- if (!string.IsNullOrEmpty(language))
- {
- // Get images in english and with no language
- url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language);
+ yield return personInfo;
}
-
- using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
- foreach (var header in TmdbUtils.AcceptHeaders)
- {
- mainRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
-
- using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
- var englishResult = await _jsonSerializer.DeserializeFromStreamAsync<SeriesResult>(stream).ConfigureAwait(false);
-
- mainResult.Overview = englishResult.Overview;
- mainResult.ResultLanguage = "en";
- }
-
- return mainResult;
- }
-
- internal Task EnsureSeriesInfo(string tmdbId, string language, CancellationToken cancellationToken)
- {
- if (string.IsNullOrEmpty(tmdbId))
- {
- throw new ArgumentNullException(nameof(tmdbId));
}
- var path = GetDataFilePath(tmdbId, language);
-
- var fileInfo = new FileInfo(path);
- if (fileInfo.Exists)
+ if (seriesResult.Credits?.Crew != null)
{
- // If it's recent or automatic updates are enabled, don't re-download
- if ((DateTime.UtcNow - fileInfo.LastWriteTimeUtc).TotalDays <= 2)
+ var keepTypes = new[]
{
- return Task.CompletedTask;
- }
- }
-
- return DownloadSeriesInfo(tmdbId, language, cancellationToken);
- }
-
- internal string GetDataFilePath(string tmdbId, string preferredLanguage)
- {
- if (string.IsNullOrEmpty(tmdbId))
- {
- throw new ArgumentNullException(nameof(tmdbId));
- }
-
- var path = GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId);
-
- var filename = string.Format(CultureInfo.InvariantCulture, "series-{0}.json", preferredLanguage ?? string.Empty);
-
- return Path.Combine(path, filename);
- }
-
- private async Task<RemoteSearchResult> FindByExternalId(string id, string externalSource, CancellationToken cancellationToken)
- {
- var url = string.Format(
- CultureInfo.InvariantCulture,
- TmdbUtils.BaseTmdbApiUrl + @"3/find/{0}?api_key={1}&external_source={2}",
- id,
- TmdbUtils.ApiKey,
- externalSource);
-
- using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
- foreach (var header in TmdbUtils.AcceptHeaders)
- {
- requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
-
- using var response = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
-
- var result = await _jsonSerializer.DeserializeFromStreamAsync<ExternalIdLookupResult>(stream).ConfigureAwait(false);
-
- if (result != null && result.Tv_Results != null)
- {
- var tv = result.Tv_Results.FirstOrDefault();
+ PersonType.Director,
+ PersonType.Writer,
+ PersonType.Producer
+ };
- if (tv != null)
+ foreach (var person in seriesResult.Credits.Crew)
{
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
+ // Normalize this
+ var type = TmdbUtils.MapCrewToPersonType(person);
- var remoteResult = new RemoteSearchResult
+ if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
+ && !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
{
- Name = tv.Name,
- SearchProviderName = Name,
- ImageUrl = string.IsNullOrWhiteSpace(tv.Poster_Path)
- ? null
- : tmdbImageUrl + tv.Poster_Path
- };
-
- remoteResult.SetProviderId(MetadataProvider.Tmdb, tv.Id.ToString(_usCulture));
+ continue;
+ }
- return remoteResult;
+ yield return new PersonInfo
+ {
+ Name = person.Name.Trim(),
+ Role = person.Job,
+ Type = type
+ };
}
}
-
- return null;
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
new file mode 100644
index 000000000..2dc5cd55d
--- /dev/null
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
@@ -0,0 +1,469 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Caching.Memory;
+using TMDbLib.Client;
+using TMDbLib.Objects.Collections;
+using TMDbLib.Objects.Find;
+using TMDbLib.Objects.General;
+using TMDbLib.Objects.Movies;
+using TMDbLib.Objects.People;
+using TMDbLib.Objects.Search;
+using TMDbLib.Objects.TvShows;
+
+namespace MediaBrowser.Providers.Plugins.Tmdb
+{
+ /// <summary>
+ /// Manager class for abstracting the TMDb API client library.
+ /// </summary>
+ public class TmdbClientManager
+ {
+ private const int CacheDurationInHours = 1;
+
+ private readonly IMemoryCache _memoryCache;
+ private readonly TMDbClient _tmDbClient;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TmdbClientManager"/> class.
+ /// </summary>
+ /// <param name="memoryCache">An instance of <see cref="IMemoryCache"/>.</param>
+ public TmdbClientManager(IMemoryCache memoryCache)
+ {
+ _memoryCache = memoryCache;
+ _tmDbClient = new TMDbClient(TmdbUtils.ApiKey);
+ // Not really interested in NotFoundException
+ _tmDbClient.ThrowApiExceptions = false;
+ }
+
+ /// <summary>
+ /// Gets a movie from the TMDb API based on its TMDb id.
+ /// </summary>
+ /// <param name="tmdbId">The movie's TMDb id.</param>
+ /// <param name="language">The movie's language.</param>
+ /// <param name="imageLanguages">A comma-separated list of image languages.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb movie or null if not found.</returns>
+ public async Task<Movie> GetMovieAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken)
+ {
+ var key = $"movie-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
+ if (_memoryCache.TryGetValue(key, out Movie movie))
+ {
+ return movie;
+ }
+
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ movie = await _tmDbClient.GetMovieAsync(
+ tmdbId,
+ TmdbUtils.NormalizeLanguage(language),
+ imageLanguages,
+ MovieMethods.Credits | MovieMethods.Releases | MovieMethods.Images | MovieMethods.Keywords | MovieMethods.Videos,
+ cancellationToken).ConfigureAwait(false);
+
+ if (movie != null)
+ {
+ _memoryCache.Set(key, movie, TimeSpan.FromHours(CacheDurationInHours));
+ }
+
+ return movie;
+ }
+
+ /// <summary>
+ /// Gets a collection from the TMDb API based on its TMDb id.
+ /// </summary>
+ /// <param name="tmdbId">The collection's TMDb id.</param>
+ /// <param name="language">The collection's language.</param>
+ /// <param name="imageLanguages">A comma-separated list of image languages.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb collection or null if not found.</returns>
+ public async Task<Collection> GetCollectionAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken)
+ {
+ var key = $"collection-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
+ if (_memoryCache.TryGetValue(key, out Collection collection))
+ {
+ return collection;
+ }
+
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ collection = await _tmDbClient.GetCollectionAsync(
+ tmdbId,
+ TmdbUtils.NormalizeLanguage(language),
+ imageLanguages,
+ CollectionMethods.Images,
+ cancellationToken).ConfigureAwait(false);
+
+ if (collection != null)
+ {
+ _memoryCache.Set(key, collection, TimeSpan.FromHours(CacheDurationInHours));
+ }
+
+ return collection;
+ }
+
+ /// <summary>
+ /// Gets a tv show from the TMDb API based on its TMDb id.
+ /// </summary>
+ /// <param name="tmdbId">The tv show's TMDb id.</param>
+ /// <param name="language">The tv show's language.</param>
+ /// <param name="imageLanguages">A comma-separated list of image languages.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb tv show information or null if not found.</returns>
+ public async Task<TvShow> GetSeriesAsync(int tmdbId, string language, string imageLanguages, CancellationToken cancellationToken)
+ {
+ var key = $"series-{tmdbId.ToString(CultureInfo.InvariantCulture)}-{language}";
+ if (_memoryCache.TryGetValue(key, out TvShow series))
+ {
+ return series;
+ }
+
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ series = await _tmDbClient.GetTvShowAsync(
+ tmdbId,
+ language: TmdbUtils.NormalizeLanguage(language),
+ includeImageLanguage: imageLanguages,
+ extraMethods: TvShowMethods.Credits | TvShowMethods.Images | TvShowMethods.Keywords | TvShowMethods.ExternalIds | TvShowMethods.Videos | TvShowMethods.ContentRatings,
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+
+ if (series != null)
+ {
+ _memoryCache.Set(key, series, TimeSpan.FromHours(CacheDurationInHours));
+ }
+
+ return series;
+ }
+
+ /// <summary>
+ /// Gets a tv season from the TMDb API based on the tv show's TMDb id.
+ /// </summary>
+ /// <param name="tvShowId">The tv season's TMDb id.</param>
+ /// <param name="seasonNumber">The season number.</param>
+ /// <param name="language">The tv season's language.</param>
+ /// <param name="imageLanguages">A comma-separated list of image languages.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb tv season information or null if not found.</returns>
+ public async Task<TvSeason> GetSeasonAsync(int tvShowId, int seasonNumber, string language, string imageLanguages, CancellationToken cancellationToken)
+ {
+ var key = $"season-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}-{language}";
+ if (_memoryCache.TryGetValue(key, out TvSeason season))
+ {
+ return season;
+ }
+
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ season = await _tmDbClient.GetTvSeasonAsync(
+ tvShowId,
+ seasonNumber,
+ language: TmdbUtils.NormalizeLanguage(language),
+ includeImageLanguage: imageLanguages,
+ extraMethods: TvSeasonMethods.Credits | TvSeasonMethods.Images | TvSeasonMethods.ExternalIds | TvSeasonMethods.Videos,
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+
+ if (season != null)
+ {
+ _memoryCache.Set(key, season, TimeSpan.FromHours(CacheDurationInHours));
+ }
+
+ return season;
+ }
+
+ /// <summary>
+ /// Gets a movie from the TMDb API based on the tv show's TMDb id.
+ /// </summary>
+ /// <param name="tvShowId">The tv show's TMDb id.</param>
+ /// <param name="seasonNumber">The season number.</param>
+ /// <param name="episodeNumber">The episode number.</param>
+ /// <param name="language">The episode's language.</param>
+ /// <param name="imageLanguages">A comma-separated list of image languages.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb tv episode information or null if not found.</returns>
+ public async Task<TvEpisode> GetEpisodeAsync(int tvShowId, int seasonNumber, int episodeNumber, string language, string imageLanguages, CancellationToken cancellationToken)
+ {
+ var key = $"episode-{tvShowId.ToString(CultureInfo.InvariantCulture)}-s{seasonNumber.ToString(CultureInfo.InvariantCulture)}e{episodeNumber.ToString(CultureInfo.InvariantCulture)}-{language}";
+ if (_memoryCache.TryGetValue(key, out TvEpisode episode))
+ {
+ return episode;
+ }
+
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ episode = await _tmDbClient.GetTvEpisodeAsync(
+ tvShowId,
+ seasonNumber,
+ episodeNumber,
+ language: TmdbUtils.NormalizeLanguage(language),
+ includeImageLanguage: imageLanguages,
+ extraMethods: TvEpisodeMethods.Credits | TvEpisodeMethods.Images | TvEpisodeMethods.ExternalIds | TvEpisodeMethods.Videos,
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+
+ if (episode != null)
+ {
+ _memoryCache.Set(key, episode, TimeSpan.FromHours(CacheDurationInHours));
+ }
+
+ return episode;
+ }
+
+ /// <summary>
+ /// Gets a person eg. cast or crew member from the TMDb API based on its TMDb id.
+ /// </summary>
+ /// <param name="personTmdbId">The person's TMDb id.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb person information or null if not found.</returns>
+ public async Task<Person> GetPersonAsync(int personTmdbId, CancellationToken cancellationToken)
+ {
+ var key = $"person-{personTmdbId.ToString(CultureInfo.InvariantCulture)}";
+ if (_memoryCache.TryGetValue(key, out Person person))
+ {
+ return person;
+ }
+
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ person = await _tmDbClient.GetPersonAsync(
+ personTmdbId,
+ PersonMethods.TvCredits | PersonMethods.MovieCredits | PersonMethods.Images | PersonMethods.ExternalIds,
+ cancellationToken).ConfigureAwait(false);
+
+ if (person != null)
+ {
+ _memoryCache.Set(key, person, TimeSpan.FromHours(CacheDurationInHours));
+ }
+
+ return person;
+ }
+
+ /// <summary>
+ /// Gets an item from the TMDb API based on its id from an external service eg. IMDb id, TvDb id.
+ /// </summary>
+ /// <param name="externalId">The item's external id.</param>
+ /// <param name="source">The source of the id eg. IMDb.</param>
+ /// <param name="language">The item's language.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb item or null if not found.</returns>
+ public async Task<FindContainer> FindByExternalIdAsync(
+ string externalId,
+ FindExternalSource source,
+ string language,
+ CancellationToken cancellationToken)
+ {
+ var key = $"find-{source.ToString()}-{externalId.ToString(CultureInfo.InvariantCulture)}-{language}";
+ if (_memoryCache.TryGetValue(key, out FindContainer result))
+ {
+ return result;
+ }
+
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ result = await _tmDbClient.FindAsync(
+ source,
+ externalId,
+ TmdbUtils.NormalizeLanguage(language),
+ cancellationToken).ConfigureAwait(false);
+
+ if (result != null)
+ {
+ _memoryCache.Set(key, result, TimeSpan.FromHours(CacheDurationInHours));
+ }
+
+ return result;
+ }
+
+ /// <summary>
+ /// Searches for a tv show using the TMDb API based on its name.
+ /// </summary>
+ /// <param name="name">The name of the tv show.</param>
+ /// <param name="language">The tv show's language.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb tv show information.</returns>
+ public async Task<IReadOnlyList<SearchTv>> SearchSeriesAsync(string name, string language, CancellationToken cancellationToken)
+ {
+ var key = $"searchseries-{name}-{language}";
+ if (_memoryCache.TryGetValue(key, out SearchContainer<SearchTv> series))
+ {
+ return series.Results;
+ }
+
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ var searchResults = await _tmDbClient
+ .SearchTvShowAsync(name, TmdbUtils.NormalizeLanguage(language), cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
+
+ if (searchResults.Results.Count > 0)
+ {
+ _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
+ }
+
+ return searchResults.Results;
+ }
+
+ /// <summary>
+ /// Searches for a person based on their name using the TMDb API.
+ /// </summary>
+ /// <param name="name">The name of the person.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb person information.</returns>
+ public async Task<IReadOnlyList<SearchPerson>> SearchPersonAsync(string name, CancellationToken cancellationToken)
+ {
+ var key = $"searchperson-{name}";
+ if (_memoryCache.TryGetValue(key, out SearchContainer<SearchPerson> person))
+ {
+ return person.Results;
+ }
+
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ var searchResults = await _tmDbClient
+ .SearchPersonAsync(name, cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
+
+ if (searchResults.Results.Count > 0)
+ {
+ _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
+ }
+
+ return searchResults.Results;
+ }
+
+ /// <summary>
+ /// Searches for a movie based on its name using the TMDb API.
+ /// </summary>
+ /// <param name="name">The name of the movie.</param>
+ /// <param name="language">The movie's language.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb movie information.</returns>
+ public Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, string language, CancellationToken cancellationToken)
+ {
+ return SearchMovieAsync(name, 0, language, cancellationToken);
+ }
+
+ /// <summary>
+ /// Searches for a movie based on its name using the TMDb API.
+ /// </summary>
+ /// <param name="name">The name of the movie.</param>
+ /// <param name="year">The release year of the movie.</param>
+ /// <param name="language">The movie's language.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb movie information.</returns>
+ public async Task<IReadOnlyList<SearchMovie>> SearchMovieAsync(string name, int year, string language, CancellationToken cancellationToken)
+ {
+ var key = $"moviesearch-{name}-{year.ToString(CultureInfo.InvariantCulture)}-{language}";
+ if (_memoryCache.TryGetValue(key, out SearchContainer<SearchMovie> movies))
+ {
+ return movies.Results;
+ }
+
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ var searchResults = await _tmDbClient
+ .SearchMovieAsync(name, TmdbUtils.NormalizeLanguage(language), year: year, cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
+
+ if (searchResults.Results.Count > 0)
+ {
+ _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
+ }
+
+ return searchResults.Results;
+ }
+
+ /// <summary>
+ /// Searches for a collection based on its name using the TMDb API.
+ /// </summary>
+ /// <param name="name">The name of the collection.</param>
+ /// <param name="language">The collection's language.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The TMDb collection information.</returns>
+ public async Task<IReadOnlyList<SearchCollection>> SearchCollectionAsync(string name, string language, CancellationToken cancellationToken)
+ {
+ var key = $"collectionsearch-{name}-{language}";
+ if (_memoryCache.TryGetValue(key, out SearchContainer<SearchCollection> collections))
+ {
+ return collections.Results;
+ }
+
+ await EnsureClientConfigAsync().ConfigureAwait(false);
+
+ var searchResults = await _tmDbClient
+ .SearchCollectionAsync(name, TmdbUtils.NormalizeLanguage(language), cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
+
+ if (searchResults.Results.Count > 0)
+ {
+ _memoryCache.Set(key, searchResults, TimeSpan.FromHours(CacheDurationInHours));
+ }
+
+ return searchResults.Results;
+ }
+
+ /// <summary>
+ /// Gets the absolute URL of the poster.
+ /// </summary>
+ /// <param name="posterPath">The relative URL of the poster.</param>
+ /// <returns>The absolute URL.</returns>
+ public string GetPosterUrl(string posterPath)
+ {
+ if (string.IsNullOrEmpty(posterPath))
+ {
+ return null;
+ }
+
+ return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.PosterSizes[^1], posterPath).ToString();
+ }
+
+ /// <summary>
+ /// Gets the absolute URL of the backdrop image.
+ /// </summary>
+ /// <param name="posterPath">The relative URL of the backdrop image.</param>
+ /// <returns>The absolute URL.</returns>
+ public string GetBackdropUrl(string posterPath)
+ {
+ if (string.IsNullOrEmpty(posterPath))
+ {
+ return null;
+ }
+
+ return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.BackdropSizes[^1], posterPath).ToString();
+ }
+
+ /// <summary>
+ /// Gets the absolute URL of the profile image.
+ /// </summary>
+ /// <param name="actorProfilePath">The relative URL of the profile image.</param>
+ /// <returns>The absolute URL.</returns>
+ public string GetProfileUrl(string actorProfilePath)
+ {
+ if (string.IsNullOrEmpty(actorProfilePath))
+ {
+ return null;
+ }
+
+ return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.ProfileSizes[^1], actorProfilePath).ToString();
+ }
+
+ /// <summary>
+ /// Gets the absolute URL of the still image.
+ /// </summary>
+ /// <param name="filePath">The relative URL of the still image.</param>
+ /// <returns>The absolute URL.</returns>
+ public string GetStillUrl(string filePath)
+ {
+ if (string.IsNullOrEmpty(filePath))
+ {
+ return null;
+ }
+
+ return _tmDbClient.GetImageUrl(_tmDbClient.Config.Images.StillSizes[^1], filePath).ToString();
+ }
+
+ private Task EnsureClientConfigAsync()
+ {
+ return !_tmDbClient.HasConfig ? _tmDbClient.GetConfigAsync() : Task.CompletedTask;
+ }
+ }
+}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
index 1415d6976..b754a0795 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
@@ -1,7 +1,9 @@
+#nullable enable
+
using System;
-using System.Net.Mime;
+using System.Collections.Generic;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Providers.Plugins.Tmdb.Models.General;
+using TMDbLib.Objects.General;
namespace MediaBrowser.Providers.Plugins.Tmdb
{
@@ -16,11 +18,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
public const string BaseTmdbUrl = "https://www.themoviedb.org/";
/// <summary>
- /// URL of the TMDB API instance to use.
- /// </summary>
- public const string BaseTmdbApiUrl = "https://api.themoviedb.org/";
-
- /// <summary>
/// Name of the provider.
/// </summary>
public const string ProviderName = "TheMovieDb";
@@ -31,9 +28,19 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
public const string ApiKey = "4219e299c89411838049ab0dab19ebd5";
/// <summary>
- /// Value of the Accept header for requests to the provider.
+ /// Maximum number of cast members to pull.
+ /// </summary>
+ public const int MaxCastMembers = 15;
+
+ /// <summary>
+ /// The crew types to keep.
/// </summary>
- public static readonly string[] AcceptHeaders = { MediaTypeNames.Application.Json, "image/*" };
+ public static readonly string[] WantedCrewTypes =
+ {
+ PersonType.Director,
+ PersonType.Writer,
+ PersonType.Producer
+ };
/// <summary>
/// Maps the TMDB provided roles for crew members to Jellyfin roles.
@@ -59,7 +66,98 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
return PersonType.Writer;
}
- return null;
+ return string.Empty;
+ }
+
+ /// <summary>
+ /// Determines whether a video is a trailer.
+ /// </summary>
+ /// <param name="video">The TMDb video.</param>
+ /// <returns>A boolean indicating whether the video is a trailer.</returns>
+ public static bool IsTrailerType(Video video)
+ {
+ return video.Site.Equals("youtube", StringComparison.OrdinalIgnoreCase)
+ && (!video.Type.Equals("trailer", StringComparison.OrdinalIgnoreCase)
+ || !video.Type.Equals("teaser", StringComparison.OrdinalIgnoreCase));
+ }
+
+ /// <summary>
+ /// Normalizes a language string for use with TMDb's include image language parameter.
+ /// </summary>
+ /// <param name="preferredLanguage">The preferred language as either a 2 letter code with or without country code.</param>
+ /// <returns>The comma separated language string.</returns>
+ public static string GetImageLanguagesParam(string preferredLanguage)
+ {
+ var languages = new List<string>();
+
+ if (!string.IsNullOrEmpty(preferredLanguage))
+ {
+ preferredLanguage = NormalizeLanguage(preferredLanguage);
+
+ languages.Add(preferredLanguage);
+
+ if (preferredLanguage.Length == 5) // like en-US
+ {
+ // Currenty, TMDB supports 2-letter language codes only
+ // They are planning to change this in the future, thus we're
+ // supplying both codes if we're having a 5-letter code.
+ languages.Add(preferredLanguage.Substring(0, 2));
+ }
+ }
+
+ languages.Add("null");
+
+ if (!string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase))
+ {
+ languages.Add("en");
+ }
+
+ return string.Join(',', languages);
+ }
+
+ /// <summary>
+ /// Normalizes a language string for use with TMDb's language parameter.
+ /// </summary>
+ /// <param name="language">The language code.</param>
+ /// <returns>The normalized language code.</returns>
+ public static string NormalizeLanguage(string language)
+ {
+ if (string.IsNullOrEmpty(language))
+ {
+ return language;
+ }
+
+ // They require this to be uppercase
+ // Everything after the hyphen must be written in uppercase due to a way TMDB wrote their api.
+ // See here: https://www.themoviedb.org/talk/5119221d760ee36c642af4ad?page=3#56e372a0c3a3685a9e0019ab
+ var parts = language.Split('-');
+
+ if (parts.Length == 2)
+ {
+ language = parts[0] + "-" + parts[1].ToUpperInvariant();
+ }
+
+ return language;
+ }
+
+ /// <summary>
+ /// Adjusts the image's language code preferring the 5 letter language code eg. en-US.
+ /// </summary>
+ /// <param name="imageLanguage">The image's actual language code.</param>
+ /// <param name="requestLanguage">The requested language code.</param>
+ /// <returns>The language code.</returns>
+ public static string AdjustImageLanguage(string imageLanguage, string requestLanguage)
+ {
+ if (!string.IsNullOrEmpty(imageLanguage)
+ && !string.IsNullOrEmpty(requestLanguage)
+ && requestLanguage.Length > 2
+ && imageLanguage.Length == 2
+ && requestLanguage.StartsWith(imageLanguage, StringComparison.OrdinalIgnoreCase))
+ {
+ return requestLanguage;
+ }
+
+ return imageLanguage;
}
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs
deleted file mode 100644
index 613dc17e3..000000000
--- a/MediaBrowser.Providers/Plugins/Tmdb/Trailers/TmdbTrailerProvider.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Providers;
-using MediaBrowser.Providers.Plugins.Tmdb.Movies;
-
-namespace MediaBrowser.Providers.Plugins.Tmdb.Trailers
-{
- public class TmdbTrailerProvider : IHasOrder, IRemoteMetadataProvider<Trailer, TrailerInfo>
- {
- private readonly IHttpClientFactory _httpClientFactory;
-
- public TmdbTrailerProvider(IHttpClientFactory httpClientFactory)
- {
- _httpClientFactory = httpClientFactory;
- }
-
- public string Name => TmdbMovieProvider.Current.Name;
-
- public int Order => 0;
-
- public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(TrailerInfo searchInfo, CancellationToken cancellationToken)
- {
- return TmdbMovieProvider.Current.GetMovieSearchResults(searchInfo, cancellationToken);
- }
-
- public Task<MetadataResult<Trailer>> GetMetadata(TrailerInfo info, CancellationToken cancellationToken)
- {
- return TmdbMovieProvider.Current.GetItemMetadata<Trailer>(info, cancellationToken);
- }
-
- public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
- {
- return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
- }
- }
-}
diff --git a/MediaBrowser.Providers/TV/DummySeasonProvider.cs b/MediaBrowser.Providers/TV/DummySeasonProvider.cs
deleted file mode 100644
index 905cbefd3..000000000
--- a/MediaBrowser.Providers/TV/DummySeasonProvider.cs
+++ /dev/null
@@ -1,229 +0,0 @@
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Providers.TV
-{
- public class DummySeasonProvider
- {
- private readonly ILogger _logger;
- private readonly ILocalizationManager _localization;
- private readonly ILibraryManager _libraryManager;
- private readonly IFileSystem _fileSystem;
-
- public DummySeasonProvider(
- ILogger logger,
- ILocalizationManager localization,
- ILibraryManager libraryManager,
- IFileSystem fileSystem)
- {
- _logger = logger;
- _localization = localization;
- _libraryManager = libraryManager;
- _fileSystem = fileSystem;
- }
-
- public async Task<bool> Run(Series series, CancellationToken cancellationToken)
- {
- var seasonsRemoved = RemoveObsoleteSeasons(series);
-
- var hasNewSeasons = await AddDummySeasonFolders(series, cancellationToken).ConfigureAwait(false);
-
- if (hasNewSeasons)
- {
- // var directoryService = new DirectoryService(_fileSystem);
-
- // await series.RefreshMetadata(new MetadataRefreshOptions(directoryService), cancellationToken).ConfigureAwait(false);
-
- // await series.ValidateChildren(new SimpleProgress<double>(), cancellationToken, new MetadataRefreshOptions(directoryService))
- // .ConfigureAwait(false);
- }
-
- return seasonsRemoved || hasNewSeasons;
- }
-
- private async Task<bool> AddDummySeasonFolders(Series series, CancellationToken cancellationToken)
- {
- var episodesInSeriesFolder = series.GetRecursiveChildren(i => i is Episode)
- .Cast<Episode>()
- .Where(i => !i.IsInSeasonFolder)
- .ToList();
-
- var hasChanges = false;
-
- List<Season> seasons = null;
-
- // Loop through the unique season numbers
- foreach (var seasonNumber in episodesInSeriesFolder.Select(i => i.ParentIndexNumber ?? -1)
- .Where(i => i >= 0)
- .Distinct()
- .ToList())
- {
- if (seasons == null)
- {
- seasons = series.Children.OfType<Season>().ToList();
- }
-
- var existingSeason = seasons
- .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber);
-
- if (existingSeason == null)
- {
- await AddSeason(series, seasonNumber, false, cancellationToken).ConfigureAwait(false);
- hasChanges = true;
- seasons = null;
- }
- else if (existingSeason.IsVirtualItem)
- {
- existingSeason.IsVirtualItem = false;
- await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
- seasons = null;
- }
- }
-
- // Unknown season - create a dummy season to put these under
- if (episodesInSeriesFolder.Any(i => !i.ParentIndexNumber.HasValue))
- {
- if (seasons == null)
- {
- seasons = series.Children.OfType<Season>().ToList();
- }
-
- var existingSeason = seasons
- .FirstOrDefault(i => !i.IndexNumber.HasValue);
-
- if (existingSeason == null)
- {
- await AddSeason(series, null, false, cancellationToken).ConfigureAwait(false);
-
- hasChanges = true;
- seasons = null;
- }
- else if (existingSeason.IsVirtualItem)
- {
- existingSeason.IsVirtualItem = false;
- await existingSeason.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
- seasons = null;
- }
- }
-
- return hasChanges;
- }
-
- /// <summary>
- /// Adds the season.
- /// </summary>
- public async Task<Season> AddSeason(
- Series series,
- int? seasonNumber,
- bool isVirtualItem,
- CancellationToken cancellationToken)
- {
- string seasonName;
- if (seasonNumber == null)
- {
- seasonName = _localization.GetLocalizedString("NameSeasonUnknown");
- }
- else if (seasonNumber == 0)
- {
- seasonName = _libraryManager.GetLibraryOptions(series).SeasonZeroDisplayName;
- }
- else
- {
- seasonName = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("NameSeasonNumber"),
- seasonNumber.Value);
- }
-
- _logger.LogInformation("Creating Season {0} entry for {1}", seasonName, series.Name);
-
- var season = new Season
- {
- Name = seasonName,
- IndexNumber = seasonNumber,
- Id = _libraryManager.GetNewItemId(
- series.Id + (seasonNumber ?? -1).ToString(CultureInfo.InvariantCulture) + seasonName,
- typeof(Season)),
- IsVirtualItem = isVirtualItem,
- SeriesId = series.Id,
- SeriesName = series.Name
- };
-
- season.SetParent(series);
-
- series.AddChild(season, cancellationToken);
-
- await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false);
-
- return season;
- }
-
- private bool RemoveObsoleteSeasons(Series series)
- {
- var existingSeasons = series.Children.OfType<Season>().ToList();
-
- var physicalSeasons = existingSeasons
- .Where(i => i.LocationType != LocationType.Virtual)
- .ToList();
-
- var virtualSeasons = existingSeasons
- .Where(i => i.LocationType == LocationType.Virtual)
- .ToList();
-
- var seasonsToRemove = virtualSeasons
- .Where(i =>
- {
- if (i.IndexNumber.HasValue)
- {
- var seasonNumber = i.IndexNumber.Value;
-
- // If there's a physical season with the same number, delete it
- if (physicalSeasons.Any(p => p.IndexNumber.HasValue && (p.IndexNumber.Value == seasonNumber)))
- {
- return true;
- }
- }
-
- // If there are no episodes with this season number, delete it
- if (!i.GetEpisodes().Any())
- {
- return true;
- }
-
- return false;
- })
- .ToList();
-
- var hasChanges = false;
-
- foreach (var seasonToRemove in seasonsToRemove)
- {
- _logger.LogInformation("Removing virtual season {0} {1}", series.Name, seasonToRemove.IndexNumber);
-
- _libraryManager.DeleteItem(
- seasonToRemove,
- new DeleteOptions
- {
- DeleteFileLocation = true
- },
- false);
-
- hasChanges = true;
- }
-
- return hasChanges;
- }
- }
-}
diff --git a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs b/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs
deleted file mode 100644
index c833b1227..000000000
--- a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs
+++ /dev/null
@@ -1,404 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.TV;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Providers.Plugins.TheTvdb;
-using Microsoft.Extensions.Logging;
-
-namespace MediaBrowser.Providers.TV
-{
- public class MissingEpisodeProvider
- {
- private const double UnairedEpisodeThresholdDays = 2;
-
- private readonly IServerConfigurationManager _config;
- private readonly ILogger _logger;
- private readonly ILibraryManager _libraryManager;
- private readonly ILocalizationManager _localization;
- private readonly IFileSystem _fileSystem;
- private readonly TvdbClientManager _tvdbClientManager;
-
- public MissingEpisodeProvider(
- ILogger logger,
- IServerConfigurationManager config,
- ILibraryManager libraryManager,
- ILocalizationManager localization,
- IFileSystem fileSystem,
- TvdbClientManager tvdbClientManager)
- {
- _logger = logger;
- _config = config;
- _libraryManager = libraryManager;
- _localization = localization;
- _fileSystem = fileSystem;
- _tvdbClientManager = tvdbClientManager;
- }
-
- public async Task<bool> Run(Series series, bool addNewItems, CancellationToken cancellationToken)
- {
- var tvdbIdString = series.GetProviderId(MetadataProvider.Tvdb);
- if (string.IsNullOrEmpty(tvdbIdString))
- {
- return false;
- }
-
- var episodes = await _tvdbClientManager.GetAllEpisodesAsync(
- int.Parse(tvdbIdString, CultureInfo.InvariantCulture),
- series.GetPreferredMetadataLanguage(),
- cancellationToken).ConfigureAwait(false);
-
- var episodeLookup = episodes
- .Select(i =>
- {
- if (!DateTime.TryParse(i.FirstAired, out var firstAired))
- {
- firstAired = default;
- }
-
- var seasonNumber = i.AiredSeason.GetValueOrDefault(-1);
- var episodeNumber = i.AiredEpisodeNumber.GetValueOrDefault(-1);
- return (seasonNumber, episodeNumber, firstAired);
- })
- .Where(i => i.seasonNumber != -1 && i.episodeNumber != -1)
- .OrderBy(i => i.seasonNumber)
- .ThenBy(i => i.episodeNumber)
- .ToList();
-
- var allRecursiveChildren = series.GetRecursiveChildren();
-
- var hasBadData = HasInvalidContent(allRecursiveChildren);
-
- // Be conservative here to avoid creating missing episodes for ones they already have
- var addMissingEpisodes = !hasBadData && _libraryManager.GetLibraryOptions(series).ImportMissingEpisodes;
-
- var anySeasonsRemoved = RemoveObsoleteOrMissingSeasons(allRecursiveChildren, episodeLookup);
-
- if (anySeasonsRemoved)
- {
- // refresh this
- allRecursiveChildren = series.GetRecursiveChildren();
- }
-
- var anyEpisodesRemoved = RemoveObsoleteOrMissingEpisodes(allRecursiveChildren, episodeLookup, addMissingEpisodes);
-
- if (anyEpisodesRemoved)
- {
- // refresh this
- allRecursiveChildren = series.GetRecursiveChildren();
- }
-
- var hasNewEpisodes = false;
-
- if (addNewItems && series.IsMetadataFetcherEnabled(_libraryManager.GetLibraryOptions(series), TvdbSeriesProvider.Current.Name))
- {
- hasNewEpisodes = await AddMissingEpisodes(series, allRecursiveChildren, addMissingEpisodes, episodeLookup, cancellationToken)
- .ConfigureAwait(false);
- }
-
- if (hasNewEpisodes || anySeasonsRemoved || anyEpisodesRemoved)
- {
- return true;
- }
-
- return false;
- }
-
- /// <summary>
- /// Returns true if a series has any seasons or episodes without season or episode numbers
- /// If this data is missing no virtual items will be added in order to prevent possible duplicates.
- /// </summary>
- private bool HasInvalidContent(IList<BaseItem> allItems)
- {
- return allItems.OfType<Season>().Any(i => !i.IndexNumber.HasValue) ||
- allItems.OfType<Episode>().Any(i =>
- {
- if (!i.ParentIndexNumber.HasValue)
- {
- return true;
- }
-
- // You could have episodes under season 0 with no number
- return false;
- });
- }
-
- private async Task<bool> AddMissingEpisodes(
- Series series,
- IEnumerable<BaseItem> allItems,
- bool addMissingEpisodes,
- IReadOnlyCollection<(int seasonNumber, int episodenumber, DateTime firstAired)> episodeLookup,
- CancellationToken cancellationToken)
- {
- var existingEpisodes = allItems.OfType<Episode>().ToList();
-
- var seasonCounts = episodeLookup.GroupBy(e => e.seasonNumber).ToDictionary(g => g.Key, g => g.Count());
-
- var hasChanges = false;
-
- foreach (var tuple in episodeLookup)
- {
- if (tuple.seasonNumber <= 0 || tuple.episodenumber <= 0)
- {
- // Ignore episode/season zeros
- continue;
- }
-
- var existingEpisode = GetExistingEpisode(existingEpisodes, seasonCounts, tuple);
-
- if (existingEpisode != null)
- {
- continue;
- }
-
- var airDate = tuple.firstAired;
-
- var now = DateTime.UtcNow.AddDays(-UnairedEpisodeThresholdDays);
-
- if ((airDate < now && addMissingEpisodes) || airDate > now)
- {
- // tvdb has a lot of nearly blank episodes
- _logger.LogInformation("Creating virtual missing/unaired episode {0} {1}x{2}", series.Name, tuple.seasonNumber, tuple.episodenumber);
- await AddEpisode(series, tuple.seasonNumber, tuple.episodenumber, cancellationToken).ConfigureAwait(false);
-
- hasChanges = true;
- }
- }
-
- return hasChanges;
- }
-
- /// <summary>
- /// Removes the virtual entry after a corresponding physical version has been added.
- /// </summary>
- private bool RemoveObsoleteOrMissingEpisodes(
- IEnumerable<BaseItem> allRecursiveChildren,
- IEnumerable<(int seasonNumber, int episodeNumber, DateTime firstAired)> episodeLookup,
- bool allowMissingEpisodes)
- {
- var existingEpisodes = allRecursiveChildren.OfType<Episode>();
-
- var physicalEpisodes = new List<Episode>();
- var virtualEpisodes = new List<Episode>();
- foreach (var episode in existingEpisodes)
- {
- if (episode.LocationType == LocationType.Virtual)
- {
- virtualEpisodes.Add(episode);
- }
- else
- {
- physicalEpisodes.Add(episode);
- }
- }
-
- var episodesToRemove = virtualEpisodes
- .Where(i =>
- {
- if (!i.IndexNumber.HasValue || !i.ParentIndexNumber.HasValue)
- {
- return true;
- }
-
- var seasonNumber = i.ParentIndexNumber.Value;
- var episodeNumber = i.IndexNumber.Value;
-
- // If there's a physical episode with the same season and episode number, delete it
- if (physicalEpisodes.Any(p =>
- p.ParentIndexNumber.HasValue && p.ParentIndexNumber.Value == seasonNumber &&
- p.ContainsEpisodeNumber(episodeNumber)))
- {
- return true;
- }
-
- // If the episode no longer exists in the remote lookup, delete it
- if (!episodeLookup.Any(e => e.seasonNumber == seasonNumber && e.episodeNumber == episodeNumber))
- {
- return true;
- }
-
- // If it's missing, but not unaired, remove it
- return !allowMissingEpisodes && i.IsMissingEpisode &&
- (!i.PremiereDate.HasValue ||
- i.PremiereDate.Value.ToLocalTime().Date.AddDays(UnairedEpisodeThresholdDays) <
- DateTime.Now.Date);
- });
-
- var hasChanges = false;
-
- foreach (var episodeToRemove in episodesToRemove)
- {
- _libraryManager.DeleteItem(
- episodeToRemove,
- new DeleteOptions
- {
- DeleteFileLocation = true
- },
- false);
-
- hasChanges = true;
- }
-
- return hasChanges;
- }
-
- /// <summary>
- /// Removes the obsolete or missing seasons.
- /// </summary>
- /// <param name="allRecursiveChildren">All recursive children.</param>
- /// <param name="episodeLookup">The episode lookup.</param>
- /// <returns><see cref="bool" />.</returns>
- private bool RemoveObsoleteOrMissingSeasons(
- IList<BaseItem> allRecursiveChildren,
- IEnumerable<(int seasonNumber, int episodeNumber, DateTime firstAired)> episodeLookup)
- {
- var existingSeasons = allRecursiveChildren.OfType<Season>().ToList();
-
- var physicalSeasons = new List<Season>();
- var virtualSeasons = new List<Season>();
- foreach (var season in existingSeasons)
- {
- if (season.LocationType == LocationType.Virtual)
- {
- virtualSeasons.Add(season);
- }
- else
- {
- physicalSeasons.Add(season);
- }
- }
-
- var allEpisodes = allRecursiveChildren.OfType<Episode>().ToList();
-
- var seasonsToRemove = virtualSeasons
- .Where(i =>
- {
- if (i.IndexNumber.HasValue)
- {
- var seasonNumber = i.IndexNumber.Value;
-
- // If there's a physical season with the same number, delete it
- if (physicalSeasons.Any(p => p.IndexNumber.HasValue && p.IndexNumber.Value == seasonNumber && string.Equals(p.Series.PresentationUniqueKey, i.Series.PresentationUniqueKey, StringComparison.Ordinal)))
- {
- return true;
- }
-
- // If the season no longer exists in the remote lookup, delete it, but only if an existing episode doesn't require it
- return episodeLookup.All(e => e.seasonNumber != seasonNumber) && allEpisodes.All(s => s.ParentIndexNumber != seasonNumber || s.IsInSeasonFolder);
- }
-
- // Season does not have a number
- // Remove if there are no episodes directly in series without a season number
- return allEpisodes.All(s => s.ParentIndexNumber.HasValue || s.IsInSeasonFolder);
- });
-
- var hasChanges = false;
-
- foreach (var seasonToRemove in seasonsToRemove)
- {
- _libraryManager.DeleteItem(
- seasonToRemove,
- new DeleteOptions
- {
- DeleteFileLocation = true
- },
- false);
-
- hasChanges = true;
- }
-
- return hasChanges;
- }
-
- /// <summary>
- /// Adds the episode.
- /// </summary>
- /// <param name="series">The series.</param>
- /// <param name="seasonNumber">The season number.</param>
- /// <param name="episodeNumber">The episode number.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- private async Task AddEpisode(Series series, int seasonNumber, int episodeNumber, CancellationToken cancellationToken)
- {
- var season = series.Children.OfType<Season>()
- .FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == seasonNumber);
-
- if (season == null)
- {
- var provider = new DummySeasonProvider(_logger, _localization, _libraryManager, _fileSystem);
- season = await provider.AddSeason(series, seasonNumber, true, cancellationToken).ConfigureAwait(false);
- }
-
- var name = "Episode " + episodeNumber.ToString(CultureInfo.InvariantCulture);
-
- var episode = new Episode
- {
- Name = name,
- IndexNumber = episodeNumber,
- ParentIndexNumber = seasonNumber,
- Id = _libraryManager.GetNewItemId(
- series.Id + seasonNumber.ToString(CultureInfo.InvariantCulture) + name,
- typeof(Episode)),
- IsVirtualItem = true,
- SeasonId = season?.Id ?? Guid.Empty,
- SeriesId = series.Id
- };
-
- season.AddChild(episode, cancellationToken);
-
- await episode.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false);
- }
-
- /// <summary>
- /// Gets the existing episode.
- /// </summary>
- /// <param name="existingEpisodes">The existing episodes.</param>
- /// <param name="seasonCounts"></param>
- /// <param name="episodeTuple"></param>
- /// <returns>Episode.</returns>
- private Episode GetExistingEpisode(
- IEnumerable<Episode> existingEpisodes,
- IReadOnlyDictionary<int, int> seasonCounts,
- (int seasonNumber, int episodeNumber, DateTime firstAired) episodeTuple)
- {
- var seasonNumber = episodeTuple.seasonNumber;
- var episodeNumber = episodeTuple.episodeNumber;
-
- while (true)
- {
- var episode = GetExistingEpisode(existingEpisodes, seasonNumber, episodeNumber);
- if (episode != null)
- {
- return episode;
- }
-
- seasonNumber--;
-
- if (seasonCounts.ContainsKey(seasonNumber))
- {
- episodeNumber += seasonCounts[seasonNumber];
- }
- else
- {
- break;
- }
- }
-
- return null;
- }
-
- private Episode GetExistingEpisode(IEnumerable<Episode> existingEpisodes, int season, int episode)
- => existingEpisodes.FirstOrDefault(i => i.ParentIndexNumber == season && i.ContainsEpisodeNumber(episode));
- }
-}
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index a2c0e62c1..c8fc568a2 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -1,65 +1,26 @@
#pragma warning disable CS1591
-using System;
-using System.Threading;
-using System.Threading.Tasks;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
-using MediaBrowser.Providers.Plugins.TheTvdb;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Providers.TV
{
public class SeriesMetadataService : MetadataService<Series, SeriesInfo>
{
- private readonly ILocalizationManager _localization;
- private readonly TvdbClientManager _tvdbClientManager;
-
public SeriesMetadataService(
IServerConfigurationManager serverConfigurationManager,
ILogger<SeriesMetadataService> logger,
IProviderManager providerManager,
IFileSystem fileSystem,
- ILibraryManager libraryManager,
- ILocalizationManager localization,
- TvdbClientManager tvdbClientManager)
+ ILibraryManager libraryManager)
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
{
- _localization = localization;
- _tvdbClientManager = tvdbClientManager;
- }
-
- /// <inheritdoc />
- protected override async Task AfterMetadataRefresh(Series item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
- {
- await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false);
-
- var seasonProvider = new DummySeasonProvider(Logger, _localization, LibraryManager, FileSystem);
- await seasonProvider.Run(item, cancellationToken).ConfigureAwait(false);
-
- // TODO why does it not register this itself omg
- var provider = new MissingEpisodeProvider(
- Logger,
- ServerConfigurationManager,
- LibraryManager,
- _localization,
- FileSystem,
- _tvdbClientManager);
-
- try
- {
- await provider.Run(item, true, CancellationToken.None).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error in DummySeasonProvider for {ItemPath}", item.Path);
- }
}
/// <inheritdoc />
diff --git a/apiclient/templates/typescript/axios/generate.sh b/apiclient/templates/typescript/axios/generate.sh
new file mode 100644
index 000000000..8c4d74282
--- /dev/null
+++ b/apiclient/templates/typescript/axios/generate.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+artifactsDirectory="${1}"
+buildNumber="${2}"
+if [[ -n ${buildNumber} ]]; then
+ # Unstable build
+ additionalProperties=",snapshotVersion=-SNAPSHOT.${buildNumber},npmRepository=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/"
+else
+ # Stable build
+ additionalProperties=""
+fi
+
+java -jar openapi-generator-cli.jar generate \
+ --input-spec ${artifactsDirectory}/openapispec/openapi.json \
+ --generator-name typescript-axios \
+ --output ./apiclient/generated/typescript/axios \
+ --template-dir ./apiclient/templates/typescript/axios \
+ --ignore-file-override ./apiclient/.openapi-generator-ignore \
+ --additional-properties=useSingleRequestParameter="true",withSeparateModelsAndApi="true",modelPackage="models",apiPackage="api",npmName="axios"${additionalProperties}
diff --git a/apiclient/templates/typescript/axios/stable.sh b/apiclient/templates/typescript/axios/stable.sh
deleted file mode 100644
index 118ef219f..000000000
--- a/apiclient/templates/typescript/axios/stable.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/bash
-
-java -jar openapi-generator-cli.jar generate \
- --input-spec $(System.ArtifactsDirectory)/openapispec/openapi.json \
- --generator-name typescript-axios \
- --output ./apiclient/generated/typescript/axios \
- --template-dir ./apiclient/templates/typescript/axios \
- --ignore-file-override ./apiclient/.openapi-generator-ignore \
- --additional-properties=useSingleRequestParameter="true",withSeparateModelsAndApi="true",npmName="axios"
diff --git a/apiclient/templates/typescript/axios/unstable.sh b/apiclient/templates/typescript/axios/unstable.sh
deleted file mode 100644
index be9f9be43..000000000
--- a/apiclient/templates/typescript/axios/unstable.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/bash
-
-java -jar openapi-generator-cli.jar generate \
- --input-spec $(System.ArtifactsDirectory)/openapispec/openapi.json \
- --generator-name typescript-axios \
- --output ./apiclient/generated/typescript/axios \
- --template-dir ./apiclient/templates/typescript/axios \
- --ignore-file-override ./apiclient/.openapi-generator-ignore \
- --additional-properties=useSingleRequestParameter="true",withSeparateModelsAndApi="true",npmName="axios",snapshotVersion="-SNAPSHOT.$(Build.BuildNumber)",npmRepository="https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/"
diff --git a/deployment/build.windows.amd64 b/deployment/build.windows.amd64
index afe490576..a9daa6a23 100755
--- a/deployment/build.windows.amd64
+++ b/deployment/build.windows.amd64
@@ -35,10 +35,6 @@ unzip ${addin_build_dir}/jellyfin-ffmpeg.zip -d ${addin_build_dir}/jellyfin-ffmp
cp ${addin_build_dir}/jellyfin-ffmpeg/* ${output_dir}
rm -rf ${addin_build_dir}
-# Prepare scripts
-cp ${SOURCE_DIR}/windows/legacy/install-jellyfin.ps1 ${output_dir}/install-jellyfin.ps1
-cp ${SOURCE_DIR}/windows/legacy/install.bat ${output_dir}/install.bat
-
# Create zip package
pushd dist
zip -qr jellyfin-server_${version}.portable.zip jellyfin-server_${version}
diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec
index bfb2b3be2..93fb9fb41 100644
--- a/fedora/jellyfin.spec
+++ b/fedora/jellyfin.spec
@@ -79,15 +79,7 @@ EOF
%files server
%attr(755,root,root) %{_bindir}/jellyfin
-%{_libdir}/jellyfin/*.json
-%{_libdir}/jellyfin/*.dll
-%{_libdir}/jellyfin/*.so
-%{_libdir}/jellyfin/*.a
-%{_libdir}/jellyfin/createdump
-%{_libdir}/jellyfin/*.xml
-%{_libdir}/jellyfin/wwwroot/api-docs/*
-%{_libdir}/jellyfin/wwwroot/api-docs/redoc/*
-%{_libdir}/jellyfin/wwwroot/api-docs/swagger/*
+%{_libdir}/jellyfin/*
# Needs 755 else only root can run it since binary build by dotnet is 722
%attr(755,root,root) %{_libdir}/jellyfin/jellyfin
%{_libdir}/jellyfin/SOS_README.md
diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
index e3a7a5428..8a559b7b6 100644
--- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
+++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj
@@ -22,7 +22,7 @@
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="1.3.0" />
- <PackageReference Include="Moq" Version="4.14.5" />
+ <PackageReference Include="Moq" Version="4.14.6" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs
new file mode 100644
index 000000000..c801b4a52
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/ModelBinders/CommaDelimitedArrayModelBinderTests.cs
@@ -0,0 +1,229 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Globalization;
+using System.Text;
+using System.Threading.Tasks;
+using Jellyfin.Api.ModelBinders;
+using MediaBrowser.Controller.Entities;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.Extensions.Primitives;
+using Moq;
+using Xunit;
+using Xunit.Sdk;
+
+namespace Jellyfin.Api.Tests.ModelBinders
+{
+ public sealed class CommaDelimitedArrayModelBinderTests
+ {
+ [Fact]
+ public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedStringArrayQuery()
+ {
+ var queryParamName = "test";
+ var queryParamValues = new string[] { "lol", "xd" };
+ var queryParamString = "lol,xd";
+ var queryParamType = typeof(string[]);
+
+ var modelBinder = new CommaDelimitedArrayModelBinder();
+ var valueProvider = new QueryStringValueProvider(
+ new BindingSource(string.Empty, string.Empty, false, false),
+ new QueryCollection(new Dictionary<string, StringValues>() { { queryParamName, new StringValues(queryParamString) } }),
+ CultureInfo.InvariantCulture);
+ var bindingContextMock = new Mock<ModelBindingContext>();
+ bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+ bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+ bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+ bindingContextMock.SetupProperty(b => b.Result);
+
+ await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+ Assert.True(bindingContextMock.Object.Result.IsModelSet);
+ Assert.Equal((string[])bindingContextMock.Object.Result.Model, queryParamValues);
+ }
+
+ [Fact]
+ public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedIntArrayQuery()
+ {
+ var queryParamName = "test";
+ var queryParamValues = new int[] { 42, 0 };
+ var queryParamString = "42,0";
+ var queryParamType = typeof(int[]);
+
+ var modelBinder = new CommaDelimitedArrayModelBinder();
+ var valueProvider = new QueryStringValueProvider(
+ new BindingSource(string.Empty, string.Empty, false, false),
+ new QueryCollection(new Dictionary<string, StringValues>() { { queryParamName, new StringValues(queryParamString) } }),
+ CultureInfo.InvariantCulture);
+ var bindingContextMock = new Mock<ModelBindingContext>();
+ bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+ bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+ bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+ bindingContextMock.SetupProperty(b => b.Result);
+
+ await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+ Assert.True(bindingContextMock.Object.Result.IsModelSet);
+ Assert.Equal((int[])bindingContextMock.Object.Result.Model, queryParamValues);
+ }
+
+ [Fact]
+ public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedEnumArrayQuery()
+ {
+ var queryParamName = "test";
+ var queryParamValues = new TestType[] { TestType.How, TestType.Much };
+ var queryParamString = "How,Much";
+ var queryParamType = typeof(TestType[]);
+
+ var modelBinder = new CommaDelimitedArrayModelBinder();
+ var valueProvider = new QueryStringValueProvider(
+ new BindingSource(string.Empty, string.Empty, false, false),
+ new QueryCollection(new Dictionary<string, StringValues>() { { queryParamName, new StringValues(queryParamString) } }),
+ CultureInfo.InvariantCulture);
+ var bindingContextMock = new Mock<ModelBindingContext>();
+ bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+ bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+ bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+ bindingContextMock.SetupProperty(b => b.Result);
+
+ await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+ Assert.True(bindingContextMock.Object.Result.IsModelSet);
+ Assert.Equal((TestType[])bindingContextMock.Object.Result.Model, queryParamValues);
+ }
+
+ [Fact]
+ public async Task BindModelAsync_CorrectlyBindsValidCommaDelimitedEnumArrayQueryWithDoubleCommas()
+ {
+ var queryParamName = "test";
+ var queryParamValues = new TestType[] { TestType.How, TestType.Much };
+ var queryParamString = "How,,Much";
+ var queryParamType = typeof(TestType[]);
+
+ var modelBinder = new CommaDelimitedArrayModelBinder();
+ var valueProvider = new QueryStringValueProvider(
+ new BindingSource(string.Empty, string.Empty, false, false),
+ new QueryCollection(new Dictionary<string, StringValues>() { { queryParamName, new StringValues(queryParamString) } }),
+ CultureInfo.InvariantCulture);
+ var bindingContextMock = new Mock<ModelBindingContext>();
+ bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+ bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+ bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+ bindingContextMock.SetupProperty(b => b.Result);
+
+ await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+ Assert.True(bindingContextMock.Object.Result.IsModelSet);
+ Assert.Equal((TestType[])bindingContextMock.Object.Result.Model, queryParamValues);
+ }
+
+ [Fact]
+ public async Task BindModelAsync_CorrectlyBindsValidEnumArrayQuery()
+ {
+ var queryParamName = "test";
+ var queryParamValues = new TestType[] { TestType.How, TestType.Much };
+ var queryParamString1 = "How";
+ var queryParamString2 = "Much";
+ var queryParamType = typeof(TestType[]);
+
+ var modelBinder = new CommaDelimitedArrayModelBinder();
+
+ var valueProvider = new QueryStringValueProvider(
+ new BindingSource(string.Empty, string.Empty, false, false),
+ new QueryCollection(new Dictionary<string, StringValues>()
+ {
+ { queryParamName, new StringValues(new string[] { queryParamString1, queryParamString2 }) },
+ }),
+ CultureInfo.InvariantCulture);
+ var bindingContextMock = new Mock<ModelBindingContext>();
+ bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+ bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+ bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+ bindingContextMock.SetupProperty(b => b.Result);
+
+ await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+ Assert.True(bindingContextMock.Object.Result.IsModelSet);
+ Assert.Equal((TestType[])bindingContextMock.Object.Result.Model, queryParamValues);
+ }
+
+ [Fact]
+ public async Task BindModelAsync_CorrectlyBindsEmptyEnumArrayQuery()
+ {
+ var queryParamName = "test";
+ var queryParamValues = Array.Empty<TestType>();
+ var queryParamType = typeof(TestType[]);
+
+ var modelBinder = new CommaDelimitedArrayModelBinder();
+
+ var valueProvider = new QueryStringValueProvider(
+ new BindingSource(string.Empty, string.Empty, false, false),
+ new QueryCollection(new Dictionary<string, StringValues>()
+ {
+ { queryParamName, new StringValues(value: null) },
+ }),
+ CultureInfo.InvariantCulture);
+ var bindingContextMock = new Mock<ModelBindingContext>();
+ bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+ bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+ bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+ bindingContextMock.SetupProperty(b => b.Result);
+
+ await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+ Assert.False(bindingContextMock.Object.Result.IsModelSet);
+ }
+
+ [Fact]
+ public async Task BindModelAsync_ThrowsIfCommaDelimitedEnumArrayQueryIsInvalid()
+ {
+ var queryParamName = "test";
+ var queryParamString = "🔥,😢";
+ var queryParamType = typeof(TestType[]);
+
+ var modelBinder = new CommaDelimitedArrayModelBinder();
+ var valueProvider = new QueryStringValueProvider(
+ new BindingSource(string.Empty, string.Empty, false, false),
+ new QueryCollection(new Dictionary<string, StringValues>() { { queryParamName, new StringValues(queryParamString) } }),
+ CultureInfo.InvariantCulture);
+ var bindingContextMock = new Mock<ModelBindingContext>();
+ bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+ bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+ bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+ bindingContextMock.SetupProperty(b => b.Result);
+
+ Func<Task> act = async () => await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+ await Assert.ThrowsAsync<FormatException>(act);
+ }
+
+ [Fact]
+ public async Task BindModelAsync_ThrowsIfCommaDelimitedEnumArrayQueryIsInvalid2()
+ {
+ var queryParamName = "test";
+ var queryParamValues = new TestType[] { TestType.How, TestType.Much };
+ var queryParamString1 = "How";
+ var queryParamString2 = "😱";
+ var queryParamType = typeof(TestType[]);
+
+ var modelBinder = new CommaDelimitedArrayModelBinder();
+
+ var valueProvider = new QueryStringValueProvider(
+ new BindingSource(string.Empty, string.Empty, false, false),
+ new QueryCollection(new Dictionary<string, StringValues>()
+ {
+ { queryParamName, new StringValues(new string[] { queryParamString1, queryParamString2 }) },
+ }),
+ CultureInfo.InvariantCulture);
+ var bindingContextMock = new Mock<ModelBindingContext>();
+ bindingContextMock.Setup(b => b.ValueProvider).Returns(valueProvider);
+ bindingContextMock.Setup(b => b.ModelName).Returns(queryParamName);
+ bindingContextMock.Setup(b => b.ModelType).Returns(queryParamType);
+ bindingContextMock.SetupProperty(b => b.Result);
+
+ Func<Task> act = async () => await modelBinder.BindModelAsync(bindingContextMock.Object);
+
+ await Assert.ThrowsAsync<FormatException>(act);
+ }
+ }
+}
diff --git a/tests/Jellyfin.Api.Tests/ModelBinders/TestType.cs b/tests/Jellyfin.Api.Tests/ModelBinders/TestType.cs
new file mode 100644
index 000000000..544a74637
--- /dev/null
+++ b/tests/Jellyfin.Api.Tests/ModelBinders/TestType.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Jellyfin.Api.Tests.ModelBinders
+{
+ public enum TestType
+ {
+#pragma warning disable SA1602 // Enumeration items should be documented
+ How,
+ Much,
+ Is,
+ The,
+ Fish
+#pragma warning restore SA1602 // Enumeration items should be documented
+ }
+}
diff --git a/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs b/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs
new file mode 100644
index 000000000..d9e66d677
--- /dev/null
+++ b/tests/Jellyfin.Common.Tests/Json/JsonGuidConverterTests.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Text.Json;
+using MediaBrowser.Common.Json.Converters;
+using Xunit;
+
+namespace Jellyfin.Common.Tests.Extensions
+{
+ public static class JsonGuidConverterTests
+ {
+ [Fact]
+ public static void Deserialize_Valid_Success()
+ {
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new JsonGuidConverter());
+ Guid value = JsonSerializer.Deserialize<Guid>(@"""a852a27afe324084ae66db579ee3ee18""", options);
+ Assert.Equal(new Guid("a852a27afe324084ae66db579ee3ee18"), value);
+
+ value = JsonSerializer.Deserialize<Guid>(@"""e9b2dcaa-529c-426e-9433-5e9981f27f2e""", options);
+ Assert.Equal(new Guid("e9b2dcaa-529c-426e-9433-5e9981f27f2e"), value);
+ }
+
+ [Fact]
+ public static void Roundtrip_Valid_Success()
+ {
+ var options = new JsonSerializerOptions();
+ options.Converters.Add(new JsonGuidConverter());
+ Guid guid = new Guid("a852a27afe324084ae66db579ee3ee18");
+ string value = JsonSerializer.Serialize(guid, options);
+ Assert.Equal(guid, JsonSerializer.Deserialize<Guid>(value, options));
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
index d1679c279..75b67f460 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
+++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj
@@ -17,7 +17,7 @@
<PackageReference Include="AutoFixture" Version="4.13.0" />
<PackageReference Include="AutoFixture.AutoMoq" Version="4.13.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
- <PackageReference Include="Moq" Version="4.14.5" />
+ <PackageReference Include="Moq" Version="4.14.6" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="1.3.0" />