diff --git a/.ci/azure-pipelines-api-client.yml b/.ci/azure-pipelines-api-client.yml
new file mode 100644
index 0000000000..de6bbf04ce
--- /dev/null
+++ b/.ci/azure-pipelines-api-client.yml
@@ -0,0 +1,78 @@
+parameters:
+ - name: LinuxImage
+ type: string
+ default: "ubuntu-latest"
+ - name: GeneratorVersion
+ type: string
+ default: "5.0.0-beta2"
+
+jobs:
+- job: GenerateApiClients
+ displayName: 'Generate Api Clients'
+ dependsOn: Test
+
+ pool:
+ vmImage: "${{ parameters.LinuxImage }}"
+
+ steps:
+ - task: DownloadPipelineArtifact@2
+ displayName: 'Download OpenAPI Spec Artifact'
+ inputs:
+ source: 'current'
+ artifact: "OpenAPI Spec"
+ path: "$(System.ArtifactsDirectory)/openapispec"
+ runVersion: "latest"
+
+ - task: CmdLine@2
+ displayName: 'Download OpenApi Generator'
+ inputs:
+ script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar"
+
+## Authenticate with npm registry
+ - task: npmAuthenticate@0
+ inputs:
+ workingFile: ./.npmrc
+ customEndpoint: 'jellyfin-bot for NPM'
+
+## Generate npm api client
+# Unstable
+ - task: CmdLine@2
+ displayName: 'Build unstable typescript axios client'
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
+ inputs:
+ script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory) $(Build.BuildNumber)"
+
+# 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: 'jellyfin/unstable'
+ workingDir: ./apiclient/generated/typescript/axios
+
+# Stable
+ - task: Npm@1
+ displayName: 'Publish stable typescript axios client'
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
+ inputs:
+ command: publish
+ publishRegistry: useExternalRegistry
+ publishEndpoint: 'jellyfin-bot for NPM'
+ workingDir: ./apiclient/generated/typescript/axios
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index cc845afd43..67aac45c9d 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -63,7 +63,38 @@ jobs:
sshEndpoint: repository
sourceFolder: '$(Build.SourcesDirectory)/deployment/dist'
contents: '**'
- targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)'
+
+- job: OpenAPISpec
+ dependsOn: Test
+ condition: or(startsWith(variables['Build.SourceBranch'], 'refs/heads/master'),startsWith(variables['Build.SourceBranch'], 'refs/tags/v'))
+ displayName: 'Push OpenAPI Spec to repository'
+
+ pool:
+ vmImage: 'ubuntu-latest'
+
+ steps:
+ - task: DownloadPipelineArtifact@2
+ displayName: 'Download OpenAPI Spec'
+ inputs:
+ source: 'current'
+ artifact: "OpenAPI Spec"
+ path: "$(System.ArtifactsDirectory)/openapispec"
+ runVersion: "latest"
+
+ - task: SSH@0
+ displayName: 'Create target directory on repository server'
+ inputs:
+ sshEndpoint: repository
+ runOptions: 'inline'
+ inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)'
+
+ - task: CopyFilesOverSSH@0
+ displayName: 'Upload artifacts to repository server'
+ inputs:
+ sshEndpoint: repository
+ sourceFolder: '$(System.ArtifactsDirectory)/openapispec'
+ contents: 'openapi.json'
+ targetFolder: '/srv/repository/incoming/azure/$(Build.BuildNumber)'
- job: BuildDocker
displayName: 'Build Docker'
diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml
index eca8aa90f9..6a36698b56 100644
--- a/.ci/azure-pipelines-test.yml
+++ b/.ci/azure-pipelines-test.yml
@@ -56,7 +56,7 @@ jobs:
inputs:
command: "test"
projects: ${{ parameters.TestProjects }}
- arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal "-p:GenerateDocumentationFile=False"'
+ arguments: '--configuration Release --collect:"XPlat Code Coverage" --settings tests/coverletArgs.runsettings --verbosity minimal'
publishTestResults: true
testRunTitle: $(Agent.JobName)
workingDirectory: "$(Build.SourcesDirectory)"
diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml
index b417aae678..5b5a17dea2 100644
--- a/.ci/azure-pipelines.yml
+++ b/.ci/azure-pipelines.yml
@@ -34,6 +34,12 @@ jobs:
Linux: 'ubuntu-latest'
Windows: 'windows-latest'
macOS: 'macos-latest'
+
+- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
+ - template: azure-pipelines-test.yml
+ parameters:
+ ImageNames:
+ Linux: 'ubuntu-latest'
- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}:
- template: azure-pipelines-abi.yml
@@ -55,3 +61,6 @@ jobs:
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
- template: azure-pipelines-package.yml
+
+- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
+ - template: azure-pipelines-api-client.yml
diff --git a/.gitignore b/.gitignore
index 0df7606ce9..7cd3d0068a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -276,3 +276,4 @@ BenchmarkDotNet.Artifacts
web/
web-src.*
MediaBrowser.WebDashboard/jellyfin-web
+apiclient/generated
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000000..b7a317000b
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,3 @@
+registry=https://registry.npmjs.org/
+@jellyfin:registry=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/
+always-auth=true
\ No newline at end of file
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 99060d0b09..7b4772730a 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/MediaReceiverRegistrar/ControlHandler.cs b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs
index 8bf0cd961b..464f71a6f1 100644
--- a/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs
+++ b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.Xml;
@@ -10,8 +8,16 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.MediaReceiverRegistrar
{
+ ///
+ /// Defines the .
+ ///
public class ControlHandler : BaseControlHandler
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The for use with the instance.
+ /// The for use with the instance.
public ControlHandler(IServerConfigurationManager config, ILogger logger)
: base(config, logger)
{
@@ -35,9 +41,17 @@ namespace Emby.Dlna.MediaReceiverRegistrar
throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
}
+ ///
+ /// Records that the handle is authorized in the xml stream.
+ ///
+ /// The .
private static void HandleIsAuthorized(XmlWriter xmlWriter)
=> xmlWriter.WriteElementString("Result", "1");
+ ///
+ /// Records that the handle is validated in the xml stream.
+ ///
+ /// The .
private static void HandleIsValidated(XmlWriter xmlWriter)
=> xmlWriter.WriteElementString("Result", "1");
}
diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs
index e6d845e1e4..a5aae515c4 100644
--- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs
+++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarService.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System.Net.Http;
using System.Threading.Tasks;
using Emby.Dlna.Service;
@@ -8,10 +6,19 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.MediaReceiverRegistrar
{
+ ///
+ /// Defines the .
+ ///
public class MediaReceiverRegistrarService : BaseService, IMediaReceiverRegistrar
{
private readonly IServerConfigurationManager _config;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The for use with the instance.
+ /// The for use with the instance.
+ /// The for use with the instance.
public MediaReceiverRegistrarService(
ILogger logger,
IHttpClientFactory httpClientFactory,
@@ -24,7 +31,7 @@ namespace Emby.Dlna.MediaReceiverRegistrar
///
public string GetServiceXml()
{
- return new MediaReceiverRegistrarXmlBuilder().GetXml();
+ return MediaReceiverRegistrarXmlBuilder.GetXml();
}
///
diff --git a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs
index 26994925d1..37840cd096 100644
--- a/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs
+++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs
@@ -1,79 +1,89 @@
-#pragma warning disable CS1591
-
using System.Collections.Generic;
using Emby.Dlna.Common;
using Emby.Dlna.Service;
+using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.MediaReceiverRegistrar
{
- public class MediaReceiverRegistrarXmlBuilder
+ ///
+ /// Defines the .
+ /// See https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-drmnd/5d37515e-7a63-4709-8258-8fd4e0ed4482.
+ ///
+ public static class MediaReceiverRegistrarXmlBuilder
{
- public string GetXml()
+ ///
+ /// Retrieves an XML description of the X_MS_MediaReceiverRegistrar.
+ ///
+ /// An XML representation of this service.
+ public static string GetXml()
{
- return new ServiceXmlBuilder().GetXml(
- new ServiceActionListBuilder().GetActions(),
- GetStateVariables());
+ return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables());
}
+ ///
+ /// The a list of all the state variables for this invocation.
+ ///
+ /// The .
private static IEnumerable GetStateVariables()
{
- var list = new List();
-
- list.Add(new StateVariable
+ var list = new List
{
- Name = "AuthorizationGrantedUpdateID",
- DataType = "ui4",
- SendsEvents = true
- });
+ new StateVariable
+ {
+ Name = "AuthorizationGrantedUpdateID",
+ DataType = "ui4",
+ SendsEvents = true
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_DeviceID",
- DataType = "string",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_DeviceID",
+ DataType = "string",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "AuthorizationDeniedUpdateID",
- DataType = "ui4",
- SendsEvents = true
- });
+ new StateVariable
+ {
+ Name = "AuthorizationDeniedUpdateID",
+ DataType = "ui4",
+ SendsEvents = true
+ },
- list.Add(new StateVariable
- {
- Name = "ValidationSucceededUpdateID",
- DataType = "ui4",
- SendsEvents = true
- });
+ new StateVariable
+ {
+ Name = "ValidationSucceededUpdateID",
+ DataType = "ui4",
+ SendsEvents = true
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_RegistrationRespMsg",
- DataType = "bin.base64",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_RegistrationRespMsg",
+ DataType = "bin.base64",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_RegistrationReqMsg",
- DataType = "bin.base64",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_RegistrationReqMsg",
+ DataType = "bin.base64",
+ SendsEvents = false
+ },
- list.Add(new StateVariable
- {
- Name = "ValidationRevokedUpdateID",
- DataType = "ui4",
- SendsEvents = true
- });
+ new StateVariable
+ {
+ Name = "ValidationRevokedUpdateID",
+ DataType = "ui4",
+ SendsEvents = true
+ },
- list.Add(new StateVariable
- {
- Name = "A_ARG_TYPE_Result",
- DataType = "int",
- SendsEvents = false
- });
+ new StateVariable
+ {
+ Name = "A_ARG_TYPE_Result",
+ DataType = "int",
+ SendsEvents = false
+ }
+ };
return list;
}
diff --git a/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs b/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs
index 13545c6894..1dc9c79c14 100644
--- a/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs
+++ b/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs
@@ -1,13 +1,19 @@
-#pragma warning disable CS1591
-
using System.Collections.Generic;
using Emby.Dlna.Common;
+using MediaBrowser.Model.Dlna;
namespace Emby.Dlna.MediaReceiverRegistrar
{
- public class ServiceActionListBuilder
+ ///
+ /// Defines the .
+ ///
+ public static class ServiceActionListBuilder
{
- public IEnumerable GetActions()
+ ///
+ /// Returns a list of services that this instance provides.
+ ///
+ /// An .
+ public static IEnumerable GetActions()
{
return new[]
{
@@ -21,6 +27,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
};
}
+ ///
+ /// Returns the action details for "IsValidated".
+ ///
+ /// The .
private static ServiceAction GetIsValidated()
{
var action = new ServiceAction
@@ -43,6 +53,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action;
}
+ ///
+ /// Returns the action details for "IsAuthorized".
+ ///
+ /// The .
private static ServiceAction GetIsAuthorized()
{
var action = new ServiceAction
@@ -65,6 +79,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action;
}
+ ///
+ /// Returns the action details for "RegisterDevice".
+ ///
+ /// The .
private static ServiceAction GetRegisterDevice()
{
var action = new ServiceAction
@@ -87,6 +105,10 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action;
}
+ ///
+ /// Returns the action details for "GetValidationSucceededUpdateID".
+ ///
+ /// The .
private static ServiceAction GetGetValidationSucceededUpdateID()
{
var action = new ServiceAction
@@ -103,7 +125,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action;
}
- private ServiceAction GetGetAuthorizationDeniedUpdateID()
+ ///
+ /// Returns the action details for "GetGetAuthorizationDeniedUpdateID".
+ ///
+ /// The .
+ private static ServiceAction GetGetAuthorizationDeniedUpdateID()
{
var action = new ServiceAction
{
@@ -119,7 +145,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action;
}
- private ServiceAction GetGetValidationRevokedUpdateID()
+ ///
+ /// Returns the action details for "GetValidationRevokedUpdateID".
+ ///
+ /// The .
+ private static ServiceAction GetGetValidationRevokedUpdateID()
{
var action = new ServiceAction
{
@@ -135,7 +165,11 @@ namespace Emby.Dlna.MediaReceiverRegistrar
return action;
}
- private ServiceAction GetGetAuthorizationGrantedUpdateID()
+ ///
+ /// Returns the action details for "GetAuthorizationGrantedUpdateID".
+ ///
+ /// The .
+ private static ServiceAction GetGetAuthorizationGrantedUpdateID()
{
var action = new ServiceAction
{
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 63fd8ce5a7..a5b8e2b3ce 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -811,7 +811,7 @@ namespace Emby.Dlna.PlayTo
}
///
- public Task SendMessage(string name, Guid messageId, T data, CancellationToken cancellationToken)
+ public Task SendMessage(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 21877f121f..e93aef3043 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.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
index 1f429d0de3..bca9e81cd0 100644
--- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs
+++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
@@ -235,13 +235,13 @@ namespace Emby.Dlna.Server
.Append(SecurityElement.Escape(service.ServiceId ?? string.Empty))
.Append("");
builder.Append("")
- .Append(BuildUrl(service.ScpdUrl, true))
+ .Append(BuildUrl(service.ScpdUrl))
.Append("");
builder.Append("")
- .Append(BuildUrl(service.ControlUrl, true))
+ .Append(BuildUrl(service.ControlUrl))
.Append("");
builder.Append("")
- .Append(BuildUrl(service.EventSubUrl, true))
+ .Append(BuildUrl(service.EventSubUrl))
.Append("");
builder.Append("");
@@ -250,13 +250,7 @@ namespace Emby.Dlna.Server
builder.Append("");
}
- ///
- /// Builds a valid url for inclusion in the xml.
- ///
- /// Url to include.
- /// Optional. When set to true, the absolute url is always used.
- /// The url to use for the element.
- private string BuildUrl(string url, bool absoluteUrl = false)
+ private string BuildUrl(string url)
{
if (string.IsNullOrEmpty(url))
{
@@ -267,7 +261,7 @@ namespace Emby.Dlna.Server
url = "/dlna/" + _serverUdn + "/" + url;
- if (EnableAbsoluteUrls || absoluteUrl)
+ if (EnableAbsoluteUrls)
{
url = _serverAddress.TrimEnd('/') + url;
}
diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs
index d160e33393..4b0bbba969 100644
--- a/Emby.Dlna/Service/BaseControlHandler.cs
+++ b/Emby.Dlna/Service/BaseControlHandler.cs
@@ -60,10 +60,8 @@ namespace Emby.Dlna.Service
Async = true
};
- using (var reader = XmlReader.Create(streamReader, readerSettings))
- {
- requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
- }
+ using var reader = XmlReader.Create(streamReader, readerSettings);
+ requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
}
Logger.LogDebug("Received control request {0}", requestInfo.LocalName);
@@ -124,10 +122,8 @@ namespace Emby.Dlna.Service
{
if (!reader.IsEmptyElement)
{
- using (var subReader = reader.ReadSubtree())
- {
- return await ParseBodyTagAsync(subReader).ConfigureAwait(false);
- }
+ using var subReader = reader.ReadSubtree();
+ return await ParseBodyTagAsync(subReader).ConfigureAwait(false);
}
else
{
@@ -150,12 +146,12 @@ namespace Emby.Dlna.Service
}
}
- return new ControlRequestInfo();
+ throw new EndOfStreamException("Stream ended but no body tag found.");
}
private async Task ParseBodyTagAsync(XmlReader reader)
{
- var result = new ControlRequestInfo();
+ string namespaceURI = null, localName = null;
await reader.MoveToContentAsync().ConfigureAwait(false);
await reader.ReadAsync().ConfigureAwait(false);
@@ -165,16 +161,14 @@ namespace Emby.Dlna.Service
{
if (reader.NodeType == XmlNodeType.Element)
{
- result.LocalName = reader.LocalName;
- result.NamespaceURI = reader.NamespaceURI;
+ localName = reader.LocalName;
+ namespaceURI = reader.NamespaceURI;
if (!reader.IsEmptyElement)
{
- using (var subReader = reader.ReadSubtree())
- {
- await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false);
- return result;
- }
+ var result = new ControlRequestInfo(localName, namespaceURI);
+ using var subReader = reader.ReadSubtree();
+ await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false);
}
else
{
@@ -187,7 +181,12 @@ namespace Emby.Dlna.Service
}
}
- return result;
+ if (localName != null && namespaceURI != null)
+ {
+ return new ControlRequestInfo(localName, namespaceURI);
+ }
+
+ throw new EndOfStreamException("Stream ended but no control found.");
}
private async Task ParseFirstBodyChildAsync(XmlReader reader, IDictionary headers)
@@ -234,11 +233,18 @@ namespace Emby.Dlna.Service
private class ControlRequestInfo
{
+ public ControlRequestInfo(string localName, string namespaceUri)
+ {
+ LocalName = localName;
+ NamespaceURI = namespaceUri;
+ Headers = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ }
+
public string LocalName { get; set; }
public string NamespaceURI { get; set; }
- public Dictionary Headers { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ public Dictionary Headers { get; }
}
}
}
diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs
index ed20292f6b..8a2301d2d6 100644
--- a/Emby.Drawing/ImageProcessor.cs
+++ b/Emby.Drawing/ImageProcessor.cs
@@ -36,7 +36,7 @@ namespace Emby.Drawing
private readonly IImageEncoder _imageEncoder;
private readonly IMediaEncoder _mediaEncoder;
- private bool _disposed = false;
+ private bool _disposed;
///
/// Initializes a new instance of the class.
@@ -466,11 +466,11 @@ namespace Emby.Drawing
}
///
- public void CreateImageCollage(ImageCollageOptions options)
+ public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
{
_logger.LogInformation("Creating image collage and saving to {Path}", options.OutputPath);
- _imageEncoder.CreateImageCollage(options);
+ _imageEncoder.CreateImageCollage(options, libraryName);
_logger.LogInformation("Completed creation of image collage and saved to {Path}", options.OutputPath);
}
diff --git a/Emby.Drawing/NullImageEncoder.cs b/Emby.Drawing/NullImageEncoder.cs
index bbb5c17162..2a1cfd3da5 100644
--- a/Emby.Drawing/NullImageEncoder.cs
+++ b/Emby.Drawing/NullImageEncoder.cs
@@ -38,7 +38,7 @@ namespace Emby.Drawing
}
///
- public void CreateImageCollage(ImageCollageOptions options)
+ public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
{
throw new NotImplementedException();
}
diff --git a/Emby.Naming/AudioBook/AudioBookResolver.cs b/Emby.Naming/AudioBook/AudioBookResolver.cs
index ed53bd04fa..5807d4688c 100644
--- a/Emby.Naming/AudioBook/AudioBookResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookResolver.cs
@@ -1,3 +1,4 @@
+#nullable enable
#pragma warning disable CS1591
using System;
@@ -16,21 +17,11 @@ namespace Emby.Naming.AudioBook
_options = options;
}
- public AudioBookFileInfo ParseFile(string path)
+ public AudioBookFileInfo? Resolve(string path, bool isDirectory = false)
{
- return Resolve(path, false);
- }
-
- public AudioBookFileInfo ParseDirectory(string path)
- {
- return Resolve(path, true);
- }
-
- public AudioBookFileInfo Resolve(string path, bool isDirectory = false)
- {
- if (string.IsNullOrEmpty(path))
+ if (path.Length == 0)
{
- throw new ArgumentNullException(nameof(path));
+ throw new ArgumentException("String can't be empty.", nameof(path));
}
// TODO
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 7a46fdf2e7..31ca738296 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;
@@ -127,7 +128,6 @@ namespace Emby.Server.Implementations
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
private IHttpClientFactory _httpClientFactory;
- private IWebSocketManager _webSocketManager;
private string[] _urlPrefixes;
@@ -258,8 +258,8 @@ namespace Emby.Server.Implementations
IServiceCollection serviceCollection)
{
_xmlSerializer = new MyXmlSerializer();
- _jsonSerializer = new JsonSerializer();
-
+ _jsonSerializer = new JsonSerializer();
+
ServiceCollection = serviceCollection;
_networkManager = networkManager;
@@ -537,6 +537,7 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton(_fileSystemManager);
ServiceCollection.AddSingleton();
+ ServiceCollection.AddSingleton();
ServiceCollection.AddSingleton(_networkManager);
@@ -665,7 +666,6 @@ namespace Emby.Server.Implementations
_mediaEncoder = Resolve();
_sessionManager = Resolve();
_httpClientFactory = Resolve();
- _webSocketManager = Resolve();
((AuthenticationRepository)Resolve()).Initialize();
@@ -786,7 +786,6 @@ namespace Emby.Server.Implementations
.ToArray();
_urlPrefixes = GetUrlPrefixes().ToArray();
- _webSocketManager.Init(GetExports());
Resolve().AddParts(
GetExports(),
@@ -819,38 +818,6 @@ namespace Emby.Server.Implementations
{
try
{
- if (plugin is IPluginAssembly assemblyPlugin)
- {
- var assembly = plugin.GetType().Assembly;
- var assemblyName = assembly.GetName();
- var assemblyFilePath = assembly.Location;
-
- var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath));
-
- assemblyPlugin.SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version);
-
- try
- {
- var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true);
- if (idAttributes.Length > 0)
- {
- var attribute = (GuidAttribute)idAttributes[0];
- var assemblyId = new Guid(attribute.Value);
-
- assemblyPlugin.SetId(assemblyId);
- }
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error getting plugin Id from {PluginName}.", plugin.GetType().FullName);
- }
- }
-
- if (plugin is IHasPluginConfiguration hasPluginConfiguration)
- {
- hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s));
- }
-
plugin.RegisterServices(ServiceCollection);
}
catch (Exception ex)
@@ -1088,7 +1055,7 @@ namespace Emby.Server.Implementations
{
// No metafile, so lets see if the folder is versioned.
metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1];
-
+
int versionIndex = dir.LastIndexOf('_');
if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version ver))
{
@@ -1097,9 +1064,9 @@ namespace Emby.Server.Implementations
}
else
{
- // Un-versioned folder - Add it under the path name and version 0.0.0.1.
+ // Un-versioned folder - Add it under the path name and version 0.0.0.1.
versions.Add((new Version(0, 0, 0, 1), metafile, dir));
- }
+ }
}
}
catch
diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
index 0fb050a7a5..8c756a7f4c 100644
--- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
+++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs
@@ -157,7 +157,8 @@ namespace Emby.Server.Implementations.Data
protected bool TableExists(ManagedConnection connection, string name)
{
- return connection.RunInTransaction(db =>
+ return connection.RunInTransaction(
+ db =>
{
using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master"))
{
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index d09f84e174..1c2aeda709 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -219,7 +219,8 @@ namespace Emby.Server.Implementations.Data
{
connection.RunQueries(queries);
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
var existingColumnNames = GetColumnNames(db, "AncestorIds");
AddColumn(db, "AncestorIds", "AncestorIdText", "Text", existingColumnNames);
@@ -495,7 +496,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
using (var saveImagesStatement = base.PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id"))
{
@@ -546,7 +548,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
SaveItemsInTranscation(db, tuples);
}, TransactionMode);
@@ -2032,7 +2035,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
// First delete chapters
db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", idBlob);
@@ -2921,7 +2925,8 @@ namespace Emby.Server.Implementations.Data
var result = new QueryResult();
using (var connection = GetConnection(true))
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
var statements = PrepareAll(db, statementTexts);
@@ -3324,7 +3329,8 @@ namespace Emby.Server.Implementations.Data
var result = new QueryResult();
using (var connection = GetConnection(true))
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
var statements = PrepareAll(db, statementTexts);
@@ -4899,7 +4905,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
connection.ExecuteAll(sql);
}, TransactionMode);
@@ -4950,7 +4957,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
var idBlob = id.ToByteArray();
@@ -5357,7 +5365,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
itemCountColumns = new Dictionary()
{
- { "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes"}
+ { "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes" }
};
}
@@ -5744,7 +5752,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
var itemIdBlob = itemId.ToByteArray();
@@ -5898,7 +5907,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
var itemIdBlob = id.ToByteArray();
@@ -6232,7 +6242,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
var itemIdBlob = id.ToByteArray();
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
index 4a78aac8e6..2c4e8e0fcc 100644
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
@@ -44,7 +44,8 @@ namespace Emby.Server.Implementations.Data
var users = userDatasTableExists ? null : userManager.Users;
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
db.ExecuteAll(string.Join(";", new[] {
@@ -178,7 +179,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
SaveUserData(db, internalUserId, key, userData);
}, TransactionMode);
@@ -246,7 +248,8 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
foreach (var userItemData in userDataList)
{
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 9ed3cca99c..c762aa0b84 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -32,10 +32,10 @@
-
-
-
-
+
+
+
+
diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
index c9d21d9638..ff64e217a0 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 { userId }, "LibraryChanged", info, cancellationToken).ConfigureAwait(false);
+ await _sessionManager.SendMessageToUserSessions(new List { 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 44d2580d68..824bb85f44 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 e)
{
- await SendMessage("SeriesTimerCreated", e.Argument).ConfigureAwait(false);
+ await SendMessage(SessionMessageType.SeriesTimerCreated, e.Argument).ConfigureAwait(false);
}
private async void OnLiveTvManagerTimerCreated(object sender, GenericEventArgs e)
{
- await SendMessage("TimerCreated", e.Argument).ConfigureAwait(false);
+ await SendMessage(SessionMessageType.TimerCreated, e.Argument).ConfigureAwait(false);
}
private async void OnLiveTvManagerSeriesTimerCancelled(object sender, GenericEventArgs e)
{
- await SendMessage("SeriesTimerCancelled", e.Argument).ConfigureAwait(false);
+ await SendMessage(SessionMessageType.SeriesTimerCancelled, e.Argument).ConfigureAwait(false);
}
private async void OnLiveTvManagerTimerCancelled(object sender, GenericEventArgs 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 1da717e752..1989e9ed25 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 changedItems, CancellationToken cancellationToken)
{
- return _sessionManager.SendMessageToUserSessions(new List { userId }, "UserDataChanged", () => GetUserDataChangeInfo(userId, changedItems), cancellationToken);
+ return _sessionManager.SendMessageToUserSessions(new List { userId }, SessionMessageType.UserDataChanged, () => GetUserDataChangeInfo(userId, changedItems), cancellationToken);
}
private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, List changedItems)
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index 7eae4e7646..fed2addf80 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
{
MessageId = Guid.NewGuid(),
- MessageType = "KeepAlive"
+ MessageType = SessionMessageType.KeepAlive
}, CancellationToken.None);
}
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
index 89c1b7ea08..71ece80a75 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs
@@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using System.Net.WebSockets;
using System.Threading.Tasks;
using Jellyfin.Data.Events;
@@ -14,16 +13,18 @@ namespace Emby.Server.Implementations.HttpServer
{
public class WebSocketManager : IWebSocketManager
{
+ private readonly Lazy> _webSocketListeners;
private readonly ILogger _logger;
private readonly ILoggerFactory _loggerFactory;
- private IWebSocketListener[] _webSocketListeners = Array.Empty();
private bool _disposed = false;
public WebSocketManager(
+ Lazy> webSocketListeners,
ILogger logger,
ILoggerFactory loggerFactory)
{
+ _webSocketListeners = webSocketListeners;
_logger = logger;
_loggerFactory = loggerFactory;
}
@@ -68,15 +69,6 @@ namespace Emby.Server.Implementations.HttpServer
}
}
- ///
- /// Adds the rest handlers.
- ///
- /// The web socket listeners.
- public void Init(IEnumerable listeners)
- {
- _webSocketListeners = listeners.ToArray();
- }
-
///
/// Processes the web socket message received.
///
@@ -90,7 +82,8 @@ namespace Emby.Server.Implementations.HttpServer
IEnumerable GetTasks()
{
- foreach (var x in _webSocketListeners)
+ var listeners = _webSocketListeners.Value;
+ foreach (var x in listeners)
{
yield return x.ProcessMessageAsync(result);
}
diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
index 57302b5067..5f7e51858a 100644
--- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
+++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
@@ -133,9 +133,20 @@ namespace Emby.Server.Implementations.Images
protected virtual IEnumerable GetStripCollageImagePaths(BaseItem primaryItem, IEnumerable items)
{
+ var useBackdrop = primaryItem is CollectionFolder;
return items
.Select(i =>
{
+ // Use Backdrop instead of Primary image for Library images.
+ if (useBackdrop)
+ {
+ var backdrop = i.GetImageInfo(ImageType.Backdrop, 0);
+ if (backdrop != null && backdrop.IsLocalFile)
+ {
+ return backdrop.Path;
+ }
+ }
+
var image = i.GetImageInfo(ImageType.Primary, 0);
if (image != null && image.IsLocalFile)
{
@@ -190,7 +201,7 @@ namespace Emby.Server.Implementations.Images
return null;
}
- ImageProcessor.CreateImageCollage(options);
+ ImageProcessor.CreateImageCollage(options, primaryItem.Name);
return outputPath;
}
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 67cf8bf5ba..376a155705 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@@ -43,7 +44,7 @@ namespace Emby.Server.Implementations.Library
private readonly ILocalizationManager _localizationManager;
private readonly IApplicationPaths _appPaths;
- private readonly Dictionary _openStreams = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ private readonly ConcurrentDictionary _openStreams = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
private IMediaSourceProvider[] _providers;
@@ -582,29 +583,20 @@ namespace Emby.Server.Implementations.Library
mediaSource.InferTotalBitrate();
}
- public async Task GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
+ public Task GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
{
- await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- try
+ var info = _openStreams.Values.FirstOrDefault(i =>
{
- var info = _openStreams.Values.FirstOrDefault(i =>
+ var liveStream = i as ILiveStream;
+ if (liveStream != null)
{
- var liveStream = i as ILiveStream;
- if (liveStream != null)
- {
- return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase);
- }
+ return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase);
+ }
- return false;
- });
+ return false;
+ });
- return info as IDirectStreamProvider;
- }
- finally
- {
- _liveStreamSemaphore.Release();
- }
+ return Task.FromResult(info as IDirectStreamProvider);
}
public async Task OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken)
@@ -793,29 +785,20 @@ namespace Emby.Server.Implementations.Library
return new Tuple(info.MediaSource, info as IDirectStreamProvider);
}
- private async Task GetLiveStreamInfo(string id, CancellationToken cancellationToken)
+ private Task GetLiveStreamInfo(string id, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(id))
{
throw new ArgumentNullException(nameof(id));
}
- await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
-
- try
+ if (_openStreams.TryGetValue(id, out ILiveStream info))
{
- if (_openStreams.TryGetValue(id, out ILiveStream info))
- {
- return info;
- }
- else
- {
- throw new ResourceNotFoundException();
- }
+ return Task.FromResult(info);
}
- finally
+ else
{
- _liveStreamSemaphore.Release();
+ return Task.FromException(new ResourceNotFoundException());
}
}
@@ -844,7 +827,7 @@ namespace Emby.Server.Implementations.Library
if (liveStream.ConsumerCount <= 0)
{
- _openStreams.Remove(id);
+ _openStreams.TryRemove(id, out _);
_logger.LogInformation("Closing live stream {0}", id);
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
index 03059e6d35..70be524116 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
@@ -32,7 +32,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// The priority.
public override ResolverPriority Priority => ResolverPriority.Fourth;
- public MultiItemResolverResult ResolveMultiple(Folder parent,
+ public MultiItemResolverResult ResolveMultiple(
+ Folder parent,
List files,
string collectionType,
IDirectoryService directoryService)
@@ -50,7 +51,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
return result;
}
- private MultiItemResolverResult ResolveMultipleInternal(Folder parent,
+ private MultiItemResolverResult ResolveMultipleInternal(
+ Folder parent,
List files,
string collectionType,
IDirectoryService directoryService)
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
index 79b6dded3b..18ceb5e761 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
using Emby.Naming.Audio;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
@@ -113,52 +116,48 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
IFileSystem fileSystem,
ILibraryManager libraryManager)
{
+ // check for audio files before digging down into directories
+ var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && libraryManager.IsAudioFile(fileSystemInfo.FullName));
+ if (foundAudioFile)
+ {
+ // at least one audio file exists
+ return true;
+ }
+
+ if (!allowSubfolders)
+ {
+ // not music since no audio file exists and we're not looking into subfolders
+ return false;
+ }
+
var discSubfolderCount = 0;
- var notMultiDisc = false;
var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
var parser = new AlbumParser(namingOptions);
- foreach (var fileSystemInfo in list)
+
+ var directories = list.Where(fileSystemInfo => fileSystemInfo.IsDirectory);
+
+ var result = Parallel.ForEach(directories, (fileSystemInfo, state) =>
{
- if (fileSystemInfo.IsDirectory)
+ var path = fileSystemInfo.FullName;
+ var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager);
+
+ if (hasMusic)
{
- if (allowSubfolders)
+ if (parser.IsMultiPart(path))
{
- if (notMultiDisc)
- {
- continue;
- }
-
- var path = fileSystemInfo.FullName;
- var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager);
-
- if (hasMusic)
- {
- if (parser.IsMultiPart(path))
- {
- logger.LogDebug("Found multi-disc folder: " + path);
- discSubfolderCount++;
- }
- else
- {
- // If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album
- notMultiDisc = true;
- }
- }
+ logger.LogDebug("Found multi-disc folder: " + path);
+ Interlocked.Increment(ref discSubfolderCount);
+ }
+ else
+ {
+ // If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album
+ state.Stop();
}
}
- else
- {
- var fullName = fileSystemInfo.FullName;
+ });
- if (libraryManager.IsAudioFile(fullName))
- {
- return true;
- }
- }
- }
-
- if (notMultiDisc)
+ if (!result.IsCompleted)
{
return false;
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
index 5f5cd0e928..e9e688fa67 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
@@ -1,5 +1,6 @@
using System;
using System.Linq;
+using System.Threading.Tasks;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
@@ -94,7 +95,18 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager);
// If we contain an album assume we are an artist folder
- return args.FileSystemChildren.Where(i => i.IsDirectory).Any(i => albumResolver.IsMusicAlbum(i.FullName, directoryService)) ? new MusicArtist() : null;
+ var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
+
+ var result = Parallel.ForEach(directories, (fileSystemInfo, state) =>
+ {
+ if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, directoryService))
+ {
+ // stop once we see a music album
+ state.Stop();
+ }
+ });
+
+ return !result.IsCompleted ? new MusicArtist() : null;
}
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
index 86a5d8b7d8..59af7ce8ac 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
@@ -50,7 +50,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
var fileExtension = Path.GetExtension(f.FullName) ??
string.Empty;
- return _validExtensions.Contains(fileExtension,
+ return _validExtensions.Contains(
+ fileExtension,
StringComparer
.OrdinalIgnoreCase);
}).ToList();
diff --git a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
index f1b61f7c7d..582b649235 100644
--- a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
+++ b/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs
@@ -43,7 +43,7 @@ namespace Emby.Server.Implementations.LiveTv
return new[]
{
// Every so often
- new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
+ new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
};
}
diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json
index d33e118931..977a1c2d70 100644
--- a/Emby.Server.Implementations/Localization/Core/af.json
+++ b/Emby.Server.Implementations/Localization/Core/af.json
@@ -85,7 +85,6 @@
"ItemAddedWithName": "{0} is in die versameling",
"HomeVideos": "Tuis opnames",
"HeaderRecordingGroups": "Groep Opnames",
- "HeaderCameraUploads": "Kamera Oplaai",
"Genres": "Genres",
"FailedLoginAttemptWithUserName": "Mislukte aansluiting van {0}",
"ChapterNameValue": "Hoofstuk",
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index 4eac8e75de..4b898e6fe0 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -16,7 +16,6 @@
"Folders": "المجلدات",
"Genres": "التضنيفات",
"HeaderAlbumArtists": "فناني الألبومات",
- "HeaderCameraUploads": "تحميلات الكاميرا",
"HeaderContinueWatching": "استئناف",
"HeaderFavoriteAlbums": "الألبومات المفضلة",
"HeaderFavoriteArtists": "الفنانون المفضلون",
diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json
index 3fc7c7dc0f..1fed832768 100644
--- a/Emby.Server.Implementations/Localization/Core/bg-BG.json
+++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json
@@ -16,7 +16,6 @@
"Folders": "Папки",
"Genres": "Жанрове",
"HeaderAlbumArtists": "Изпълнители на албуми",
- "HeaderCameraUploads": "Качени от камера",
"HeaderContinueWatching": "Продължаване на гледането",
"HeaderFavoriteAlbums": "Любими албуми",
"HeaderFavoriteArtists": "Любими изпълнители",
diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json
index 1bd1909827..5667bf337e 100644
--- a/Emby.Server.Implementations/Localization/Core/bn.json
+++ b/Emby.Server.Implementations/Localization/Core/bn.json
@@ -14,7 +14,6 @@
"HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
"HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
"HeaderContinueWatching": "দেখতে থাকুন",
- "HeaderCameraUploads": "ক্যামেরার আপলোড সমূহ",
"HeaderAlbumArtists": "এলবাম শিল্পী",
"Genres": "জেনার",
"Folders": "ফোল্ডারগুলো",
diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index 2c802a39ef..b7852eccb3 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -16,7 +16,6 @@
"Folders": "Carpetes",
"Genres": "Gèneres",
"HeaderAlbumArtists": "Artistes del Àlbum",
- "HeaderCameraUploads": "Pujades de Càmera",
"HeaderContinueWatching": "Continua Veient",
"HeaderFavoriteAlbums": "Àlbums Preferits",
"HeaderFavoriteArtists": "Artistes Preferits",
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index 464ca28ca0..b34fad066a 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -16,7 +16,6 @@
"Folders": "Složky",
"Genres": "Žánry",
"HeaderAlbumArtists": "Umělci alba",
- "HeaderCameraUploads": "Nahrané fotografie",
"HeaderContinueWatching": "Pokračovat ve sledování",
"HeaderFavoriteAlbums": "Oblíbená alba",
"HeaderFavoriteArtists": "Oblíbení interpreti",
diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json
index f5397b62cd..b29ad94eff 100644
--- a/Emby.Server.Implementations/Localization/Core/da.json
+++ b/Emby.Server.Implementations/Localization/Core/da.json
@@ -16,7 +16,6 @@
"Folders": "Mapper",
"Genres": "Genrer",
"HeaderAlbumArtists": "Albumkunstnere",
- "HeaderCameraUploads": "Kamera Uploads",
"HeaderContinueWatching": "Fortsæt Afspilning",
"HeaderFavoriteAlbums": "Favoritalbummer",
"HeaderFavoriteArtists": "Favoritkunstnere",
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index fcbe9566e2..97ad1694a6 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -16,7 +16,6 @@
"Folders": "Verzeichnisse",
"Genres": "Genres",
"HeaderAlbumArtists": "Album-Interpreten",
- "HeaderCameraUploads": "Kamera-Uploads",
"HeaderContinueWatching": "Fortsetzen",
"HeaderFavoriteAlbums": "Lieblingsalben",
"HeaderFavoriteArtists": "Lieblings-Interpreten",
diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json
index 0753ea39d4..c45cc11cb8 100644
--- a/Emby.Server.Implementations/Localization/Core/el.json
+++ b/Emby.Server.Implementations/Localization/Core/el.json
@@ -16,7 +16,6 @@
"Folders": "Φάκελοι",
"Genres": "Είδη",
"HeaderAlbumArtists": "Καλλιτέχνες του Άλμπουμ",
- "HeaderCameraUploads": "Μεταφορτώσεις Κάμερας",
"HeaderContinueWatching": "Συνεχίστε την παρακολούθηση",
"HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ",
"HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες",
diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json
index 544c38cfa9..57ff13219f 100644
--- a/Emby.Server.Implementations/Localization/Core/en-GB.json
+++ b/Emby.Server.Implementations/Localization/Core/en-GB.json
@@ -16,7 +16,6 @@
"Folders": "Folders",
"Genres": "Genres",
"HeaderAlbumArtists": "Album Artists",
- "HeaderCameraUploads": "Camera Uploads",
"HeaderContinueWatching": "Continue Watching",
"HeaderFavoriteAlbums": "Favourite Albums",
"HeaderFavoriteArtists": "Favourite Artists",
diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json
index 97a843160e..92c54fb0e6 100644
--- a/Emby.Server.Implementations/Localization/Core/en-US.json
+++ b/Emby.Server.Implementations/Localization/Core/en-US.json
@@ -16,7 +16,6 @@
"Folders": "Folders",
"Genres": "Genres",
"HeaderAlbumArtists": "Album Artists",
- "HeaderCameraUploads": "Camera Uploads",
"HeaderContinueWatching": "Continue Watching",
"HeaderFavoriteAlbums": "Favorite Albums",
"HeaderFavoriteArtists": "Favorite Artists",
diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json
index ac96c788c1..390074cdd7 100644
--- a/Emby.Server.Implementations/Localization/Core/es-AR.json
+++ b/Emby.Server.Implementations/Localization/Core/es-AR.json
@@ -16,7 +16,6 @@
"Folders": "Carpetas",
"Genres": "Géneros",
"HeaderAlbumArtists": "Artistas de álbum",
- "HeaderCameraUploads": "Subidas de cámara",
"HeaderContinueWatching": "Seguir viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json
index 4ba324aa1b..ab54c0ea6a 100644
--- a/Emby.Server.Implementations/Localization/Core/es-MX.json
+++ b/Emby.Server.Implementations/Localization/Core/es-MX.json
@@ -16,7 +16,6 @@
"Folders": "Carpetas",
"Genres": "Géneros",
"HeaderAlbumArtists": "Artistas del álbum",
- "HeaderCameraUploads": "Subidas desde la cámara",
"HeaderContinueWatching": "Continuar viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index e7bd3959bf..d4e0b299dd 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -16,7 +16,6 @@
"Folders": "Carpetas",
"Genres": "Géneros",
"HeaderAlbumArtists": "Artistas del álbum",
- "HeaderCameraUploads": "Subidas desde la cámara",
"HeaderContinueWatching": "Continuar viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json
index 0959ef2ca0..dcd30694f0 100644
--- a/Emby.Server.Implementations/Localization/Core/es_419.json
+++ b/Emby.Server.Implementations/Localization/Core/es_419.json
@@ -105,7 +105,6 @@
"Inherit": "Heredar",
"HomeVideos": "Videos caseros",
"HeaderRecordingGroups": "Grupos de grabación",
- "HeaderCameraUploads": "Subidas desde la cámara",
"FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}",
"DeviceOnlineWithName": "{0} está conectado",
"DeviceOfflineWithName": "{0} se ha desconectado",
diff --git a/Emby.Server.Implementations/Localization/Core/es_DO.json b/Emby.Server.Implementations/Localization/Core/es_DO.json
index 26732eb3f1..b64ffbfbba 100644
--- a/Emby.Server.Implementations/Localization/Core/es_DO.json
+++ b/Emby.Server.Implementations/Localization/Core/es_DO.json
@@ -12,7 +12,6 @@
"Application": "Aplicación",
"AppDeviceValues": "App: {0}, Dispositivo: {1}",
"HeaderContinueWatching": "Continuar Viendo",
- "HeaderCameraUploads": "Subidas de Cámara",
"HeaderAlbumArtists": "Artistas del Álbum",
"Genres": "Géneros",
"Folders": "Carpetas",
diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json
index 500c292170..1986decf03 100644
--- a/Emby.Server.Implementations/Localization/Core/fa.json
+++ b/Emby.Server.Implementations/Localization/Core/fa.json
@@ -16,7 +16,6 @@
"Folders": "پوشهها",
"Genres": "ژانرها",
"HeaderAlbumArtists": "هنرمندان آلبوم",
- "HeaderCameraUploads": "آپلودهای دوربین",
"HeaderContinueWatching": "ادامه تماشا",
"HeaderFavoriteAlbums": "آلبومهای مورد علاقه",
"HeaderFavoriteArtists": "هنرمندان مورد علاقه",
diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json
index f8d6e0e09b..8e219a9cef 100644
--- a/Emby.Server.Implementations/Localization/Core/fi.json
+++ b/Emby.Server.Implementations/Localization/Core/fi.json
@@ -1,7 +1,7 @@
{
"HeaderLiveTV": "Live-TV",
"NewVersionIsAvailable": "Uusi versio Jellyfin palvelimesta on ladattavissa.",
- "NameSeasonUnknown": "Tuntematon Kausi",
+ "NameSeasonUnknown": "Tuntematon kausi",
"NameSeasonNumber": "Kausi {0}",
"NameInstallFailed": "{0} asennus epäonnistui",
"MusicVideos": "Musiikkivideot",
@@ -19,24 +19,23 @@
"ItemAddedWithName": "{0} lisättiin kirjastoon",
"Inherit": "Periytyä",
"HomeVideos": "Kotivideot",
- "HeaderRecordingGroups": "Nauhoiteryhmät",
+ "HeaderRecordingGroups": "Tallennusryhmät",
"HeaderNextUp": "Seuraavaksi",
- "HeaderFavoriteSongs": "Lempikappaleet",
- "HeaderFavoriteShows": "Lempisarjat",
- "HeaderFavoriteEpisodes": "Lempijaksot",
- "HeaderCameraUploads": "Kamerasta Lähetetyt",
- "HeaderFavoriteArtists": "Lempiartistit",
- "HeaderFavoriteAlbums": "Lempialbumit",
+ "HeaderFavoriteSongs": "Suosikkikappaleet",
+ "HeaderFavoriteShows": "Suosikkisarjat",
+ "HeaderFavoriteEpisodes": "Suosikkijaksot",
+ "HeaderFavoriteArtists": "Suosikkiartistit",
+ "HeaderFavoriteAlbums": "Suosikkialbumit",
"HeaderContinueWatching": "Jatka katsomista",
- "HeaderAlbumArtists": "Albumin esittäjä",
+ "HeaderAlbumArtists": "Albumin artistit",
"Genres": "Tyylilajit",
"Folders": "Kansiot",
"Favorites": "Suosikit",
"FailedLoginAttemptWithUserName": "Kirjautuminen epäonnistui kohteesta {0}",
"DeviceOnlineWithName": "{0} on yhdistetty",
- "DeviceOfflineWithName": "{0} on katkaissut yhteytensä",
+ "DeviceOfflineWithName": "{0} yhteys on katkaistu",
"Collections": "Kokoelmat",
- "ChapterNameValue": "Luku: {0}",
+ "ChapterNameValue": "Jakso: {0}",
"Channels": "Kanavat",
"CameraImageUploadedFrom": "Uusi kamerakuva on ladattu {0}",
"Books": "Kirjat",
@@ -62,25 +61,25 @@
"UserPolicyUpdatedWithName": "Käyttöoikeudet päivitetty käyttäjälle {0}",
"UserPasswordChangedWithName": "Salasana vaihdettu käyttäjälle {0}",
"UserOnlineFromDevice": "{0} on paikalla osoitteesta {1}",
- "UserOfflineFromDevice": "{0} yhteys katkaistu {1}",
+ "UserOfflineFromDevice": "{0} yhteys katkaistu kohteesta {1}",
"UserLockedOutWithName": "Käyttäjä {0} lukittu",
"UserDownloadingItemWithValues": "{0} lataa {1}",
"UserDeletedWithName": "Käyttäjä {0} poistettu",
"UserCreatedWithName": "Käyttäjä {0} luotu",
- "TvShows": "TV-sarjat",
+ "TvShows": "TV-ohjelmat",
"Sync": "Synkronoi",
- "SubtitleDownloadFailureFromForItem": "Tekstitysten lataus ({0} -> {1}) epäonnistui //this string would have to be generated for each provider and movie because of finnish cases, sorry",
- "StartupEmbyServerIsLoading": "Jellyfin palvelin latautuu. Kokeile hetken kuluttua uudelleen.",
+ "SubtitleDownloadFailureFromForItem": "Tekstitystä ei voitu ladata osoitteesta {0} kohteelle {1}",
+ "StartupEmbyServerIsLoading": "Jellyfin palvelin latautuu. Yritä hetken kuluttua uudelleen.",
"Songs": "Kappaleet",
- "Shows": "Sarjat",
- "ServerNameNeedsToBeRestarted": "{0} täytyy käynnistää uudelleen",
+ "Shows": "Ohjelmat",
+ "ServerNameNeedsToBeRestarted": "{0} on käynnistettävä uudelleen",
"ProviderValue": "Tarjoaja: {0}",
"Plugin": "Liitännäinen",
"NotificationOptionVideoPlaybackStopped": "Videon toisto pysäytetty",
"NotificationOptionVideoPlayback": "Videota toistetaan",
"NotificationOptionUserLockedOut": "Käyttäjä kirjautui ulos",
"NotificationOptionTaskFailed": "Ajastettu tehtävä epäonnistui",
- "NotificationOptionServerRestartRequired": "Palvelin pitää käynnistää uudelleen",
+ "NotificationOptionServerRestartRequired": "Palvelin on käynnistettävä uudelleen",
"NotificationOptionPluginUpdateInstalled": "Liitännäinen päivitetty",
"NotificationOptionPluginUninstalled": "Liitännäinen poistettu",
"NotificationOptionPluginInstalled": "Liitännäinen asennettu",
@@ -105,10 +104,10 @@
"TaskRefreshPeople": "Päivitä henkilöt",
"TaskCleanLogsDescription": "Poistaa lokitiedostot jotka ovat yli {0} päivää vanhoja.",
"TaskCleanLogs": "Puhdista lokihakemisto",
- "TaskRefreshLibraryDescription": "Skannaa mediakirjastosi uusien tiedostojen varalle, sekä virkistää metatiedot.",
+ "TaskRefreshLibraryDescription": "Skannaa mediakirjastosi uudet tiedostot ja päivittää metatiedot.",
"TaskRefreshLibrary": "Skannaa mediakirjasto",
- "TaskRefreshChapterImagesDescription": "Luo pienoiskuvat videoille joissa on lukuja.",
- "TaskRefreshChapterImages": "Eristä lukujen kuvat",
+ "TaskRefreshChapterImagesDescription": "Luo pienoiskuvat videoille joissa on jaksoja.",
+ "TaskRefreshChapterImages": "Pura jakson kuvat",
"TaskCleanCacheDescription": "Poistaa järjestelmälle tarpeettomat väliaikaistiedostot.",
"TaskCleanCache": "Tyhjennä välimuisti-hakemisto",
"TasksChannelsCategory": "Internet kanavat",
diff --git a/Emby.Server.Implementations/Localization/Core/fil.json b/Emby.Server.Implementations/Localization/Core/fil.json
index 47daf20442..1a3e188327 100644
--- a/Emby.Server.Implementations/Localization/Core/fil.json
+++ b/Emby.Server.Implementations/Localization/Core/fil.json
@@ -73,7 +73,6 @@
"HeaderFavoriteArtists": "Paboritong Artista",
"HeaderFavoriteAlbums": "Paboritong Albums",
"HeaderContinueWatching": "Ituloy Manood",
- "HeaderCameraUploads": "Camera Uploads",
"HeaderAlbumArtists": "Artista ng Album",
"Genres": "Kategorya",
"Folders": "Folders",
diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json
index cd1c8144fd..3d7592e3c8 100644
--- a/Emby.Server.Implementations/Localization/Core/fr-CA.json
+++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json
@@ -16,7 +16,6 @@
"Folders": "Dossiers",
"Genres": "Genres",
"HeaderAlbumArtists": "Artistes de l'album",
- "HeaderCameraUploads": "Photos transférées",
"HeaderContinueWatching": "Continuer à regarder",
"HeaderFavoriteAlbums": "Albums favoris",
"HeaderFavoriteArtists": "Artistes favoris",
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index 7fc9968219..f4ca8575a1 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -16,7 +16,6 @@
"Folders": "Dossiers",
"Genres": "Genres",
"HeaderAlbumArtists": "Artistes",
- "HeaderCameraUploads": "Photos transférées",
"HeaderContinueWatching": "Continuer à regarder",
"HeaderFavoriteAlbums": "Albums favoris",
"HeaderFavoriteArtists": "Artistes préférés",
diff --git a/Emby.Server.Implementations/Localization/Core/gsw.json b/Emby.Server.Implementations/Localization/Core/gsw.json
index 8780a884ba..ee1f8775e5 100644
--- a/Emby.Server.Implementations/Localization/Core/gsw.json
+++ b/Emby.Server.Implementations/Localization/Core/gsw.json
@@ -16,7 +16,6 @@
"Folders": "Ordner",
"Genres": "Genres",
"HeaderAlbumArtists": "Album-Künstler",
- "HeaderCameraUploads": "Kamera-Uploads",
"HeaderContinueWatching": "weiter schauen",
"HeaderFavoriteAlbums": "Lieblingsalben",
"HeaderFavoriteArtists": "Lieblings-Künstler",
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index dc3a981540..f906d6e117 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -16,7 +16,6 @@
"Folders": "תיקיות",
"Genres": "ז'אנרים",
"HeaderAlbumArtists": "אמני האלבום",
- "HeaderCameraUploads": "העלאות ממצלמה",
"HeaderContinueWatching": "המשך לצפות",
"HeaderFavoriteAlbums": "אלבומים מועדפים",
"HeaderFavoriteArtists": "אמנים מועדפים",
diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json
new file mode 100644
index 0000000000..df68d3bbd4
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/hi.json
@@ -0,0 +1,3 @@
+{
+ "Albums": "आल्बुम्"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index 97c77017b5..a3c936240d 100644
--- a/Emby.Server.Implementations/Localization/Core/hr.json
+++ b/Emby.Server.Implementations/Localization/Core/hr.json
@@ -16,7 +16,6 @@
"Folders": "Mape",
"Genres": "Žanrovi",
"HeaderAlbumArtists": "Izvođači na albumu",
- "HeaderCameraUploads": "Uvoz sa kamere",
"HeaderContinueWatching": "Nastavi gledati",
"HeaderFavoriteAlbums": "Omiljeni albumi",
"HeaderFavoriteArtists": "Omiljeni izvođači",
diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json
index c5c3844e3f..343d213d41 100644
--- a/Emby.Server.Implementations/Localization/Core/hu.json
+++ b/Emby.Server.Implementations/Localization/Core/hu.json
@@ -16,7 +16,6 @@
"Folders": "Könyvtárak",
"Genres": "Műfajok",
"HeaderAlbumArtists": "Album előadók",
- "HeaderCameraUploads": "Kamera feltöltések",
"HeaderContinueWatching": "Megtekintés folytatása",
"HeaderFavoriteAlbums": "Kedvenc albumok",
"HeaderFavoriteArtists": "Kedvenc előadók",
diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json
index 585fc6f027..ef3ed2580a 100644
--- a/Emby.Server.Implementations/Localization/Core/id.json
+++ b/Emby.Server.Implementations/Localization/Core/id.json
@@ -20,7 +20,6 @@
"HeaderFavoriteArtists": "Artis Favorit",
"HeaderFavoriteAlbums": "Album Favorit",
"HeaderContinueWatching": "Lanjut Menonton",
- "HeaderCameraUploads": "Unggahan Kamera",
"HeaderAlbumArtists": "Album Artis",
"Genres": "Aliran",
"Folders": "Folder",
diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json
index 0f0f9130b0..0f769eaada 100644
--- a/Emby.Server.Implementations/Localization/Core/is.json
+++ b/Emby.Server.Implementations/Localization/Core/is.json
@@ -13,7 +13,6 @@
"HeaderFavoriteArtists": "Uppáhalds Listamenn",
"HeaderFavoriteAlbums": "Uppáhalds Plötur",
"HeaderContinueWatching": "Halda áfram að horfa",
- "HeaderCameraUploads": "Myndavéla upphal",
"HeaderAlbumArtists": "Höfundur plötu",
"Genres": "Tegundir",
"Folders": "Möppur",
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index bf1a0ef136..0a6238578d 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -16,7 +16,6 @@
"Folders": "Cartelle",
"Genres": "Generi",
"HeaderAlbumArtists": "Artisti degli Album",
- "HeaderCameraUploads": "Caricamenti Fotocamera",
"HeaderContinueWatching": "Continua a guardare",
"HeaderFavoriteAlbums": "Album Preferiti",
"HeaderFavoriteArtists": "Artisti Preferiti",
diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json
index a4d9f9ef6b..35004f0eb4 100644
--- a/Emby.Server.Implementations/Localization/Core/ja.json
+++ b/Emby.Server.Implementations/Localization/Core/ja.json
@@ -16,7 +16,6 @@
"Folders": "フォルダー",
"Genres": "ジャンル",
"HeaderAlbumArtists": "アルバムアーティスト",
- "HeaderCameraUploads": "カメラアップロード",
"HeaderContinueWatching": "視聴を続ける",
"HeaderFavoriteAlbums": "お気に入りのアルバム",
"HeaderFavoriteArtists": "お気に入りのアーティスト",
diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json
index 5618ff4a8f..91c1fb15bc 100644
--- a/Emby.Server.Implementations/Localization/Core/kk.json
+++ b/Emby.Server.Implementations/Localization/Core/kk.json
@@ -16,7 +16,6 @@
"Folders": "Qaltalar",
"Genres": "Janrlar",
"HeaderAlbumArtists": "Álbom oryndaýshylary",
- "HeaderCameraUploads": "Kameradan júktelgender",
"HeaderContinueWatching": "Qaraýdy jalǵastyrý",
"HeaderFavoriteAlbums": "Tańdaýly álbomdar",
"HeaderFavoriteArtists": "Tańdaýly oryndaýshylar",
diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json
index a33953c273..fb01e46451 100644
--- a/Emby.Server.Implementations/Localization/Core/ko.json
+++ b/Emby.Server.Implementations/Localization/Core/ko.json
@@ -16,7 +16,6 @@
"Folders": "폴더",
"Genres": "장르",
"HeaderAlbumArtists": "앨범 아티스트",
- "HeaderCameraUploads": "카메라 업로드",
"HeaderContinueWatching": "계속 시청하기",
"HeaderFavoriteAlbums": "즐겨찾는 앨범",
"HeaderFavoriteArtists": "즐겨찾는 아티스트",
diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json
index 35053766b4..d4cb592efc 100644
--- a/Emby.Server.Implementations/Localization/Core/lt-LT.json
+++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json
@@ -16,7 +16,6 @@
"Folders": "Katalogai",
"Genres": "Žanrai",
"HeaderAlbumArtists": "Albumo atlikėjai",
- "HeaderCameraUploads": "Kameros",
"HeaderContinueWatching": "Žiūrėti toliau",
"HeaderFavoriteAlbums": "Mėgstami Albumai",
"HeaderFavoriteArtists": "Mėgstami Atlikėjai",
diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json
index dbcf172871..5e3acfbe96 100644
--- a/Emby.Server.Implementations/Localization/Core/lv.json
+++ b/Emby.Server.Implementations/Localization/Core/lv.json
@@ -72,7 +72,6 @@
"ItemAddedWithName": "{0} tika pievienots bibliotēkai",
"HeaderLiveTV": "Tiešraides TV",
"HeaderContinueWatching": "Turpināt Skatīšanos",
- "HeaderCameraUploads": "Kameras augšupielādes",
"HeaderAlbumArtists": "Albumu Izpildītāji",
"Genres": "Žanri",
"Folders": "Mapes",
diff --git a/Emby.Server.Implementations/Localization/Core/mk.json b/Emby.Server.Implementations/Localization/Core/mk.json
index bbdf99abab..b780ef498a 100644
--- a/Emby.Server.Implementations/Localization/Core/mk.json
+++ b/Emby.Server.Implementations/Localization/Core/mk.json
@@ -51,7 +51,6 @@
"HeaderFavoriteArtists": "Омилени Изведувачи",
"HeaderFavoriteAlbums": "Омилени Албуми",
"HeaderContinueWatching": "Продолжи со гледање",
- "HeaderCameraUploads": "Поставувања од камера",
"HeaderAlbumArtists": "Изведувачи од Албуми",
"Genres": "Жанрови",
"Folders": "Папки",
diff --git a/Emby.Server.Implementations/Localization/Core/mr.json b/Emby.Server.Implementations/Localization/Core/mr.json
index b6db2b0f29..fdb4171b53 100644
--- a/Emby.Server.Implementations/Localization/Core/mr.json
+++ b/Emby.Server.Implementations/Localization/Core/mr.json
@@ -54,7 +54,6 @@
"ItemAddedWithName": "{0} हे संग्रहालयात जोडले गेले",
"HomeVideos": "घरचे व्हिडीयो",
"HeaderRecordingGroups": "रेकॉर्डिंग गट",
- "HeaderCameraUploads": "कॅमेरा अपलोड",
"CameraImageUploadedFrom": "एक नवीन कॅमेरा चित्र {0} येथून अपलोड केले आहे",
"Application": "अॅप्लिकेशन",
"AppDeviceValues": "अॅप: {0}, यंत्र: {1}",
diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json
index 7f8df12895..5e3d095ff1 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -16,7 +16,6 @@
"Folders": "Fail-fail",
"Genres": "Genre-genre",
"HeaderAlbumArtists": "Album Artis-artis",
- "HeaderCameraUploads": "Muatnaik Kamera",
"HeaderContinueWatching": "Terus Menonton",
"HeaderFavoriteAlbums": "Album-album Kegemaran",
"HeaderFavoriteArtists": "Artis-artis Kegemaran",
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index 07a5991211..245c3cd636 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -16,7 +16,6 @@
"Folders": "Mapper",
"Genres": "Sjangre",
"HeaderAlbumArtists": "Albumartister",
- "HeaderCameraUploads": "Kameraopplastinger",
"HeaderContinueWatching": "Fortsett å se",
"HeaderFavoriteAlbums": "Favorittalbum",
"HeaderFavoriteArtists": "Favorittartister",
diff --git a/Emby.Server.Implementations/Localization/Core/ne.json b/Emby.Server.Implementations/Localization/Core/ne.json
index 38c0737098..8e820d40c7 100644
--- a/Emby.Server.Implementations/Localization/Core/ne.json
+++ b/Emby.Server.Implementations/Localization/Core/ne.json
@@ -41,7 +41,6 @@
"HeaderFavoriteArtists": "मनपर्ने कलाकारहरू",
"HeaderFavoriteAlbums": "मनपर्ने एल्बमहरू",
"HeaderContinueWatching": "हेर्न जारी राख्नुहोस्",
- "HeaderCameraUploads": "क्यामेरा अपलोडहरू",
"HeaderAlbumArtists": "एल्बमका कलाकारहरू",
"Genres": "विधाहरू",
"Folders": "फोल्डरहरू",
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index 41c74d54de..e102b92b90 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -16,7 +16,6 @@
"Folders": "Mappen",
"Genres": "Genres",
"HeaderAlbumArtists": "Albumartiesten",
- "HeaderCameraUploads": "Camera-uploads",
"HeaderContinueWatching": "Kijken hervatten",
"HeaderFavoriteAlbums": "Favoriete albums",
"HeaderFavoriteArtists": "Favoriete artiesten",
diff --git a/Emby.Server.Implementations/Localization/Core/nn.json b/Emby.Server.Implementations/Localization/Core/nn.json
index fb6e81beb3..6236515b2c 100644
--- a/Emby.Server.Implementations/Localization/Core/nn.json
+++ b/Emby.Server.Implementations/Localization/Core/nn.json
@@ -19,7 +19,6 @@
"HeaderFavoriteArtists": "Favoritt Artistar",
"HeaderFavoriteAlbums": "Favoritt Album",
"HeaderContinueWatching": "Fortsett å sjå",
- "HeaderCameraUploads": "Kamera Opplastingar",
"HeaderAlbumArtists": "Album Artist",
"Genres": "Sjangrar",
"Folders": "Mapper",
diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json
index bdc0d0169b..003e591b37 100644
--- a/Emby.Server.Implementations/Localization/Core/pl.json
+++ b/Emby.Server.Implementations/Localization/Core/pl.json
@@ -16,7 +16,6 @@
"Folders": "Foldery",
"Genres": "Gatunki",
"HeaderAlbumArtists": "Wykonawcy albumów",
- "HeaderCameraUploads": "Przekazane obrazy",
"HeaderContinueWatching": "Kontynuuj odtwarzanie",
"HeaderFavoriteAlbums": "Ulubione albumy",
"HeaderFavoriteArtists": "Ulubieni wykonawcy",
diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json
index 275195640b..5e49ca702e 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-BR.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json
@@ -16,7 +16,6 @@
"Folders": "Pastas",
"Genres": "Gêneros",
"HeaderAlbumArtists": "Artistas do Álbum",
- "HeaderCameraUploads": "Envios da Câmera",
"HeaderContinueWatching": "Continuar Assistindo",
"HeaderFavoriteAlbums": "Álbuns Favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json
index c1fb65743d..90a4941c56 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-PT.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json
@@ -16,7 +16,6 @@
"Folders": "Pastas",
"Genres": "Géneros",
"HeaderAlbumArtists": "Artistas do Álbum",
- "HeaderCameraUploads": "Envios a partir da câmara",
"HeaderContinueWatching": "Continuar a Ver",
"HeaderFavoriteAlbums": "Álbuns Favoritos",
"HeaderFavoriteArtists": "Artistas Favoritos",
@@ -26,7 +25,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/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json
index b534d0bbeb..2079940cd1 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -83,7 +83,6 @@
"Playlists": "Listas de Reprodução",
"Photos": "Fotografias",
"Movies": "Filmes",
- "HeaderCameraUploads": "Carregamentos a partir da câmara",
"FailedLoginAttemptWithUserName": "Tentativa de ligação falhada a partir de {0}",
"DeviceOnlineWithName": "{0} está connectado",
"DeviceOfflineWithName": "{0} desconectou-se",
diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json
index 699dd26daa..bc008df3b4 100644
--- a/Emby.Server.Implementations/Localization/Core/ro.json
+++ b/Emby.Server.Implementations/Localization/Core/ro.json
@@ -74,7 +74,6 @@
"HeaderFavoriteArtists": "Artiști Favoriți",
"HeaderFavoriteAlbums": "Albume Favorite",
"HeaderContinueWatching": "Vizionează în continuare",
- "HeaderCameraUploads": "Incărcări Cameră Foto",
"HeaderAlbumArtists": "Album Artiști",
"Genres": "Genuri",
"Folders": "Dosare",
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index 648aa384b0..95b93afb82 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -16,7 +16,6 @@
"Folders": "Папки",
"Genres": "Жанры",
"HeaderAlbumArtists": "Исполнители альбома",
- "HeaderCameraUploads": "Камеры",
"HeaderContinueWatching": "Продолжение просмотра",
"HeaderFavoriteAlbums": "Избранные альбомы",
"HeaderFavoriteArtists": "Избранные исполнители",
diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json
index 0ee652637c..8e50269444 100644
--- a/Emby.Server.Implementations/Localization/Core/sk.json
+++ b/Emby.Server.Implementations/Localization/Core/sk.json
@@ -16,7 +16,6 @@
"Folders": "Priečinky",
"Genres": "Žánre",
"HeaderAlbumArtists": "Umelci albumu",
- "HeaderCameraUploads": "Nahrané fotografie",
"HeaderContinueWatching": "Pokračovať v pozeraní",
"HeaderFavoriteAlbums": "Obľúbené albumy",
"HeaderFavoriteArtists": "Obľúbení umelci",
diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json
index 329c562e73..ff4b9e84fd 100644
--- a/Emby.Server.Implementations/Localization/Core/sl-SI.json
+++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json
@@ -16,7 +16,6 @@
"Folders": "Mape",
"Genres": "Zvrsti",
"HeaderAlbumArtists": "Izvajalci albuma",
- "HeaderCameraUploads": "Posnetki kamere",
"HeaderContinueWatching": "Nadaljuj gledanje",
"HeaderFavoriteAlbums": "Priljubljeni albumi",
"HeaderFavoriteArtists": "Priljubljeni izvajalci",
diff --git a/Emby.Server.Implementations/Localization/Core/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json
index 347ba5f970..0d909b06e9 100644
--- a/Emby.Server.Implementations/Localization/Core/sq.json
+++ b/Emby.Server.Implementations/Localization/Core/sq.json
@@ -96,7 +96,6 @@
"HeaderFavoriteArtists": "Artistët e preferuar",
"HeaderFavoriteAlbums": "Albumet e preferuar",
"HeaderContinueWatching": "Vazhdo të shikosh",
- "HeaderCameraUploads": "Ngarkimet nga Kamera",
"HeaderAlbumArtists": "Artistët e albumeve",
"Genres": "Zhanre",
"Folders": "Dosje",
@@ -113,5 +112,5 @@
"Artists": "Artistë",
"Application": "Aplikacioni",
"AppDeviceValues": "Aplikacioni: {0}, Pajisja: {1}",
- "Albums": "Albumet"
+ "Albums": "Albume"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json
index 5f3cbb1c8b..2b1eccfaf3 100644
--- a/Emby.Server.Implementations/Localization/Core/sr.json
+++ b/Emby.Server.Implementations/Localization/Core/sr.json
@@ -74,7 +74,6 @@
"HeaderFavoriteArtists": "Омиљени извођачи",
"HeaderFavoriteAlbums": "Омиљени албуми",
"HeaderContinueWatching": "Настави гледање",
- "HeaderCameraUploads": "Слања са камере",
"HeaderAlbumArtists": "Извођачи албума",
"Genres": "Жанрови",
"Folders": "Фасцикле",
diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json
index c8662b2cab..bea294ba2e 100644
--- a/Emby.Server.Implementations/Localization/Core/sv.json
+++ b/Emby.Server.Implementations/Localization/Core/sv.json
@@ -9,14 +9,13 @@
"Channels": "Kanaler",
"ChapterNameValue": "Kapitel {0}",
"Collections": "Samlingar",
- "DeviceOfflineWithName": "{0} har kopplat från",
+ "DeviceOfflineWithName": "{0} har kopplat ner",
"DeviceOnlineWithName": "{0} är ansluten",
"FailedLoginAttemptWithUserName": "Misslyckat inloggningsförsök från {0}",
"Favorites": "Favoriter",
"Folders": "Mappar",
"Genres": "Genrer",
"HeaderAlbumArtists": "Albumartister",
- "HeaderCameraUploads": "Kamerauppladdningar",
"HeaderContinueWatching": "Fortsätt kolla",
"HeaderFavoriteAlbums": "Favoritalbum",
"HeaderFavoriteArtists": "Favoritartister",
diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json
index 810b1b9abe..ae38f45e1c 100644
--- a/Emby.Server.Implementations/Localization/Core/ta.json
+++ b/Emby.Server.Implementations/Localization/Core/ta.json
@@ -20,7 +20,6 @@
"MessageApplicationUpdated": "ஜெல்லிஃபின் சேவையகம் புதுப்பிக்கப்பட்டது",
"Inherit": "மரபுரிமையாகப் பெறு",
"HeaderRecordingGroups": "பதிவு குழுக்கள்",
- "HeaderCameraUploads": "புகைப்பட பதிவேற்றங்கள்",
"Folders": "கோப்புறைகள்",
"FailedLoginAttemptWithUserName": "{0} இலிருந்து உள்நுழைவு முயற்சி தோல்வியடைந்தது",
"DeviceOnlineWithName": "{0} இணைக்கப்பட்டது",
diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json
index 3b77215a30..71dd2c7a30 100644
--- a/Emby.Server.Implementations/Localization/Core/th.json
+++ b/Emby.Server.Implementations/Localization/Core/th.json
@@ -50,7 +50,6 @@
"HeaderFavoriteArtists": "ศิลปินที่ชื่นชอบ",
"HeaderFavoriteAlbums": "อัมบั้มที่ชื่นชอบ",
"HeaderContinueWatching": "ดูต่อ",
- "HeaderCameraUploads": "อัปโหลดรูปถ่าย",
"HeaderAlbumArtists": "อัลบั้มศิลปิน",
"Genres": "ประเภท",
"Folders": "โฟลเดอร์",
diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json
index 3cf3482eba..1e5f2cf198 100644
--- a/Emby.Server.Implementations/Localization/Core/tr.json
+++ b/Emby.Server.Implementations/Localization/Core/tr.json
@@ -16,7 +16,6 @@
"Folders": "Klasörler",
"Genres": "Türler",
"HeaderAlbumArtists": "Albüm Sanatçıları",
- "HeaderCameraUploads": "Kamera Yüklemeleri",
"HeaderContinueWatching": "İzlemeye Devam Et",
"HeaderFavoriteAlbums": "Favori Albümler",
"HeaderFavoriteArtists": "Favori Sanatçılar",
diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
index e673465a42..06cc5f633e 100644
--- a/Emby.Server.Implementations/Localization/Core/uk.json
+++ b/Emby.Server.Implementations/Localization/Core/uk.json
@@ -16,7 +16,6 @@
"HeaderFavoriteArtists": "Улюблені виконавці",
"HeaderFavoriteAlbums": "Улюблені альбоми",
"HeaderContinueWatching": "Продовжити перегляд",
- "HeaderCameraUploads": "Завантажено з камери",
"HeaderAlbumArtists": "Виконавці альбому",
"Genres": "Жанри",
"Folders": "Каталоги",
diff --git a/Emby.Server.Implementations/Localization/Core/ur_PK.json b/Emby.Server.Implementations/Localization/Core/ur_PK.json
index 9a5874e295..fa7b2d4d0c 100644
--- a/Emby.Server.Implementations/Localization/Core/ur_PK.json
+++ b/Emby.Server.Implementations/Localization/Core/ur_PK.json
@@ -105,7 +105,6 @@
"Inherit": "وراثت میں",
"HomeVideos": "ہوم ویڈیو",
"HeaderRecordingGroups": "ریکارڈنگ گروپس",
- "HeaderCameraUploads": "کیمرہ اپلوڈز",
"FailedLoginAttemptWithUserName": "لاگن کئ کوشش ناکام {0}",
"DeviceOnlineWithName": "{0} متصل ھو چکا ھے",
"DeviceOfflineWithName": "{0} منقطع ھو چکا ھے",
diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json
index 2392c83479..ac74deff82 100644
--- a/Emby.Server.Implementations/Localization/Core/vi.json
+++ b/Emby.Server.Implementations/Localization/Core/vi.json
@@ -103,7 +103,6 @@
"HeaderFavoriteEpisodes": "Tập Phim Yêu Thích",
"HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích",
"HeaderFavoriteAlbums": "Album Ưa Thích",
- "HeaderCameraUploads": "Máy Ảnh Tải Lên",
"FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập thất bại từ {0}",
"DeviceOnlineWithName": "{0} đã kết nối",
"DeviceOfflineWithName": "{0} đã ngắt kết nối",
diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json
index 6b563a9b13..53a902de28 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-CN.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json
@@ -16,7 +16,6 @@
"Folders": "文件夹",
"Genres": "风格",
"HeaderAlbumArtists": "专辑作家",
- "HeaderCameraUploads": "相机上传",
"HeaderContinueWatching": "继续观影",
"HeaderFavoriteAlbums": "收藏的专辑",
"HeaderFavoriteArtists": "最爱的艺术家",
diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json
index 1ac62baca9..435e294ef2 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-HK.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json
@@ -16,7 +16,6 @@
"Folders": "檔案夾",
"Genres": "風格",
"HeaderAlbumArtists": "專輯藝人",
- "HeaderCameraUploads": "相機上載",
"HeaderContinueWatching": "繼續觀看",
"HeaderFavoriteAlbums": "最愛專輯",
"HeaderFavoriteArtists": "最愛的藝人",
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index 7b6540c3e3..30f7266300 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -16,7 +16,6 @@
"Folders": "資料夾",
"Genres": "風格",
"HeaderAlbumArtists": "專輯演出者",
- "HeaderCameraUploads": "相機上傳",
"HeaderContinueWatching": "繼續觀賞",
"HeaderFavoriteAlbums": "最愛專輯",
"HeaderFavoriteArtists": "最愛演出者",
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
index 5adcefc1fe..692d1667d2 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
@@ -71,7 +71,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
return new[]
{
// Every so often
- new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
+ new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
};
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
index 54e18eaea0..184d155d44 100644
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
@@ -65,7 +65,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{
return new[]
{
- new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
+ new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
};
}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs
index 6914081673..26ef193542 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;
///
- public bool IsEnabled => false;
+ public bool IsEnabled => true;
///
public bool IsLogged => true;
diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs
index 29393ae07d..4bc12f44a4 100644
--- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs
+++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs
@@ -54,7 +54,8 @@ namespace Emby.Server.Implementations.Security
{
if (tableNewlyCreated && TableExists(connection, "AccessTokens"))
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
var existingColumnNames = GetColumnNames(db, "AccessTokens");
@@ -88,7 +89,8 @@ namespace Emby.Server.Implementations.Security
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
using (var statement = db.PrepareStatement("insert into Tokens (AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity) values (@AccessToken, @DeviceId, @AppName, @AppVersion, @DeviceName, @UserId, @UserName, @IsActive, @DateCreated, @DateLastActivity)"))
{
@@ -119,7 +121,8 @@ namespace Emby.Server.Implementations.Security
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
using (var statement = db.PrepareStatement("Update Tokens set AccessToken=@AccessToken, DeviceId=@DeviceId, AppName=@AppName, AppVersion=@AppVersion, DeviceName=@DeviceName, UserId=@UserId, UserName=@UserName, DateCreated=@DateCreated, DateLastActivity=@DateLastActivity where Id=@Id"))
{
@@ -151,7 +154,8 @@ namespace Emby.Server.Implementations.Security
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
using (var statement = db.PrepareStatement("Delete from Tokens where Id=@Id"))
{
@@ -346,7 +350,8 @@ namespace Emby.Server.Implementations.Security
{
using (var connection = GetConnection(true))
{
- return connection.RunInTransaction(db =>
+ return connection.RunInTransaction(
+ db =>
{
using (var statement = base.PrepareStatement(db, "select CustomName from Devices where Id=@DeviceId"))
{
@@ -377,7 +382,8 @@ namespace Emby.Server.Implementations.Security
using (var connection = GetConnection())
{
- connection.RunInTransaction(db =>
+ connection.RunInTransaction(
+ db =>
{
using (var statement = db.PrepareStatement("replace into devices (Id, CustomName, Capabilities) VALUES (@Id, @CustomName, (Select Capabilities from Devices where Id=@Id))"))
{
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index e42d478533..607b322f2e 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -666,7 +666,7 @@ namespace Emby.Server.Implementations.Session
}
}
- var eventArgs = new PlaybackProgressEventArgs
+ var eventArgs = new PlaybackStartEventArgs
{
Item = libraryItem,
Users = users,
@@ -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(SessionInfo session, string name, T data, CancellationToken cancellationToken)
+ private static async Task SendMessageToSession(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(IEnumerable sessions, string name, T data, CancellationToken cancellationToken)
+ private static Task SendMessageToSessions(IEnumerable sessions, SessionMessageType name, T data, CancellationToken cancellationToken)
{
IEnumerable 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);
}
///
@@ -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);
}
///
@@ -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 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);
}
///
@@ -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);
}
///
@@ -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);
}
///
@@ -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
}
///
- public Task SendMessageToAdminSessions(string name, T data, CancellationToken cancellationToken)
+ public Task SendMessageToAdminSessions(SessionMessageType name, T data, CancellationToken cancellationToken)
{
CheckDisposed();
@@ -1879,7 +1887,7 @@ namespace Emby.Server.Implementations.Session
}
///
- public Task SendMessageToUserSessions(List userIds, string name, Func dataFn, CancellationToken cancellationToken)
+ public Task SendMessageToUserSessions(List userIds, SessionMessageType name, Func dataFn, CancellationToken cancellationToken)
{
CheckDisposed();
@@ -1894,7 +1902,7 @@ namespace Emby.Server.Implementations.Session
}
///
- public Task SendMessageToUserSessions(List userIds, string name, T data, CancellationToken cancellationToken)
+ public Task SendMessageToUserSessions(List userIds, SessionMessageType name, T data, CancellationToken cancellationToken)
{
CheckDisposed();
@@ -1903,7 +1911,7 @@ namespace Emby.Server.Implementations.Session
}
///
- public Task SendMessageToUserDeviceSessions(string deviceId, string name, T data, CancellationToken cancellationToken)
+ public Task SendMessageToUserDeviceSessions(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 15c2af220d..a5f8479537 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
{
- 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 94604ca1e0..b986ffa1cd 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
///
public Task SendMessage(
- 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 80b977731c..5384795122 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);
}
///
diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs
index a07cea9c0c..b429cebecb 100644
--- a/Jellyfin.Api/Controllers/ActivityLogController.cs
+++ b/Jellyfin.Api/Controllers/ActivityLogController.cs
@@ -1,7 +1,7 @@
using System;
-using System.Linq;
+using System.Threading.Tasks;
using Jellyfin.Api.Constants;
-using Jellyfin.Data.Entities;
+using Jellyfin.Data.Queries;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
@@ -39,19 +39,19 @@ namespace Jellyfin.Api.Controllers
/// A containing the log entries.
[HttpGet("Entries")]
[ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult> GetLogEntries(
+ public async Task>> GetLogEntries(
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] DateTime? minDate,
[FromQuery] bool? hasUserId)
{
- var filterFunc = new Func, IQueryable>(
- entries => entries.Where(entry => entry.DateCreated >= minDate
- && (!hasUserId.HasValue || (hasUserId.Value
- ? entry.UserId != Guid.Empty
- : entry.UserId == Guid.Empty))));
-
- return _activityManager.GetPagedResult(filterFunc, startIndex, limit);
+ return await _activityManager.GetPagedResultAsync(new ActivityLogQuery
+ {
+ StartIndex = startIndex,
+ Limit = limit,
+ MinDate = minDate,
+ HasUserId = hasUserId
+ }).ConfigureAwait(false);
}
}
}
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index d38214116c..784ecaeb32 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -54,7 +54,7 @@ namespace Jellyfin.Api.Controllers
/// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.
/// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.
/// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.
- /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.
+ /// Optional. Specify additional filters to apply.
/// Optional filter by items that are marked as favorite, or not.
/// Optional filter by MediaType. Allows multiple, comma delimited.
/// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.
@@ -89,7 +89,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] string? filters,
+ [FromQuery] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes,
[FromQuery] string? genres,
@@ -188,7 +188,7 @@ namespace Jellyfin.Api.Controllers
}).Where(i => i != null).Select(i => i!.Id).ToArray();
}
- foreach (var filter in RequestHelpers.GetFilters(filters))
+ foreach (var filter in filters)
{
switch (filter)
{
@@ -263,7 +263,7 @@ namespace Jellyfin.Api.Controllers
/// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.
/// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.
/// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.
- /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.
+ /// Optional. Specify additional filters to apply.
/// Optional filter by items that are marked as favorite, or not.
/// Optional filter by MediaType. Allows multiple, comma delimited.
/// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.
@@ -298,7 +298,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] string? filters,
+ [FromQuery] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes,
[FromQuery] string? genres,
@@ -397,7 +397,7 @@ namespace Jellyfin.Api.Controllers
}).Where(i => i != null).Select(i => i!.Id).ToArray();
}
- foreach (var filter in RequestHelpers.GetFilters(filters))
+ foreach (var filter in filters)
{
switch (filter)
{
diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index 33a969f859..fda00b8d46 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -105,7 +105,7 @@ namespace Jellyfin.Api.Controllers
/// Optional. The record index to start at. All items with a lower index will be dropped from the results.
/// Optional. The maximum number of records to return.
/// Optional. Sort Order - Ascending,Descending.
- /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.
+ /// Optional. Specify additional filters to apply.
/// Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.
/// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.
/// Channel items returned.
@@ -121,7 +121,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? sortOrder,
- [FromQuery] string? filters,
+ [FromQuery] ItemFilter[] filters,
[FromQuery] string? sortBy,
[FromQuery] string? fields)
{
@@ -140,7 +140,7 @@ namespace Jellyfin.Api.Controllers
.AddItemFields(fields)
};
- foreach (var filter in RequestHelpers.GetFilters(filters))
+ foreach (var filter in filters)
{
switch (filter)
{
@@ -183,7 +183,7 @@ namespace Jellyfin.Api.Controllers
/// Optional. User Id.
/// Optional. The record index to start at. All items with a lower index will be dropped from the results.
/// Optional. The maximum number of records to return.
- /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.
+ /// Optional. Specify additional filters to apply.
/// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.
/// Optional. Specify one or more channel id's, comma delimited.
/// Latest channel items returned.
@@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] Guid? userId,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
- [FromQuery] string? filters,
+ [FromQuery] ItemFilter[] filters,
[FromQuery] string? fields,
[FromQuery] string? channelIds)
{
@@ -217,7 +217,7 @@ namespace Jellyfin.Api.Controllers
.AddItemFields(fields)
};
- foreach (var filter in RequestHelpers.GetFilters(filters))
+ foreach (var filter in filters)
{
switch (filter)
{
diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs
index de6aa86c94..f4b69b312e 100644
--- a/Jellyfin.Api/Controllers/GenresController.cs
+++ b/Jellyfin.Api/Controllers/GenresController.cs
@@ -55,7 +55,7 @@ namespace Jellyfin.Api.Controllers
/// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.
/// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.
/// Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.
- /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.
+ /// Optional. Specify additional filters to apply.
/// Optional filter by items that are marked as favorite, or not.
/// Optional filter by MediaType. Allows multiple, comma delimited.
/// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.
@@ -90,7 +90,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] string? filters,
+ [FromQuery] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes,
[FromQuery] string? genres,
@@ -188,7 +188,7 @@ namespace Jellyfin.Api.Controllers
.ToArray();
}
- foreach (var filter in RequestHelpers.GetFilters(filters))
+ foreach (var filter in filters)
{
switch (filter)
{
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 7afec1219e..05efe23553 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -333,7 +333,7 @@ namespace Jellyfin.Api.Controllers
/// Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.
/// Optional. Supply the cache tag from the item object to receive strong caching headers.
/// Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.
- /// Determines the output format of the image - original,gif,jpg,png.
+ /// Optional. The of the returned image.
/// Optional. Add a played indicator.
/// Optional. Percent to render for the percent played overlay.
/// Optional. Unplayed count overlay to render.
@@ -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 requestAcceptTypes, string acceptParam, string format, bool acceptAll)
+ private bool SupportsFormat(IReadOnlyCollection 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 GetImageResult(
diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs
index 652c4689d0..b50b1e836b 100644
--- a/Jellyfin.Api/Controllers/ItemsController.cs
+++ b/Jellyfin.Api/Controllers/ItemsController.cs
@@ -159,7 +159,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isHd,
[FromQuery] bool? is4K,
[FromQuery] string? locationTypes,
- [FromQuery] string? excludeLocationTypes,
+ [FromQuery] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired,
[FromQuery] double? minCommunityRating,
@@ -182,7 +182,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] string? filters,
+ [FromQuery] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes,
[FromQuery] string? imageTypes,
@@ -365,7 +365,7 @@ namespace Jellyfin.Api.Controllers
query.CollapseBoxSetItems = false;
}
- foreach (var filter in RequestHelpers.GetFilters(filters!))
+ foreach (var filter in filters)
{
switch (filter)
{
@@ -406,12 +406,9 @@ namespace Jellyfin.Api.Controllers
}
// ExcludeLocationTypes
- if (!string.IsNullOrEmpty(excludeLocationTypes))
+ if (excludeLocationTypes.Any(t => t == LocationType.Virtual))
{
- if (excludeLocationTypes.Split(',').Select(d => (LocationType)Enum.Parse(typeof(LocationType), d, true)).ToArray().Contains(LocationType.Virtual))
- {
- query.IsVirtualItem = false;
- }
+ query.IsVirtualItem = false;
}
if (!string.IsNullOrEmpty(locationTypes))
diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs
index 570ae8fdc7..d216b7f728 100644
--- a/Jellyfin.Api/Controllers/MusicGenresController.cs
+++ b/Jellyfin.Api/Controllers/MusicGenresController.cs
@@ -55,7 +55,7 @@ namespace Jellyfin.Api.Controllers
/// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.
/// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.
/// Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.
- /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.
+ /// Optional. Specify additional filters to apply.
/// Optional filter by items that are marked as favorite, or not.
/// Optional filter by MediaType. Allows multiple, comma delimited.
/// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.
@@ -89,7 +89,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] string? filters,
+ [FromQuery] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes,
[FromQuery] string? genres,
@@ -187,7 +187,7 @@ namespace Jellyfin.Api.Controllers
.ToArray();
}
- foreach (var filter in RequestHelpers.GetFilters(filters))
+ foreach (var filter in filters)
{
switch (filter)
{
diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs
index 8bd610dad9..bbef1ff9a1 100644
--- a/Jellyfin.Api/Controllers/PersonsController.cs
+++ b/Jellyfin.Api/Controllers/PersonsController.cs
@@ -89,7 +89,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] string? filters,
+ [FromQuery] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes,
[FromQuery] string? genres,
@@ -187,7 +187,7 @@ namespace Jellyfin.Api.Controllers
.ToArray();
}
- foreach (var filter in RequestHelpers.GetFilters(filters))
+ foreach (var filter in filters)
{
switch (filter)
{
diff --git a/Jellyfin.Api/Controllers/ScheduledTasksController.cs b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
index ab7920895b..68e4f0586f 100644
--- a/Jellyfin.Api/Controllers/ScheduledTasksController.cs
+++ b/Jellyfin.Api/Controllers/ScheduledTasksController.cs
@@ -36,7 +36,7 @@ namespace Jellyfin.Api.Controllers
/// The list of scheduled tasks.
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
- public IEnumerable GetTasks(
+ public IEnumerable GetTasks(
[FromQuery] bool? isHidden,
[FromQuery] bool? isEnabled)
{
@@ -57,7 +57,7 @@ namespace Jellyfin.Api.Controllers
}
}
- yield return task;
+ yield return ScheduledTaskHelpers.GetTaskInfo(task);
}
}
diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs
index 39bf6e6dc7..5656709629 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/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs
index cdd5f958e9..3a8ac1b243 100644
--- a/Jellyfin.Api/Controllers/StudiosController.cs
+++ b/Jellyfin.Api/Controllers/StudiosController.cs
@@ -53,7 +53,7 @@ namespace Jellyfin.Api.Controllers
/// Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.
/// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.
/// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.
- /// Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.
+ /// Optional. Specify additional filters to apply.
/// Optional filter by items that are marked as favorite, or not.
/// Optional filter by MediaType. Allows multiple, comma delimited.
/// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.
@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
- [FromQuery] string? filters,
+ [FromQuery] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes,
[FromQuery] string? genres,
@@ -188,7 +188,7 @@ namespace Jellyfin.Api.Controllers
.ToArray();
}
- foreach (var filter in RequestHelpers.GetFilters(filters))
+ foreach (var filter in filters)
{
switch (filter)
{
diff --git a/Jellyfin.Api/Controllers/TrailersController.cs b/Jellyfin.Api/Controllers/TrailersController.cs
index 5157b08ae5..dec4498575 100644
--- a/Jellyfin.Api/Controllers/TrailersController.cs
+++ b/Jellyfin.Api/Controllers/TrailersController.cs
@@ -1,6 +1,7 @@
using System;
using Jellyfin.Api.Constants;
using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@@ -124,7 +125,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? isHd,
[FromQuery] bool? is4K,
[FromQuery] string? locationTypes,
- [FromQuery] string? excludeLocationTypes,
+ [FromQuery] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired,
[FromQuery] double? minCommunityRating,
@@ -146,7 +147,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? parentId,
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
- [FromQuery] string? filters,
+ [FromQuery] ItemFilter[] filters,
[FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes,
[FromQuery] string? imageTypes,
diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs
index 1207fb5134..e78f63b256 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/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index 8dcf08af56..fb5c283f59 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -56,18 +56,6 @@ namespace Jellyfin.Api.Helpers
return result;
}
- ///
- /// Get parsed filters.
- ///
- /// The filters.
- /// Item filters.
- public static IEnumerable GetFilters(string? filters)
- {
- return string.IsNullOrEmpty(filters)
- ? Array.Empty()
- : filters.Split(',').Select(v => Enum.Parse(v, true));
- }
-
///
/// Splits a string at a separating character into an array of substrings.
///
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index cc9dcd10e1..1cecbeb888 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -740,10 +740,7 @@ namespace Jellyfin.Api.Helpers
/// The state.
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/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj
index 6a00db4b1a..da0852cebc 100644
--- a/Jellyfin.Api/Jellyfin.Api.csproj
+++ b/Jellyfin.Api/Jellyfin.Api.csproj
@@ -14,9 +14,9 @@
-
+
-
+
diff --git a/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs
new file mode 100644
index 0000000000..208566dc8e
--- /dev/null
+++ b/Jellyfin.Api/ModelBinders/CommaDelimitedArrayModelBinder.cs
@@ -0,0 +1,59 @@
+using System;
+using System.ComponentModel;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+
+namespace Jellyfin.Api.ModelBinders
+{
+ ///
+ /// Comma delimited array model binder.
+ /// Returns an empty array of specified type if there is no query parameter.
+ ///
+ public class CommaDelimitedArrayModelBinder : IModelBinder
+ {
+ ///
+ public Task BindModelAsync(ModelBindingContext bindingContext)
+ {
+ var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
+ var elementType = bindingContext.ModelType.GetElementType();
+ var converter = TypeDescriptor.GetConverter(elementType);
+
+ 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 0000000000..b9785a73b8
--- /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
+{
+ ///
+ /// Comma delimited array model binder provider.
+ ///
+ public class CommaDelimitedArrayModelBinderProvider : IModelBinderProvider
+ {
+ private readonly IModelBinder _binder;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CommaDelimitedArrayModelBinderProvider()
+ {
+ _binder = new CommaDelimitedArrayModelBinder();
+ }
+
+ ///
+ 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 849b3b7095..77d55828d1 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;
}
- ///
- /// Gets the name.
- ///
- /// The name.
- protected override string Name => "ActivityLogEntry";
+ ///
+ protected override SessionMessageType Type => SessionMessageType.ActivityLogEntry;
+
+ ///
+ protected override SessionMessageType StartType => SessionMessageType.ActivityLogEntryStart;
+
+ ///
+ protected override SessionMessageType StopType => SessionMessageType.ActivityLogEntryStop;
///
/// Gets the data to send.
diff --git a/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/ScheduledTasksWebSocketListener.cs
index 8a966c1376..80314b9236 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;
}
- ///
- /// Gets the name.
- ///
- /// The name.
- protected override string Name => "ScheduledTasksInfo";
+ ///
+ protected override SessionMessageType Type => SessionMessageType.ScheduledTasksInfo;
+
+ ///
+ protected override SessionMessageType StartType => SessionMessageType.ScheduledTasksInfoStart;
+
+ ///
+ protected override SessionMessageType StopType => SessionMessageType.ScheduledTasksInfoStop;
///
/// Gets the data to send.
diff --git a/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs b/Jellyfin.Api/WebSocketListeners/SessionInfoWebSocketListener.cs
index 1fb5dc412c..1cf43a0053 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
}
///
- protected override string Name => "Sessions";
+ protected override SessionMessageType Type => SessionMessageType.Sessions;
+
+ ///
+ protected override SessionMessageType StartType => SessionMessageType.SessionsStart;
+
+ ///
+ protected override SessionMessageType StopType => SessionMessageType.SessionsStop;
///
/// Gets the data to send.
diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs
index f7ab57a1b1..6d46819143 100644
--- a/Jellyfin.Data/Entities/User.cs
+++ b/Jellyfin.Data/Entities/User.cs
@@ -188,6 +188,11 @@ namespace Jellyfin.Data.Entities
///
public int? LoginAttemptsBeforeLockout { get; set; }
+ ///
+ /// Gets or sets the maximum number of active sessions the user can have at once.
+ ///
+ public int MaxActiveSessions { get; set; }
+
///
/// Gets or sets the subtitle mode.
///
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 6bb0d8ce27..5038988f96 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -41,8 +41,8 @@
-
-
+
+
diff --git a/Jellyfin.Data/Queries/ActivityLogQuery.cs b/Jellyfin.Data/Queries/ActivityLogQuery.cs
new file mode 100644
index 0000000000..92919d3a51
--- /dev/null
+++ b/Jellyfin.Data/Queries/ActivityLogQuery.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace Jellyfin.Data.Queries
+{
+ ///
+ /// A class representing a query to the activity logs.
+ ///
+ public class ActivityLogQuery
+ {
+ ///
+ /// Gets or sets the index to start at.
+ ///
+ public int? StartIndex { get; set; }
+
+ ///
+ /// Gets or sets the maximum number of items to include.
+ ///
+ public int? Limit { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to take entries with a user id.
+ ///
+ public bool? HasUserId { get; set; }
+
+ ///
+ /// Gets or sets the minimum date to query for.
+ ///
+ public DateTime? MinDate { get; set; }
+ }
+}
diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index a1caa751b1..6a9dbdae42 100644
--- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -553,13 +553,13 @@ namespace Jellyfin.Drawing.Skia
}
///
- public void CreateImageCollage(ImageCollageOptions options)
+ public void CreateImageCollage(ImageCollageOptions options, string? libraryName)
{
double ratio = (double)options.Width / options.Height;
if (ratio >= 1.4)
{
- new StripCollageBuilder(this).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height);
+ new StripCollageBuilder(this).BuildThumbCollage(options.InputPaths, options.OutputPath, options.Width, options.Height, libraryName);
}
else if (ratio >= .9)
{
diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
index 10bb59648f..0e94f87f6a 100644
--- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
+++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
@@ -82,48 +82,62 @@ namespace Jellyfin.Drawing.Skia
/// The path at which to place the resulting image.
/// The desired width of the collage.
/// The desired height of the collage.
- public void BuildThumbCollage(string[] paths, string outputPath, int width, int height)
+ /// The name of the library to draw on the collage.
+ public void BuildThumbCollage(string[] paths, string outputPath, int width, int height, string? libraryName)
{
- using var bitmap = BuildThumbCollageBitmap(paths, width, height);
+ using var bitmap = BuildThumbCollageBitmap(paths, width, height, libraryName);
using var outputStream = new SKFileWStream(outputPath);
using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels());
pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90);
}
- private SKBitmap BuildThumbCollageBitmap(string[] paths, int width, int height)
+ private SKBitmap BuildThumbCollageBitmap(string[] paths, int width, int height, string? libraryName)
{
var bitmap = new SKBitmap(width, height);
using var canvas = new SKCanvas(bitmap);
canvas.Clear(SKColors.Black);
- // number of images used in the thumbnail
- var iCount = 3;
-
- // determine sizes for each image that will composited into the final image
- var iSlice = Convert.ToInt32(width / iCount);
- int iHeight = Convert.ToInt32(height * 1.00);
- int imageIndex = 0;
- for (int i = 0; i < iCount; i++)
+ using var backdrop = GetNextValidImage(paths, 0, out _);
+ if (backdrop == null)
{
- using var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex);
- imageIndex = newIndex;
- if (currentBitmap == null)
- {
- continue;
- }
-
- // resize to the same aspect as the original
- int iWidth = Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height);
- using var resizedImage = SkiaEncoder.ResizeImage(currentBitmap, new SKImageInfo(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace));
-
- // crop image
- int ix = Math.Abs((iWidth - iSlice) / 2);
- using var subset = resizedImage.Subset(SKRectI.Create(ix, 0, iSlice, iHeight));
- // draw image onto canvas
- canvas.DrawImage(subset ?? resizedImage, iSlice * i, 0);
+ return bitmap;
}
+ // resize to the same aspect as the original
+ var backdropHeight = Math.Abs(width * backdrop.Height / backdrop.Width);
+ using var residedBackdrop = SkiaEncoder.ResizeImage(backdrop, new SKImageInfo(width, backdropHeight, backdrop.ColorType, backdrop.AlphaType, backdrop.ColorSpace));
+ // draw the backdrop
+ canvas.DrawImage(residedBackdrop, 0, 0);
+
+ // draw shadow rectangle
+ var paintColor = new SKPaint
+ {
+ Color = SKColors.Black.WithAlpha(0x78),
+ Style = SKPaintStyle.Fill
+ };
+ canvas.DrawRect(0, 0, width, height, paintColor);
+
+ // draw library name
+ var textPaint = new SKPaint
+ {
+ Color = SKColors.White,
+ Style = SKPaintStyle.Fill,
+ TextSize = 112,
+ TextAlign = SKTextAlign.Center,
+ Typeface = SKTypeface.FromFamilyName("sans-serif", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright),
+ IsAntialias = true
+ };
+
+ // scale down text to 90% of the width if text is larger than 95% of the width
+ var textWidth = textPaint.MeasureText(libraryName);
+ if (textWidth > width * 0.95)
+ {
+ textPaint.TextSize = 0.9f * width * textPaint.TextSize / textWidth;
+ }
+
+ canvas.DrawText(libraryName, width / 2f, (height / 2f) + (textPaint.FontMetrics.XHeight / 2), textPaint);
+
return bitmap;
}
diff --git a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
index abdd290d45..5926abfe0d 100644
--- a/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
+++ b/Jellyfin.Server.Implementations/Activity/ActivityManager.cs
@@ -3,8 +3,10 @@ using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Events;
+using Jellyfin.Data.Queries;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Querying;
+using Microsoft.EntityFrameworkCore;
namespace Jellyfin.Server.Implementations.Activity
{
@@ -39,41 +41,37 @@ namespace Jellyfin.Server.Implementations.Activity
}
///
- public QueryResult GetPagedResult(
- Func, IQueryable> func,
- int? startIndex,
- int? limit)
+ public async Task> GetPagedResultAsync(ActivityLogQuery query)
{
- using var dbContext = _provider.CreateContext();
+ await using var dbContext = _provider.CreateContext();
- var query = func(dbContext.ActivityLogs.OrderByDescending(entry => entry.DateCreated));
+ IQueryable entries = dbContext.ActivityLogs
+ .AsQueryable()
+ .OrderByDescending(entry => entry.DateCreated);
- if (startIndex.HasValue)
+ if (query.MinDate.HasValue)
{
- query = query.Skip(startIndex.Value);
+ entries = entries.Where(entry => entry.DateCreated >= query.MinDate);
}
- if (limit.HasValue)
+ if (query.HasUserId.HasValue)
{
- query = query.Take(limit.Value);
+ entries = entries.Where(entry => entry.UserId != Guid.Empty == query.HasUserId.Value );
}
- // This converts the objects from the new database model to the old for compatibility with the existing API.
- var list = query.Select(ConvertToOldModel).ToList();
-
return new QueryResult
{
- Items = list,
- TotalRecordCount = func(dbContext.ActivityLogs).Count()
+ Items = await entries
+ .Skip(query.StartIndex ?? 0)
+ .Take(query.Limit ?? 100)
+ .AsAsyncEnumerable()
+ .Select(ConvertToOldModel)
+ .ToListAsync()
+ .ConfigureAwait(false),
+ TotalRecordCount = await entries.CountAsync().ConfigureAwait(false)
};
}
- ///
- public QueryResult GetPagedResult(int? startIndex, int? limit)
- {
- return GetPagedResult(logs => logs, startIndex, limit);
- }
-
private static ActivityLogEntry ConvertToOldModel(ActivityLog entry)
{
return new ActivityLogEntry
diff --git a/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedNotifier.cs b/Jellyfin.Server.Implementations/Events/Consumers/System/TaskCompletedNotifier.cs
index 80ed56cd8f..0993c6df7b 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
///
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 1c600683a9..1d790da6b2 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
///
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 ea0c878d42..a1faf18fc4 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
///
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 3dda5a04c4..bd1a714045 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
///
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 f691d11a7d..b513ac64ae 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
///
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 709692f6bb..1fd7b9adfc 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
///
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 10367a939b..303e886210 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 { 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 6081dd044f..a14911b94a 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 { e.Argument.Id },
- "UserUpdated",
+ SessionMessageType.UserUpdated,
_userManager.GetUserDto(e.Argument),
CancellationToken.None).ConfigureAwait(false);
}
diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
index 4e79dd8d6c..c52be3b8a9 100644
--- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
+++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
@@ -24,11 +24,12 @@
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
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 0000000000..e5c326a326
--- /dev/null
+++ b/Jellyfin.Server.Implementations/Migrations/20201004171403_AddMaxActiveSessions.Designer.cs
@@ -0,0 +1,464 @@
+#pragma warning disable CS1591
+
+//
+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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DayOfWeek")
+ .HasColumnType("INTEGER");
+
+ b.Property("EndHour")
+ .HasColumnType("REAL");
+
+ b.Property("StartHour")
+ .HasColumnType("REAL");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AccessSchedules");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DateCreated")
+ .HasColumnType("TEXT");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT")
+ .HasMaxLength(256);
+
+ b.Property("LogSeverity")
+ .HasColumnType("INTEGER");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(512);
+
+ b.Property("Overview")
+ .HasColumnType("TEXT")
+ .HasMaxLength(512);
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("ShortOverview")
+ .HasColumnType("TEXT")
+ .HasMaxLength(512);
+
+ b.Property("Type")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(256);
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.ToTable("ActivityLogs");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("ChromecastVersion")
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(32);
+
+ b.Property("DashboardTheme")
+ .HasColumnType("TEXT")
+ .HasMaxLength(32);
+
+ b.Property("EnableNextVideoInfoOverlay")
+ .HasColumnType("INTEGER");
+
+ b.Property("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("ScrollDirection")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowBackdrop")
+ .HasColumnType("INTEGER");
+
+ b.Property("ShowSidebar")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkipBackwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property("SkipForwardLength")
+ .HasColumnType("INTEGER");
+
+ b.Property("TvHome")
+ .HasColumnType("TEXT")
+ .HasMaxLength(32);
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("DisplayPreferencesId")
+ .HasColumnType("INTEGER");
+
+ b.Property("Order")
+ .HasColumnType("INTEGER");
+
+ b.Property("Type")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DisplayPreferencesId");
+
+ b.ToTable("HomeSection");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("LastModified")
+ .HasColumnType("TEXT");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(512);
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("ImageInfos");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Client")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(32);
+
+ b.Property("IndexBy")
+ .HasColumnType("INTEGER");
+
+ b.Property("ItemId")
+ .HasColumnType("TEXT");
+
+ b.Property("RememberIndexing")
+ .HasColumnType("INTEGER");
+
+ b.Property("RememberSorting")
+ .HasColumnType("INTEGER");
+
+ b.Property("SortBy")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(64);
+
+ b.Property("SortOrder")
+ .HasColumnType("INTEGER");
+
+ b.Property("UserId")
+ .HasColumnType("TEXT");
+
+ b.Property("ViewType")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ItemDisplayPreferences");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property("Permission_Permissions_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("Value")
+ .HasColumnType("INTEGER");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Permission_Permissions_Guid");
+
+ b.ToTable("Permissions");
+ });
+
+ modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER");
+
+ b.Property("Kind")
+ .HasColumnType("INTEGER");
+
+ b.Property("Preference_Preferences_Guid")
+ .HasColumnType("TEXT");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT");
+
+ b.Property("AudioLanguagePreference")
+ .HasColumnType("TEXT")
+ .HasMaxLength(255);
+
+ b.Property("AuthenticationProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(255);
+
+ b.Property("DisplayCollectionsView")
+ .HasColumnType("INTEGER");
+
+ b.Property("DisplayMissingEpisodes")
+ .HasColumnType("INTEGER");
+
+ b.Property("EasyPassword")
+ .HasColumnType("TEXT")
+ .HasMaxLength(65535);
+
+ b.Property("EnableAutoLogin")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableLocalPassword")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableNextEpisodeAutoPlay")
+ .HasColumnType("INTEGER");
+
+ b.Property("EnableUserPreferenceAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property("HidePlayedInLatest")
+ .HasColumnType("INTEGER");
+
+ b.Property("InternalId")
+ .HasColumnType("INTEGER");
+
+ b.Property("InvalidLoginAttemptCount")
+ .HasColumnType("INTEGER");
+
+ b.Property("LastActivityDate")
+ .HasColumnType("TEXT");
+
+ b.Property("LastLoginDate")
+ .HasColumnType("TEXT");
+
+ b.Property("LoginAttemptsBeforeLockout")
+ .HasColumnType("INTEGER");
+
+ b.Property("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
+ b.Property("MaxParentalAgeRating")
+ .HasColumnType("INTEGER");
+
+ b.Property("MustUpdatePassword")
+ .HasColumnType("INTEGER");
+
+ b.Property("Password")
+ .HasColumnType("TEXT")
+ .HasMaxLength(65535);
+
+ b.Property("PasswordResetProviderId")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasMaxLength(255);
+
+ b.Property("PlayDefaultAudioTrack")
+ .HasColumnType("INTEGER");
+
+ b.Property("RememberAudioSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property("RememberSubtitleSelections")
+ .HasColumnType("INTEGER");
+
+ b.Property("RemoteClientBitrateLimit")
+ .HasColumnType("INTEGER");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("INTEGER");
+
+ b.Property("SubtitleLanguagePreference")
+ .HasColumnType("TEXT")
+ .HasMaxLength(255);
+
+ b.Property("SubtitleMode")
+ .HasColumnType("INTEGER");
+
+ b.Property("SyncPlayAccess")
+ .HasColumnType("INTEGER");
+
+ b.Property("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 0000000000..10acb4defb
--- /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(
+ 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 ccfcf96b1f..16d62f4826 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("LoginAttemptsBeforeLockout")
.HasColumnType("INTEGER");
+ b.Property("MaxActiveSessions")
+ .HasColumnType("INTEGER");
+
b.Property("MaxParentalAgeRating")
.HasColumnType("INTEGER");
diff --git a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
index 46f1c618f2..76f9433854 100644
--- a/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
+++ b/Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
@@ -61,6 +61,7 @@ namespace Jellyfin.Server.Implementations.Users
public IList ListItemDisplayPreferences(Guid userId, string client)
{
return _dbContext.ItemDisplayPreferences
+ .AsQueryable()
.Where(prefs => prefs.UserId == userId && prefs.ItemId != Guid.Empty && string.Equals(prefs.Client, client))
.ToList();
}
diff --git a/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs b/Jellyfin.Server.Implementations/Users/InvalidAuthProvider.cs
index e38cd07f0e..5f32479e1d 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";
///
- public bool IsEnabled => true;
+ public bool IsEnabled => false;
///
public Task Authenticate(string username, string password)
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 8f04baa089..437833aa38 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -108,6 +108,7 @@ namespace Jellyfin.Server.Implementations.Users
{
using var dbContext = _dbProvider.CreateContext();
return dbContext.Users
+ .AsQueryable()
.Select(user => user.Id)
.ToList();
}
@@ -200,8 +201,8 @@ namespace Jellyfin.Server.Implementations.Users
internal async Task CreateUserInternalAsync(string name, JellyfinDb dbContext)
{
// TODO: Remove after user item data is migrated.
- var max = await dbContext.Users.AnyAsync().ConfigureAwait(false)
- ? await dbContext.Users.Select(u => u.InternalId).MaxAsync().ConfigureAwait(false)
+ var max = await dbContext.Users.AsQueryable().AnyAsync().ConfigureAwait(false)
+ ? await dbContext.Users.AsQueryable().Select(u => u.InternalId).MaxAsync().ConfigureAwait(false)
: 0;
return new User(
@@ -221,7 +222,7 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
}
- using var dbContext = _dbProvider.CreateContext();
+ await using var dbContext = _dbProvider.CreateContext();
var newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
@@ -379,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),
@@ -587,9 +589,9 @@ namespace Jellyfin.Server.Implementations.Users
public async Task InitializeAsync()
{
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
- using var dbContext = _dbProvider.CreateContext();
+ await using var dbContext = _dbProvider.CreateContext();
- if (await dbContext.Users.AnyAsync().ConfigureAwait(false))
+ if (await dbContext.Users.AsQueryable().AnyAsync().ConfigureAwait(false))
{
return;
}
@@ -701,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);
@@ -799,7 +802,7 @@ namespace Jellyfin.Server.Implementations.Users
private IList 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/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index 8d569a779a..c44736447f 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -4,6 +4,8 @@ using System.IO;
using System.Reflection;
using Emby.Drawing;
using Emby.Server.Implementations;
+using Emby.Server.Implementations.Session;
+using Jellyfin.Api.WebSocketListeners;
using Jellyfin.Drawing.Skia;
using Jellyfin.Server.Implementations;
using Jellyfin.Server.Implementations.Activity;
@@ -14,6 +16,7 @@ using MediaBrowser.Controller;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.IO;
using Microsoft.EntityFrameworkCore;
@@ -80,6 +83,14 @@ namespace Jellyfin.Server
ServiceCollection.AddSingleton();
ServiceCollection.AddSingleton();
+ ServiceCollection.AddScoped();
+ ServiceCollection.AddScoped();
+ ServiceCollection.AddScoped();
+ ServiceCollection.AddScoped();
+
+ // TODO fix circular dependency on IWebSocketManager
+ ServiceCollection.AddScoped(serviceProvider => new Lazy>(serviceProvider.GetRequiredService>));
+
base.RegisterServices();
}
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 5bcf6d5f07..d7b9da5c2f 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();
+ c.DocumentFilter();
});
}
diff --git a/Jellyfin.Server/Filters/WebsocketModelFilter.cs b/Jellyfin.Server/Filters/WebsocketModelFilter.cs
new file mode 100644
index 0000000000..2488028576
--- /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
+{
+ ///
+ /// Add models used in websocket messaging.
+ ///
+ public class WebsocketModelFilter : IDocumentFilter
+ {
+ ///
+ 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
- /// The struct type.
- public class JsonNullableStructConverter : JsonConverter
- where T : struct
+ /// The struct type.
+ public class JsonNullableStructConverter : JsonConverter
+ where TStruct : struct
{
- private readonly JsonConverter _baseJsonConverter;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The base json converter.
- public JsonNullableStructConverter(JsonConverter baseJsonConverter)
- {
- _baseJsonConverter = baseJsonConverter;
- }
-
///
- public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ public override TStruct? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
- // Handle empty string.
+ if (reader.TokenType == JsonTokenType.Null)
+ {
+ return null;
+ }
+
+ // Token is empty string.
if (reader.TokenType == JsonTokenType.String && ((reader.HasValueSequence && reader.ValueSequence.IsEmpty) || reader.ValueSpan.IsEmpty))
{
return null;
}
- return _baseJsonConverter.Read(ref reader, typeToConvert, options);
+ return JsonSerializer.Deserialize(ref reader, options);
}
///
- public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
+ public override void Write(Utf8JsonWriter writer, TStruct? value, JsonSerializerOptions options)
{
- _baseJsonConverter.Write(writer, value, options);
+ if (value.HasValue)
+ {
+ JsonSerializer.Serialize(writer, value.Value, options);
+ }
+ else
+ {
+ writer.WriteNullValue();
+ }
}
}
}
diff --git a/MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs
new file mode 100644
index 0000000000..d5b54e3ca8
--- /dev/null
+++ b/MediaBrowser.Common/Json/Converters/JsonNullableStructConverterFactory.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MediaBrowser.Common.Json.Converters
+{
+ ///
+ /// Json nullable struct converter factory.
+ ///
+ public class JsonNullableStructConverterFactory : JsonConverterFactory
+ {
+ ///
+ public override bool CanConvert(Type typeToConvert)
+ {
+ return typeToConvert.IsGenericType
+ && typeToConvert.GetGenericTypeDefinition() == typeof(Nullable<>)
+ && typeToConvert.GenericTypeArguments[0].IsValueType;
+ }
+
+ ///
+ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
+ {
+ var structType = typeToConvert.GenericTypeArguments[0];
+ return (JsonConverter)Activator.CreateInstance(typeof(JsonNullableStructConverter<>).MakeGenericType(structType));
+ }
+ }
+}
\ No newline at end of file
diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs
index 67f7e8f14a..6605ae9624 100644
--- a/MediaBrowser.Common/Json/JsonDefaults.cs
+++ b/MediaBrowser.Common/Json/JsonDefaults.cs
@@ -39,14 +39,9 @@ namespace MediaBrowser.Common.Json
NumberHandling = JsonNumberHandling.AllowReadingFromString
};
- // Get built-in converters for fallback converting.
- var baseNullableInt32Converter = (JsonConverter)options.GetConverter(typeof(int?));
- var baseNullableInt64Converter = (JsonConverter)options.GetConverter(typeof(long?));
-
options.Converters.Add(new JsonGuidConverter());
options.Converters.Add(new JsonStringEnumConverter());
- options.Converters.Add(new JsonNullableStructConverter(baseNullableInt32Converter));
- options.Converters.Add(new JsonNullableStructConverter(baseNullableInt64Converter));
+ options.Converters.Add(new JsonNullableStructConverterFactory());
return options;
}
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index 322740cca8..e716a6610f 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -18,8 +18,8 @@
-
-
+
+
diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs
index 4b2918d085..8545fd5dcf 100644
--- a/MediaBrowser.Common/Plugins/BasePlugin.cs
+++ b/MediaBrowser.Common/Plugins/BasePlugin.cs
@@ -3,6 +3,7 @@
using System;
using System.IO;
using System.Reflection;
+using System.Runtime.InteropServices;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
@@ -140,6 +141,30 @@ namespace MediaBrowser.Common.Plugins
{
ApplicationPaths = applicationPaths;
XmlSerializer = xmlSerializer;
+ if (this is IPluginAssembly assemblyPlugin)
+ {
+ var assembly = GetType().Assembly;
+ var assemblyName = assembly.GetName();
+ var assemblyFilePath = assembly.Location;
+
+ var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath));
+
+ assemblyPlugin.SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version);
+
+ var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true);
+ if (idAttributes.Length > 0)
+ {
+ var attribute = (GuidAttribute)idAttributes[0];
+ var assemblyId = new Guid(attribute.Value);
+
+ assemblyPlugin.SetId(assemblyId);
+ }
+ }
+
+ if (this is IHasPluginConfiguration hasPluginConfiguration)
+ {
+ hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s));
+ }
}
///
diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
index f9b2e6fef3..770c6dc2d2 100644
--- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs
+++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#nullable enable
using System;
using System.Collections.Generic;
@@ -63,6 +64,7 @@ namespace MediaBrowser.Controller.Drawing
/// Create an image collage.
///
/// The options to use when creating the collage.
- void CreateImageCollage(ImageCollageOptions options);
+ /// Optional.
+ void CreateImageCollage(ImageCollageOptions options, string? libraryName);
}
}
diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
index b7edb10524..935a790312 100644
--- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs
+++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
@@ -1,4 +1,5 @@
#pragma warning disable CS1591
+#nullable enable
using System;
using System.Collections.Generic;
@@ -75,7 +76,7 @@ namespace MediaBrowser.Controller.Drawing
///
/// The options.
/// Task.
- Task<(string path, string mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options);
+ Task<(string path, string? mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options);
///
/// Gets the supported image output formats.
@@ -87,7 +88,8 @@ namespace MediaBrowser.Controller.Drawing
/// Creates the image collage.
///
/// The options.
- void CreateImageCollage(ImageCollageOptions options);
+ /// The library name to draw onto the collage.
+ void CreateImageCollage(ImageCollageOptions options, string? libraryName);
bool SupportsTransparency(string path);
}
diff --git a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
index 8a69971d0f..c65477d39a 100644
--- a/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
+++ b/MediaBrowser.Controller/Entities/BaseItemExtensions.cs
@@ -45,7 +45,8 @@ namespace MediaBrowser.Controller.Entities
{
if (file.StartsWith("http", System.StringComparison.OrdinalIgnoreCase))
{
- item.SetImage(new ItemImageInfo
+ item.SetImage(
+ new ItemImageInfo
{
Path = file,
Type = imageType
diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs
index 901ea875bc..35ddaffadc 100644
--- a/MediaBrowser.Controller/Entities/Folder.cs
+++ b/MediaBrowser.Controller/Entities/Folder.cs
@@ -255,7 +255,8 @@ namespace MediaBrowser.Controller.Entities
var id = child.Id;
if (dictionary.ContainsKey(id))
{
- Logger.LogError("Found folder containing items with duplicate id. Path: {path}, Child Name: {ChildName}",
+ Logger.LogError(
+ "Found folder containing items with duplicate id. Path: {path}, Child Name: {ChildName}",
Path ?? Name,
child.Path ?? child.Name);
}
@@ -984,7 +985,8 @@ namespace MediaBrowser.Controller.Entities
return items;
}
- private static bool CollapseBoxSetItems(InternalItemsQuery query,
+ private static bool CollapseBoxSetItems(
+ InternalItemsQuery query,
BaseItem queryParent,
User user,
IServerConfigurationManager configurationManager)
@@ -1593,7 +1595,8 @@ namespace MediaBrowser.Controller.Entities
/// The date played.
/// if set to true [reset position].
/// Task.
- public override void MarkPlayed(User user,
+ public override void MarkPlayed(
+ User user,
DateTime? datePlayed,
bool resetPosition)
{
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index 068a767698..7bb311900e 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -343,7 +343,8 @@ namespace MediaBrowser.Controller.Entities
{
var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.TvShows, string.Empty });
- var result = _tvSeriesManager.GetNextUp(new NextUpQuery
+ var result = _tvSeriesManager.GetNextUp(
+ new NextUpQuery
{
Limit = query.Limit,
StartIndex = query.StartIndex,
@@ -443,7 +444,8 @@ namespace MediaBrowser.Controller.Entities
return Filter(item, query.User, query, BaseItem.UserDataManager, BaseItem.LibraryManager);
}
- public static QueryResult PostFilterAndSort(IEnumerable items,
+ public static QueryResult PostFilterAndSort(
+ IEnumerable items,
BaseItem queryParent,
int? totalRecordLimit,
InternalItemsQuery query,
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index 332730bcca..32703c2fd1 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -566,7 +566,8 @@ namespace MediaBrowser.Controller.Library
int GetCount(InternalItemsQuery query);
- void AddExternalSubtitleStreams(List streams,
+ void AddExternalSubtitleStreams(
+ List streams,
string videoPath,
string[] files);
}
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index 6544704065..4374317d67 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -14,8 +14,8 @@
-
-
+
+
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index cf9bf0b5e8..33f2dcc6a0 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -70,15 +70,15 @@ namespace MediaBrowser.Controller.MediaEncoding
var codecMap = new Dictionary(StringComparer.OrdinalIgnoreCase)
{
- {"qsv", hwEncoder + "_qsv"},
- {hwEncoder + "_qsv", hwEncoder + "_qsv"},
- {"nvenc", hwEncoder + "_nvenc"},
- {"amf", hwEncoder + "_amf"},
- {"omx", hwEncoder + "_omx"},
- {hwEncoder + "_v4l2m2m", hwEncoder + "_v4l2m2m"},
- {"mediacodec", hwEncoder + "_mediacodec"},
- {"vaapi", hwEncoder + "_vaapi"},
- {"videotoolbox", hwEncoder + "_videotoolbox"}
+ { "qsv", hwEncoder + "_qsv" },
+ { hwEncoder + "_qsv", hwEncoder + "_qsv" },
+ { "nvenc", hwEncoder + "_nvenc" },
+ { "amf", hwEncoder + "_amf" },
+ { "omx", hwEncoder + "_omx" },
+ { hwEncoder + "_v4l2m2m", hwEncoder + "_v4l2m2m" },
+ { "mediacodec", hwEncoder + "_mediacodec" },
+ { "vaapi", hwEncoder + "_vaapi" },
+ { "videotoolbox", hwEncoder + "_videotoolbox" }
};
if (!string.IsNullOrEmpty(hwType)
@@ -435,11 +435,13 @@ namespace MediaBrowser.Controller.MediaEncoding
var arg = new StringBuilder();
var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty;
var outputVideoCodec = GetVideoEncoder(state, encodingOptions) ?? string.Empty;
+ var isSwDecoder = string.IsNullOrEmpty(videoDecoder);
+ var isD3d11vaDecoder = videoDecoder.IndexOf("d3d11va", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiEncoder = outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
var isQsvDecoder = videoDecoder.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1;
var isQsvEncoder = outputVideoCodec.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1;
- var isNvencHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
+ var isNvdecHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
var isMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
@@ -501,11 +503,12 @@ namespace MediaBrowser.Controller.MediaEncoding
}
if (state.IsVideoRequest
- && string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase))
+ && (string.Equals(encodingOptions.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && isNvdecHevcDecoder || isSwDecoder)
+ || (string.Equals(encodingOptions.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) && isD3d11vaDecoder || isSwDecoder))
{
var isColorDepth10 = IsColorDepth10(state);
- if (isNvencHevcDecoder && isColorDepth10
+ if (isColorDepth10
&& _mediaEncoder.SupportsHwaccel("opencl")
&& encodingOptions.EnableTonemapping
&& !string.IsNullOrEmpty(state.VideoStream.VideoRange)
@@ -864,6 +867,19 @@ namespace MediaBrowser.Controller.MediaEncoding
param += "-quality speed";
break;
}
+
+ var videoStream = state.VideoStream;
+ var isColorDepth10 = IsColorDepth10(state);
+
+ if (isColorDepth10
+ && _mediaEncoder.SupportsHwaccel("opencl")
+ && encodingOptions.EnableTonemapping
+ && !string.IsNullOrEmpty(videoStream.VideoRange)
+ && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase))
+ {
+ // Enhance workload when tone mapping with AMF on some APUs
+ param += " -preanalysis true";
+ }
}
else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // webm
{
@@ -1007,19 +1023,19 @@ namespace MediaBrowser.Controller.MediaEncoding
&& !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
{
param = "-pix_fmt yuv420p " + param;
}
- if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase))
{
- var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty;
var videoStream = state.VideoStream;
var isColorDepth10 = IsColorDepth10(state);
- if (videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1
- && isColorDepth10
+ if (isColorDepth10
&& _mediaEncoder.SupportsHwaccel("opencl")
&& encodingOptions.EnableTonemapping
&& !string.IsNullOrEmpty(videoStream.VideoRange)
@@ -1635,47 +1651,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var outputSizeParam = ReadOnlySpan.Empty;
var request = state.BaseRequest;
- outputSizeParam = GetOutputSizeParam(state, options, outputVideoCodec).TrimEnd('"');
-
- // All possible beginning of video filters
- // Don't break the order
- string[] beginOfOutputSizeParam = new[]
- {
- // for tonemap_opencl
- "hwupload,tonemap_opencl",
-
- // hwupload=extra_hw_frames=64,vpp_qsv (for overlay_qsv on linux)
- "hwupload=extra_hw_frames",
-
- // vpp_qsv
- "vpp",
-
- // hwdownload,format=p010le (hardware decode + software encode for vaapi)
- "hwdownload",
-
- // format=nv12|vaapi,hwupload,scale_vaapi
- "format",
-
- // bwdif,scale=expr
- "bwdif",
-
- // yadif,scale=expr
- "yadif",
-
- // scale=expr
- "scale"
- };
-
- var index = -1;
- foreach (var param in beginOfOutputSizeParam)
- {
- index = outputSizeParam.IndexOf(param, StringComparison.OrdinalIgnoreCase);
- if (index != -1)
- {
- outputSizeParam = outputSizeParam.Slice(index);
- break;
- }
- }
+ outputSizeParam = GetOutputSizeParamInternal(state, options, outputVideoCodec);
var videoSizeParam = string.Empty;
var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options) ?? string.Empty;
@@ -1806,7 +1782,8 @@ namespace MediaBrowser.Controller.MediaEncoding
return (Convert.ToInt32(outputWidth), Convert.ToInt32(outputHeight));
}
- public List GetScalingFilters(EncodingJobInfo state,
+ public List GetScalingFilters(
+ EncodingJobInfo state,
int? videoWidth,
int? videoHeight,
Video3DFormat? threedFormat,
@@ -2066,10 +2043,19 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Format(CultureInfo.InvariantCulture, filter, widthParam, heightParam);
}
+ public string GetOutputSizeParam(
+ EncodingJobInfo state,
+ EncodingOptions options,
+ string outputVideoCodec)
+ {
+ string filters = GetOutputSizeParamInternal(state, options, outputVideoCodec);
+ return string.IsNullOrEmpty(filters) ? string.Empty : " -vf \"" + filters + "\"";
+ }
+
///
/// If we're going to put a fixed size on the command line, this will calculate it.
///
- public string GetOutputSizeParam(
+ public string GetOutputSizeParamInternal(
EncodingJobInfo state,
EncodingOptions options,
string outputVideoCodec)
@@ -2085,6 +2071,8 @@ namespace MediaBrowser.Controller.MediaEncoding
var inputHeight = videoStream?.Height;
var threeDFormat = state.MediaSource.Video3DFormat;
+ var isSwDecoder = string.IsNullOrEmpty(videoDecoder);
+ var isD3d11vaDecoder = videoDecoder.IndexOf("d3d11va", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1;
var isQsvH264Encoder = outputVideoCodec.IndexOf("h264_qsv", StringComparison.OrdinalIgnoreCase) != -1;
@@ -2100,47 +2088,77 @@ namespace MediaBrowser.Controller.MediaEncoding
// If double rate deinterlacing is enabled and the input framerate is 30fps or below, otherwise the output framerate will be too high for many devices
var doubleRateDeinterlace = options.DeinterlaceDoubleRate && (videoStream?.RealFrameRate ?? 60) <= 30;
- // Currently only with the use of NVENC decoder can we get a decent performance.
- // Currently only the HEVC/H265 format is supported.
- // NVIDIA Pascal and Turing or higher are recommended.
- if (isNvdecHevcDecoder && isColorDepth10
- && _mediaEncoder.SupportsHwaccel("opencl")
- && options.EnableTonemapping
- && !string.IsNullOrEmpty(videoStream.VideoRange)
- && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase))
+ var isScalingInAdvance = false;
+ var isDeinterlaceH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
+ var isDeinterlaceHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
+
+ if ((string.Equals(options.HardwareAccelerationType, "nvenc", StringComparison.OrdinalIgnoreCase) && isNvdecHevcDecoder || isSwDecoder)
+ || (string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase) && isD3d11vaDecoder || isSwDecoder))
{
- var parameters = "tonemap_opencl=format=nv12:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:desat={1}:threshold={2}:peak={3}";
-
- if (options.TonemappingParam != 0)
+ // Currently only with the use of NVENC decoder can we get a decent performance.
+ // Currently only the HEVC/H265 format is supported with NVDEC decoder.
+ // NVIDIA Pascal and Turing or higher are recommended.
+ // AMD Polaris and Vega or higher are recommended.
+ if (isColorDepth10
+ && _mediaEncoder.SupportsHwaccel("opencl")
+ && options.EnableTonemapping
+ && !string.IsNullOrEmpty(videoStream.VideoRange)
+ && videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase))
{
- parameters += ":param={4}";
- }
+ var parameters = "tonemap_opencl=format=nv12:primaries=bt709:transfer=bt709:matrix=bt709:tonemap={0}:desat={1}:threshold={2}:peak={3}";
- if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase))
- {
- parameters += ":range={5}";
- }
+ if (options.TonemappingParam != 0)
+ {
+ parameters += ":param={4}";
+ }
- // Upload the HDR10 or HLG data to the OpenCL device,
- // use tonemap_opencl filter for tone mapping,
- // and then download the SDR data to memory.
- filters.Add("hwupload");
- filters.Add(
- string.Format(
- CultureInfo.InvariantCulture,
- parameters,
- options.TonemappingAlgorithm,
- options.TonemappingDesat,
- options.TonemappingThreshold,
- options.TonemappingPeak,
- options.TonemappingParam,
- options.TonemappingRange));
- filters.Add("hwdownload");
+ if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase))
+ {
+ parameters += ":range={5}";
+ }
- if (hasGraphicalSubs || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true)
- || string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
- {
- filters.Add("format=nv12");
+ if (isSwDecoder || isD3d11vaDecoder)
+ {
+ isScalingInAdvance = true;
+ // Add zscale filter before tone mapping filter for performance.
+ var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight);
+ if (width.HasValue && height.HasValue)
+ {
+ filters.Add(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "zscale=s={0}x{1}",
+ width.Value,
+ height.Value));
+ }
+
+ // Convert to hardware pixel format p010 when using SW decoder.
+ filters.Add("format=p010");
+ }
+
+ // Upload the HDR10 or HLG data to the OpenCL device,
+ // use tonemap_opencl filter for tone mapping,
+ // and then download the SDR data to memory.
+ filters.Add("hwupload");
+ filters.Add(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ parameters,
+ options.TonemappingAlgorithm,
+ options.TonemappingDesat,
+ options.TonemappingThreshold,
+ options.TonemappingPeak,
+ options.TonemappingParam,
+ options.TonemappingRange));
+ filters.Add("hwdownload");
+
+ if (isLibX264Encoder
+ || hasGraphicalSubs
+ || (isNvdecHevcDecoder && isDeinterlaceHevc)
+ || (!isNvdecHevcDecoder && isDeinterlaceH264 || isDeinterlaceHevc))
+ {
+ filters.Add("format=nv12");
+ }
}
}
@@ -2185,7 +2203,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// Add hardware deinterlace filter before scaling filter
- if (state.DeInterlace("h264", true) || state.DeInterlace("avc", true))
+ if (isDeinterlaceH264)
{
if (isVaapiH264Encoder)
{
@@ -2198,10 +2216,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// Add software deinterlace filter before scaling filter
- if ((state.DeInterlace("h264", true)
- || state.DeInterlace("avc", true)
- || state.DeInterlace("h265", true)
- || state.DeInterlace("hevc", true))
+ if ((isDeinterlaceH264 || isDeinterlaceHevc)
&& !isVaapiH264Encoder
&& !isQsvH264Encoder
&& !isNvdecH264Decoder)
@@ -2225,7 +2240,21 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// Add scaling filter: scale_*=format=nv12 or scale_*=w=*:h=*:format=nv12 or scale=expr
- filters.AddRange(GetScalingFilters(state, inputWidth, inputHeight, threeDFormat, videoDecoder, outputVideoCodec, request.Width, request.Height, request.MaxWidth, request.MaxHeight));
+ if (!isScalingInAdvance)
+ {
+ filters.AddRange(
+ GetScalingFilters(
+ state,
+ inputWidth,
+ inputHeight,
+ threeDFormat,
+ videoDecoder,
+ outputVideoCodec,
+ request.Width,
+ request.Height,
+ request.MaxWidth,
+ request.MaxHeight));
+ }
// Add parameters to use VAAPI with burn-in text subtitles (GH issue #642)
if (isVaapiH264Encoder)
@@ -2258,7 +2287,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
output += string.Format(
CultureInfo.InvariantCulture,
- " -vf \"{0}\"",
+ "{0}",
string.Join(",", filters));
}
@@ -3017,21 +3046,31 @@ namespace MediaBrowser.Controller.MediaEncoding
var isWindows8orLater = Environment.OSVersion.Version.Major > 6 || (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor > 1);
var isDxvaSupported = _mediaEncoder.SupportsHwaccel("dxva2") || _mediaEncoder.SupportsHwaccel("d3d11va");
- if ((isDxvaSupported || IsVaapiSupported(state)) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase))
+ if (string.Equals(options.HardwareAccelerationType, "amf", StringComparison.OrdinalIgnoreCase))
{
- if (isLinux)
+ // Currently there is no AMF decoder on Linux, only have h264 encoder.
+ if (isDxvaSupported && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase))
{
- return "-hwaccel vaapi";
- }
+ if (isWindows && isWindows8orLater)
+ {
+ return "-hwaccel d3d11va";
+ }
- if (isWindows && isWindows8orLater)
- {
- return "-hwaccel d3d11va";
+ if (isWindows && !isWindows8orLater)
+ {
+ return "-hwaccel dxva2";
+ }
}
+ }
- if (isWindows && !isWindows8orLater)
+ if (string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase))
+ {
+ if (IsVaapiSupported(state) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase))
{
- return "-hwaccel dxva2";
+ if (isLinux)
+ {
+ return "-hwaccel vaapi";
+ }
}
}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
index c93822f0b5..0296539cb1 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs
@@ -337,7 +337,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var size = new ImageDimensions(VideoStream.Width.Value, VideoStream.Height.Value);
- var newSize = DrawingUtils.Resize(size,
+ var newSize = DrawingUtils.Resize(
+ size,
BaseRequest.Width ?? 0,
BaseRequest.Height ?? 0,
BaseRequest.MaxWidth ?? 0,
@@ -363,7 +364,8 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var size = new ImageDimensions(VideoStream.Width.Value, VideoStream.Height.Value);
- var newSize = DrawingUtils.Resize(size,
+ var newSize = DrawingUtils.Resize(
+ size,
BaseRequest.Width ?? 0,
BaseRequest.Height ?? 0,
BaseRequest.MaxWidth ?? 0,
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index 2560d0e31d..e7f042d2f5 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -69,7 +69,8 @@ namespace MediaBrowser.Controller.MediaEncoding
///
/// Extracts the video images on interval.
///
- Task ExtractVideoImagesOnInterval(string inputFile,
+ Task ExtractVideoImagesOnInterval(
+ string inputFile,
string container,
MediaStream videoStream,
MediaSourceInfo mediaSource,
diff --git a/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs b/MediaBrowser.Controller/Net/BasePeriodicWebSocketListener.cs
index 916dea58be..28227603b2 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>();
///
- /// Gets the name.
+ /// Gets the type used for the messages sent to the client.
///
- /// The name.
- protected abstract string Name { get; }
+ /// The type.
+ protected abstract SessionMessageType Type { get; }
+
+ ///
+ /// Gets the message type received from the client to start sending messages.
+ ///
+ /// The type.
+ protected abstract SessionMessageType StartType { get; }
+
+ ///
+ /// Gets the message type received from the client to stop sending messages.
+ ///
+ /// The type.
+ protected abstract SessionMessageType StopType { get; }
///
/// 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
{
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/Net/IWebSocketManager.cs b/MediaBrowser.Controller/Net/IWebSocketManager.cs
index e9f00ae88b..ce74173e70 100644
--- a/MediaBrowser.Controller/Net/IWebSocketManager.cs
+++ b/MediaBrowser.Controller/Net/IWebSocketManager.cs
@@ -16,12 +16,6 @@ namespace MediaBrowser.Controller.Net
///
event EventHandler> WebSocketConnected;
- ///
- /// Inits this instance.
- ///
- /// The websocket listeners.
- void Init(IEnumerable listeners);
-
///
/// The HTTP request handler.
///
diff --git a/MediaBrowser.Controller/Resolvers/IItemResolver.cs b/MediaBrowser.Controller/Resolvers/IItemResolver.cs
index eb7fb793a4..75286eadc0 100644
--- a/MediaBrowser.Controller/Resolvers/IItemResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/IItemResolver.cs
@@ -29,7 +29,8 @@ namespace MediaBrowser.Controller.Resolvers
public interface IMultiItemResolver
{
- MultiItemResolverResult ResolveMultiple(Folder parent,
+ MultiItemResolverResult ResolveMultiple(
+ Folder parent,
List files,
string collectionType,
IDirectoryService directoryService);
diff --git a/MediaBrowser.Controller/Session/ISessionController.cs b/MediaBrowser.Controller/Session/ISessionController.cs
index 22d6e2a04e..bc4ccd44ca 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
///
/// Sends the message.
///
- Task SendMessage(string name, Guid messageId, T data, CancellationToken cancellationToken);
+ Task SendMessage(SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken);
}
}
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index 228b2331dc..04c3004ee6 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -188,16 +188,16 @@ namespace MediaBrowser.Controller.Session
/// The data.
/// The cancellation token.
/// Task.
- Task SendMessageToAdminSessions(string name, T data, CancellationToken cancellationToken);
+ Task SendMessageToAdminSessions(SessionMessageType name, T data, CancellationToken cancellationToken);
///
/// Sends the message to user sessions.
///
///
/// Task.
- Task SendMessageToUserSessions(List userIds, string name, T data, CancellationToken cancellationToken);
+ Task SendMessageToUserSessions(List userIds, SessionMessageType name, T data, CancellationToken cancellationToken);
- Task SendMessageToUserSessions(List userIds, string name, Func dataFn, CancellationToken cancellationToken);
+ Task SendMessageToUserSessions(List userIds, SessionMessageType name, Func dataFn, CancellationToken cancellationToken);
///
/// Sends the message to user device sessions.
@@ -208,7 +208,7 @@ namespace MediaBrowser.Controller.Session
/// The data.
/// The cancellation token.
/// Task.
- Task SendMessageToUserDeviceSessions(string deviceId, string name, T data, CancellationToken cancellationToken);
+ Task SendMessageToUserDeviceSessions(string deviceId, SessionMessageType name, T data, CancellationToken cancellationToken);
///
/// Sends the restart required message.
diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs
index 55e44c19db..ce58a60b9a 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.
///
/// The supported commands.
- public string[] SupportedCommands
- => Capabilities == null ? Array.Empty() : Capabilities.SupportedCommands;
+ public GeneralCommandType[] SupportedCommands
+ => Capabilities == null ? Array.Empty() : Capabilities.SupportedCommands;
public Tuple EnsureController(Func factory)
{
diff --git a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs
index e742df5179..a1cada25cc 100644
--- a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs
+++ b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs
@@ -14,12 +14,12 @@ namespace MediaBrowser.Controller.SyncPlay
public class GroupInfo
{
///
- /// Gets the default ping value used for sessions.
+ /// The default ping value used for sessions.
///
- public long DefaultPing { get; } = 500;
+ public const long DefaultPing = 500;
///
- /// Gets or sets the group identifier.
+ /// Gets the group identifier.
///
/// The group identifier.
public Guid GroupId { get; } = Guid.NewGuid();
@@ -58,7 +58,8 @@ namespace MediaBrowser.Controller.SyncPlay
///
/// Checks if a session is in this group.
///
- /// true if the session is in this group; false otherwise.
+ /// The session id to check.
+ /// true if the session is in this group; false otherwise.
public bool ContainsSession(string sessionId)
{
return Participants.ContainsKey(sessionId);
@@ -70,16 +71,14 @@ namespace MediaBrowser.Controller.SyncPlay
/// The session.
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
+ });
}
///
@@ -88,12 +87,7 @@ namespace MediaBrowser.Controller.SyncPlay
/// The session.
public void RemoveSession(SessionInfo session)
{
- if (!ContainsSession(session.Id))
- {
- return;
- }
-
- Participants.Remove(session.Id, out _);
+ Participants.Remove(session.Id);
}
///
@@ -103,18 +97,16 @@ namespace MediaBrowser.Controller.SyncPlay
/// The ping.
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;
}
///
/// Gets the highest ping in the group.
///
- /// The highest ping in the group.
+ /// The highest ping in the group.
public long GetHighestPing()
{
long max = long.MinValue;
@@ -133,18 +125,16 @@ namespace MediaBrowser.Controller.SyncPlay
/// The state.
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;
}
///
/// Gets the group buffering state.
///
- /// true if there is a session buffering in the group; false otherwise.
+ /// true if there is a session buffering in the group; false otherwise.
public bool IsBuffering()
{
foreach (var session in Participants.Values)
@@ -161,7 +151,7 @@ namespace MediaBrowser.Controller.SyncPlay
///
/// Checks if the group is empty.
///
- /// true if the group is empty; false otherwise.
+ /// true if the group is empty; false otherwise.
public bool IsEmpty()
{
return Participants.Count == 0;
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 22537a4d95..cdeefbbbdf 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -666,6 +666,16 @@ namespace MediaBrowser.MediaEncoding.Probing
stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate);
stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate);
+ // Interlaced video streams in Matroska containers return the field rate instead of the frame rate
+ // as both the average and real frame rate, so we half the returned frame rates to get the correct values
+ //
+ // https://gitlab.com/mbunkus/mkvtoolnix/-/wikis/Wrong-frame-rate-displayed
+ if (stream.IsInterlaced && formatInfo.FormatName.Contains("matroska", StringComparison.OrdinalIgnoreCase))
+ {
+ stream.AverageFrameRate /= 2;
+ stream.RealFrameRate /= 2;
+ }
+
if (isAudio || string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase) ||
string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase))
{
diff --git a/MediaBrowser.Model/Activity/IActivityManager.cs b/MediaBrowser.Model/Activity/IActivityManager.cs
index d5344494e8..3e4ea208ed 100644
--- a/MediaBrowser.Model/Activity/IActivityManager.cs
+++ b/MediaBrowser.Model/Activity/IActivityManager.cs
@@ -1,10 +1,10 @@
#pragma warning disable CS1591
using System;
-using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Events;
+using Jellyfin.Data.Queries;
using MediaBrowser.Model.Querying;
namespace MediaBrowser.Model.Activity
@@ -15,11 +15,6 @@ namespace MediaBrowser.Model.Activity
Task CreateAsync(ActivityLog entry);
- QueryResult GetPagedResult(int? startIndex, int? limit);
-
- QueryResult GetPagedResult(
- Func, IQueryable> func,
- int? startIndex,
- int? limit);
+ Task> GetPagedResultAsync(ActivityLogQuery query);
}
}
diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs
index 890469d361..54ef49ea62 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/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
index 93e60753ae..8b73ecbd4d 100644
--- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
+++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs
@@ -38,7 +38,8 @@ namespace MediaBrowser.Model.Dlna
";DLNA.ORG_FLAGS={0}",
DlnaMaps.FlagsToString(flagValue));
- ResponseProfile mediaProfile = _profile.GetImageMediaProfile(container,
+ ResponseProfile mediaProfile = _profile.GetImageMediaProfile(
+ container,
width,
height);
@@ -160,7 +161,8 @@ namespace MediaBrowser.Model.Dlna
string dlnaflags = string.Format(CultureInfo.InvariantCulture, ";DLNA.ORG_FLAGS={0}",
DlnaMaps.FlagsToString(flagValue));
- ResponseProfile mediaProfile = _profile.GetVideoMediaProfile(container,
+ ResponseProfile mediaProfile = _profile.GetVideoMediaProfile(
+ container,
audioCodec,
videoCodec,
width,
@@ -221,7 +223,8 @@ namespace MediaBrowser.Model.Dlna
private static string GetImageOrgPnValue(string container, int? width, int? height)
{
MediaFormatProfile? format = new MediaFormatProfileResolver()
- .ResolveImageFormat(container,
+ .ResolveImageFormat(
+ container,
width,
height);
@@ -231,7 +234,8 @@ namespace MediaBrowser.Model.Dlna
private static string GetAudioOrgPnValue(string container, int? audioBitrate, int? audioSampleRate, int? audioChannels)
{
MediaFormatProfile? format = new MediaFormatProfileResolver()
- .ResolveAudioFormat(container,
+ .ResolveAudioFormat(
+ container,
audioBitrate,
audioSampleRate,
audioChannels);
diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs
index 7e921b1fdf..44412f3e41 100644
--- a/MediaBrowser.Model/Dlna/DeviceProfile.cs
+++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs
@@ -277,7 +277,8 @@ namespace MediaBrowser.Model.Dlna
return null;
}
- public ResponseProfile GetVideoMediaProfile(string container,
+ public ResponseProfile GetVideoMediaProfile(
+ string container,
string audioCodec,
string videoCodec,
int? width,
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index d9e7e4fbb4..4959a9b922 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -455,9 +455,11 @@ 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(), GetTranscodeReasonsFromDirectPlayProfile(item, null, audioStream, options.Profile.DirectPlayProfiles));
}
@@ -678,7 +680,8 @@ namespace MediaBrowser.Model.Dlna
bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult.Item1);
bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directStreamEligibilityResult.Item1);
- _logger.LogInformation("Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
+ _logger.LogInformation(
+ "Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
options.Profile.Name ?? "Unknown Profile",
item.Path ?? "Unknown path",
isEligibleForDirectPlay,
@@ -972,9 +975,11 @@ 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));
}
@@ -1135,7 +1140,8 @@ namespace MediaBrowser.Model.Dlna
private void LogConditionFailure(DeviceProfile profile, string type, ProfileCondition condition, MediaSourceInfo mediaSource)
{
- _logger.LogInformation("Profile: {0}, DirectPlay=false. Reason={1}.{2} Condition: {3}. ConditionValue: {4}. IsRequired: {5}. Path: {6}",
+ _logger.LogInformation(
+ "Profile: {0}, DirectPlay=false. Reason={1}.{2} Condition: {3}. ConditionValue: {4}. IsRequired: {5}. Path: {6}",
type,
profile.Name ?? "Unknown Profile",
condition.Property,
@@ -1340,7 +1346,8 @@ namespace MediaBrowser.Model.Dlna
if (itemBitrate > requestedMaxBitrate)
{
- _logger.LogInformation("Bitrate exceeds {PlayBackMethod} limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}",
+ _logger.LogInformation(
+ "Bitrate exceeds {PlayBackMethod} limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}",
playMethod, itemBitrate, requestedMaxBitrate);
return false;
}
diff --git a/MediaBrowser.Model/Extensions/EnumerableExtensions.cs b/MediaBrowser.Model/Extensions/EnumerableExtensions.cs
new file mode 100644
index 0000000000..712fa381ee
--- /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
+{
+ ///
+ /// Extension methods for .
+ ///
+ public static class EnumerableExtensions
+ {
+ ///
+ /// Orders by requested language in descending order, prioritizing "en" over other non-matches.
+ ///
+ /// The remote image infos.
+ /// The requested language for the images.
+ /// The ordered remote image infos.
+ public static IEnumerable OrderByLanguageDescending(this IEnumerable 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/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index 2646810907..253ee7e795 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -34,7 +34,7 @@
-
+
diff --git a/MediaBrowser.Model/Net/WebSocketMessage.cs b/MediaBrowser.Model/Net/WebSocketMessage.cs
index 660eebeda6..bffbbe612d 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.
///
/// The type of the message.
- 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 d3878ca308..a85e6ff2a4 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();
- SupportedCommands = Array.Empty();
+ SupportedCommands = Array.Empty();
SupportsPersistentIdentifier = true;
}
}
diff --git a/MediaBrowser.Model/Session/GeneralCommandType.cs b/MediaBrowser.Model/Session/GeneralCommandType.cs
index 5a9042d5f9..c58fa9a6b9 100644
--- a/MediaBrowser.Model/Session/GeneralCommandType.cs
+++ b/MediaBrowser.Model/Session/GeneralCommandType.cs
@@ -43,6 +43,11 @@ namespace MediaBrowser.Model.Session
Guide = 32,
ToggleStats = 33,
PlayMediaSource = 34,
- PlayTrailers = 35
+ PlayTrailers = 35,
+ SetShuffleQueue = 36,
+ PlayState = 37,
+ PlayNext = 38,
+ ToggleOsdMenu = 39,
+ Play = 40
}
}
diff --git a/MediaBrowser.Model/Session/SessionMessageType.cs b/MediaBrowser.Model/Session/SessionMessageType.cs
new file mode 100644
index 0000000000..23c41026dc
--- /dev/null
+++ b/MediaBrowser.Model/Session/SessionMessageType.cs
@@ -0,0 +1,50 @@
+#pragma warning disable CS1591
+
+namespace MediaBrowser.Model.Session
+{
+ ///
+ /// The different kinds of messages that are used in the WebSocket api.
+ ///
+ 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/System/PublicSystemInfo.cs b/MediaBrowser.Model/System/PublicSystemInfo.cs
index d2f7556a51..53030843ae 100644
--- a/MediaBrowser.Model/System/PublicSystemInfo.cs
+++ b/MediaBrowser.Model/System/PublicSystemInfo.cs
@@ -43,7 +43,10 @@ namespace MediaBrowser.Model.System
///
/// Gets or sets a value indicating whether the startup wizard is completed.
///
- /// The startup completion status.
- public bool StartupWizardCompleted { get; set; }
+ ///
+ /// Nullable for OpenAPI specification only to retain backwards compatibility in apiclients.
+ ///
+ /// The startup completion status.]
+ public bool? StartupWizardCompleted { get; set; }
}
}
diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs
index a1f01f7e8e..363b2633fd 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();
diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs
index b6fb4267f4..a0c7d4ad09 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 813dd441f5..24400eae53 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -16,11 +16,12 @@
-
-
-
+
+
+
+
diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
index d231bfa2fc..9804ec3bb4 100644
--- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
+++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs
@@ -214,7 +214,7 @@ namespace MediaBrowser.Providers.MediaInfo
return new[]
{
// Every so often
- new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
+ new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }
};
}
}
diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
index abfa1c6e71..31f0123dcf 100644
--- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
+++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumProvider.cs
@@ -46,6 +46,7 @@ namespace MediaBrowser.Providers.Music
private readonly string _musicBrainzBaseUrl;
+ private SemaphoreSlim _apiRequestLock = new SemaphoreSlim(1, 1);
private Stopwatch _stopWatchMusicBrainz = new Stopwatch();
public MusicBrainzAlbumProvider(
@@ -742,48 +743,58 @@ namespace MediaBrowser.Providers.Music
///
internal async Task GetMusicBrainzResponse(string url, CancellationToken cancellationToken)
{
- using var options = new HttpRequestMessage(HttpMethod.Get, _musicBrainzBaseUrl.TrimEnd('/') + url);
+ await _apiRequestLock.WaitAsync(cancellationToken).ConfigureAwait(false);
- // MusicBrainz request a contact email address is supplied, as comment, in user agent field:
- // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#User-Agent
- options.Headers.UserAgent.ParseAdd(string.Format(
- CultureInfo.InvariantCulture,
- "{0} ( {1} )",
- _appHost.ApplicationUserAgent,
- _appHost.ApplicationUserAgentAddress));
-
- HttpResponseMessage response;
- var attempts = 0u;
-
- do
+ try
{
- attempts++;
+ HttpResponseMessage response;
+ var attempts = 0u;
+ var requestUrl = _musicBrainzBaseUrl.TrimEnd('/') + url;
- if (_stopWatchMusicBrainz.ElapsedMilliseconds < _musicBrainzQueryIntervalMs)
+ do
{
- // MusicBrainz is extremely adamant about limiting to one request per second
- var delayMs = _musicBrainzQueryIntervalMs - _stopWatchMusicBrainz.ElapsedMilliseconds;
- await Task.Delay((int)delayMs, cancellationToken).ConfigureAwait(false);
+ attempts++;
+
+ if (_stopWatchMusicBrainz.ElapsedMilliseconds < _musicBrainzQueryIntervalMs)
+ {
+ // MusicBrainz is extremely adamant about limiting to one request per second.
+ var delayMs = _musicBrainzQueryIntervalMs - _stopWatchMusicBrainz.ElapsedMilliseconds;
+ await Task.Delay((int)delayMs, cancellationToken).ConfigureAwait(false);
+ }
+
+ // Write time since last request to debug log as evidence we're meeting rate limit
+ // requirement, before resetting stopwatch back to zero.
+ _logger.LogDebug("GetMusicBrainzResponse: Time since previous request: {0} ms", _stopWatchMusicBrainz.ElapsedMilliseconds);
+ _stopWatchMusicBrainz.Restart();
+
+ using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
+
+ // MusicBrainz request a contact email address is supplied, as comment, in user agent field:
+ // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#User-Agent .
+ request.Headers.UserAgent.ParseAdd(string.Format(
+ CultureInfo.InvariantCulture,
+ "{0} ( {1} )",
+ _appHost.ApplicationUserAgent,
+ _appHost.ApplicationUserAgentAddress));
+
+ response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(request).ConfigureAwait(false);
+
+ // We retry a finite number of times, and only whilst MB is indicating 503 (throttling).
+ }
+ while (attempts < MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable);
+
+ // Log error if unable to query MB database due to throttling.
+ if (attempts == MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable)
+ {
+ _logger.LogError("GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}", attempts, requestUrl);
}
- // Write time since last request to debug log as evidence we're meeting rate limit
- // requirement, before resetting stopwatch back to zero.
- _logger.LogDebug("GetMusicBrainzResponse: Time since previous request: {0} ms", _stopWatchMusicBrainz.ElapsedMilliseconds);
- _stopWatchMusicBrainz.Restart();
-
- response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options).ConfigureAwait(false);
-
- // We retry a finite number of times, and only whilst MB is indicating 503 (throttling)
+ return response;
}
- while (attempts < MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable);
-
- // Log error if unable to query MB database due to throttling
- if (attempts == MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable)
+ finally
{
- _logger.LogError("GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}", attempts, options.RequestUri);
+ _apiRequestLock.Release();
}
-
- return response;
}
///
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs
index f22d484abc..ce0dab701d 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> GetAllEpisodesAsync(int tvdbId, string language,
- CancellationToken cancellationToken)
- {
- // Traverse all episode pages and join them together
- var episodes = new List();
- 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> GetSeriesByImdbIdAsync(
string imdbId,
string language,
@@ -176,7 +150,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
string language,
CancellationToken cancellationToken)
{
- searchInfo.SeriesProviderIds.TryGetValue(nameof(MetadataProvider.Tvdb),
+ searchInfo.SeriesProviderIds.TryGetValue(
+ nameof(MetadataProvider.Tvdb),
out var seriesTvdbId);
var episodeQuery = new EpisodeQuery();
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs
index 5fa8a3e1ca..fd72ea4a81 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs
@@ -106,7 +106,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
.ConfigureAwait(false);
if (string.IsNullOrEmpty(episodeTvdbId))
{
- _logger.LogError("Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}",
+ _logger.LogError(
+ "Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}",
searchInfo.ParentIndexNumber, searchInfo.IndexNumber, seriesTvdbId);
return result;
}
diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs
index ca9b1d738f..b34e52235a 100644
--- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Text;
@@ -123,7 +124,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
await _tvdbClientManager
.GetSeriesByIdAsync(Convert.ToInt32(tvdbId), metadataLanguage, cancellationToken)
.ConfigureAwait(false);
- MapSeriesToResult(result, seriesResult.Data, metadataLanguage);
+ await MapSeriesToResult(result, seriesResult.Data, metadataLanguage).ConfigureAwait(false);
}
catch (TvDbServerException e)
{
@@ -297,7 +298,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
return name.Trim();
}
- private void MapSeriesToResult(MetadataResult result, TvDbSharper.Dto.Series tvdbSeries, string metadataLanguage)
+ private async Task MapSeriesToResult(MetadataResult result, TvDbSharper.Dto.Series tvdbSeries, string metadataLanguage)
{
Series series = result.Item;
series.SetProviderId(MetadataProvider.Tvdb, tvdbSeries.Id.ToString());
@@ -340,20 +341,21 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
{
try
{
- var episodeSummary = _tvdbClientManager
- .GetSeriesEpisodeSummaryAsync(tvdbSeries.Id, metadataLanguage, CancellationToken.None).Result.Data;
- var maxSeasonNumber = episodeSummary.AiredSeasons.Select(s => Convert.ToInt32(s)).Max();
- var episodeQuery = new EpisodeQuery
+ var episodeSummary = await _tvdbClientManager.GetSeriesEpisodeSummaryAsync(tvdbSeries.Id, metadataLanguage, CancellationToken.None).ConfigureAwait(false);
+
+ if (episodeSummary.Data.AiredSeasons.Length != 0)
{
- AiredSeason = maxSeasonNumber
- };
- var episodesPage =
- _tvdbClientManager.GetEpisodesPageAsync(tvdbSeries.Id, episodeQuery, metadataLanguage, CancellationToken.None).Result.Data;
- result.Item.EndDate = episodesPage.Select(e =>
+ var maxSeasonNumber = episodeSummary.Data.AiredSeasons.Max(s => Convert.ToInt32(s, CultureInfo.InvariantCulture));
+ var episodeQuery = new EpisodeQuery
{
- DateTime.TryParse(e.FirstAired, out var firstAired);
- return firstAired;
- }).Max();
+ AiredSeason = maxSeasonNumber
+ };
+ var episodesPage = await _tvdbClientManager.GetEpisodesPageAsync(tvdbSeries.Id, episodeQuery, metadataLanguage, CancellationToken.None).ConfigureAwait(false);
+
+ result.Item.EndDate = episodesPage.Data
+ .Select(e => DateTime.TryParse(e.FirstAired, out var firstAired) ? firstAired : (DateTime?)null)
+ .Max();
+ }
}
catch (TvDbServerException e)
{
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs
index f6592afe46..df1e12240d 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> 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);
-
- if (mainResult != null)
- {
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
-
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
-
- return GetImages(mainResult, language, tmdbImageUrl);
- }
+ return Enumerable.Empty();
}
- return new List();
- }
+ var language = item.GetPreferredMetadataLanguage();
- private IEnumerable GetImages(CollectionResult obj, string language, string baseUrl)
- {
- var list = new List();
+ var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false);
- var images = obj.Images ?? new CollectionImages();
-
- list.AddRange(GetPosters(images).Select(i => new RemoteImageInfo
+ if (collection?.Images == null)
{
- 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
- }));
+ return Enumerable.Empty();
+ }
- list.AddRange(GetBackdrops(images).Select(i => new RemoteImageInfo
+ var remoteImages = new List();
+
+ 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 poster = collection.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
+ });
+ }
- var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
-
- return list.OrderByDescending(i =>
+ for (var i = 0; i < collection.Images.Backdrops.Count; i++)
{
- if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase))
+ var backdrop = collection.Images.Backdrops[i];
+ remoteImages.Add(new RemoteImageInfo
{
- return 3;
- }
+ 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
+ });
+ }
- 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);
+ return remoteImages.OrderByLanguageDescending(language);
}
- ///
- /// Gets the posters.
- ///
- /// The images.
- /// IEnumerable{MovieDbProvider.Poster}.
- private IEnumerable GetPosters(CollectionImages images)
- {
- return images.Posters ?? new List();
- }
-
- ///
- /// Gets the backdrops.
- ///
- /// The images.
- /// IEnumerable{MovieDbProvider.Backdrop}.
- private IEnumerable GetBackdrops(CollectionImages images)
- {
- var eligibleBackdrops = images.Backdrops == null ? new List() :
- images.Backdrops;
-
- return eligibleBackdrops.OrderByDescending(i => i.Vote_Average)
- .ThenByDescending(i => i.Vote_Count);
- }
-
- public int Order => 0;
-
public Task 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 e7328b5535..fcd8e614c1 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs
@@ -3,270 +3,118 @@
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
{
- private const string GetCollectionInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/collection/{0}?api_key={1}&append_to_response=images";
-
- internal static TmdbBoxSetProvider Current;
-
- private readonly ILogger _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 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> 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 collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false);
- var dataFilePath = GetDataFilePath(_config.ApplicationPaths, tmdbId, searchInfo.MetadataLanguage);
- var info = _json.DeserializeFromFile(dataFilePath);
-
- var images = (info.Images ?? new CollectionImages()).Posters ?? new List();
-
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
-
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
+ if (collection == null)
+ {
+ return Enumerable.Empty();
+ }
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));
+ if (collection.Images != null)
+ {
+ result.ImageUrl = _tmdbClientManager.GetPosterUrl(collection.PosterPath);
+ }
+
+ result.SetProviderId(MetadataProvider.Tmdb, collection.Id.ToString(CultureInfo.InvariantCulture));
return new[] { result };
}
- return await new TmdbSearch(_logger, _json, _libraryManager).GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false);
+ var collectionSearchResults = await _tmdbClientManager.SearchCollectionAsync(searchInfo.Name, language, cancellationToken).ConfigureAwait(false);
+
+ var collections = new List();
+ for (var i = 0; i < collectionSearchResults.Count; i++)
+ {
+ var collection = new RemoteSearchResult
+ {
+ Name = collectionSearchResults[i].Name,
+ SearchProviderName = Name
+ };
+ collection.SetProviderId(MetadataProvider.Tmdb, collectionSearchResults[i].Id.ToString(CultureInfo.InvariantCulture));
+
+ collections.Add(collection);
+ }
+
+ return collections;
}
public async Task> GetMetadata(BoxSetInfo id, CancellationToken cancellationToken)
{
- var tmdbId = id.GetProviderId(MetadataProvider.Tmdb);
-
+ 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 (string.IsNullOrEmpty(tmdbId))
+ if (tmdbId <= 0)
{
- var searchResults = await new TmdbSearch(_logger, _json, _libraryManager).GetSearchResults(id, cancellationToken).ConfigureAwait(false);
+ var searchResults = await _tmdbClientManager.SearchCollectionAsync(id.Name, language, cancellationToken).ConfigureAwait(false);
- var searchResult = searchResults.FirstOrDefault();
-
- if (searchResult != null)
+ if (searchResults != null && searchResults.Count > 0)
{
- tmdbId = searchResult.GetProviderId(MetadataProvider.Tmdb);
+ tmdbId = searchResults[0].Id;
}
}
var result = new MetadataResult();
- if (!string.IsNullOrEmpty(tmdbId))
+ if (tmdbId > 0)
{
- var mainResult = await GetMovieDbResult(tmdbId, id.MetadataLanguage, cancellationToken).ConfigureAwait(false);
+ var collection = await _tmdbClientManager.GetCollectionAsync(tmdbId, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken).ConfigureAwait(false);
- if (mainResult != null)
+ if (collection != null)
{
+ var item = new BoxSet
+ {
+ Name = collection.Name,
+ Overview = collection.Overview
+ };
+
+ item.SetProviderId(MetadataProvider.Tmdb, collection.Id.ToString(CultureInfo.InvariantCulture));
+
result.HasMetadata = true;
- result.Item = GetItem(mainResult);
+ result.Item = item;
}
}
return result;
}
- internal async Task GetMovieDbResult(string tmdbId, string language, CancellationToken cancellationToken)
- {
- if (string.IsNullOrEmpty(tmdbId))
- {
- throw new ArgumentNullException(nameof(tmdbId));
- }
-
- await EnsureInfo(tmdbId, language, cancellationToken).ConfigureAwait(false);
-
- var dataFilePath = GetDataFilePath(_config.ApplicationPaths, tmdbId, language);
-
- if (!string.IsNullOrEmpty(dataFilePath))
- {
- return _json.DeserializeFromFile(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);
-
- if (mainResult == null)
- {
- return;
- }
-
- var dataFilePath = GetDataFilePath(_config.ApplicationPaths, tmdbId, preferredMetadataLanguage);
-
- Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
-
- _json.SerializeToFile(mainResult, dataFilePath);
- }
-
- private async Task FetchMainResult(string id, string language, CancellationToken cancellationToken)
- {
- var url = string.Format(CultureInfo.InvariantCulture, GetCollectionInfo3, id, TmdbUtils.ApiKey);
-
- if (!string.IsNullOrEmpty(language))
- {
- url += string.Format(CultureInfo.InvariantCulture, "&language={0}", TmdbMovieProvider.NormalizeLanguage(language));
-
- // Get images in english and with no language
- url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language);
- }
-
- 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 mainResponse = await TmdbMovieProvider.Current.GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
- await using var stream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
- var mainResult = await _json.DeserializeFromStreamAsync(stream).ConfigureAwait(false);
-
- cancellationToken.ThrowIfCancellationRequested();
-
- if (mainResult != null && string.IsNullOrEmpty(mainResult.Name))
- {
- if (!string.IsNullOrEmpty(language) && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase))
- {
- url = string.Format(CultureInfo.InvariantCulture, GetCollectionInfo3, id, TmdbUtils.ApiKey) + "&language=en";
-
- if (!string.IsNullOrEmpty(language))
- {
- // 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(langStream).ConfigureAwait(false);
- }
- }
-
- return mainResult;
- }
-
- internal Task EnsureInfo(string tmdbId, string preferredMetadataLanguage, CancellationToken cancellationToken)
- {
- var path = GetDataFilePath(_config.ApplicationPaths, tmdbId, preferredMetadataLanguage);
-
- 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 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;
- }
-
public Task GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, 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 0a8994d540..0000000000
--- 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 Backdrops { get; set; }
-
- public List 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 c6b851c237..0000000000
--- 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 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 a48124b3e1..0000000000
--- 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 5b7627f6e8..0000000000
--- 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 339ecb6285..0000000000
--- 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 aac4420e8b..0000000000
--- 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 9ba1c15c65..0000000000
--- 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 0538cf174d..0000000000
--- 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 Backdrops { get; set; }
-
- public List 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 fff86931be..0000000000
--- 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 235ecb5682..0000000000
--- 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 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 4f61e978be..0000000000
--- 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 0a1f8843eb..0000000000
--- 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 61de819b93..0000000000
--- 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 59ab18b7bf..0000000000
--- 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 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 ebd5c7acee..0000000000
--- 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 1c673fdbd6..0000000000
--- 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
public class TmdbMovieProvider : IRemoteMetadataProvider, 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 _logger;
private readonly ILibraryManager _libraryManager;
- private readonly IApplicationHost _appHost;
-
- ///
- /// The _TMDB settings task.
- ///
- private TmdbSettingsResult _tmdbSettings;
+ private readonly TmdbClientManager _tmdbClientManager;
public TmdbMovieProvider(
- IJsonSerializer jsonSerializer,
- IHttpClientFactory httpClientFactory,
- IFileSystem fileSystem,
- IServerConfigurationManager configurationManager,
- ILogger 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; }
-
- ///
public string Name => TmdbUtils.ProviderName;
///
public int Order => 1;
- public Task> GetSearchResults(MovieInfo searchInfo, CancellationToken cancellationToken)
+ public async Task> GetSearchResults(MovieInfo searchInfo, CancellationToken cancellationToken)
{
- return GetMovieSearchResults(searchInfo, cancellationToken);
+ var tmdbId = Convert.ToInt32(searchInfo.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
+
+ if (tmdbId == 0)
+ {
+ var movieResults = await _tmdbClientManager
+ .SearchMovieAsync(searchInfo.Name, searchInfo.MetadataLanguage, cancellationToken)
+ .ConfigureAwait(false);
+ var remoteSearchResults = new List();
+ for (var i = 0; i < movieResults.Count; i++)
+ {
+ var movieResult = movieResults[i];
+ var remoteSearchResult = new RemoteSearchResult
+ {
+ 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 remoteSearchResults;
+ }
+
+ var movie = await _tmdbClientManager
+ .GetMovieAsync(tmdbId, searchInfo.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(searchInfo.MetadataLanguage), cancellationToken)
+ .ConfigureAwait(false);
+
+ var remoteResult = new RemoteSearchResult
+ {
+ Name = movie.Title ?? movie.OriginalTitle,
+ SearchProviderName = Name,
+ ImageUrl = _tmdbClientManager.GetPosterUrl(movie.PosterPath),
+ Overview = movie.Overview
+ };
+
+ if (movie.ReleaseDate != null)
+ {
+ var releaseDate = movie.ReleaseDate.Value.ToUniversalTime();
+ remoteResult.PremiereDate = releaseDate;
+ remoteResult.ProductionYear = releaseDate.Year;
+ }
+
+ remoteResult.SetProviderId(MetadataProvider.Tmdb, movie.Id.ToString(CultureInfo.InvariantCulture));
+
+ if (!string.IsNullOrWhiteSpace(movie.ImdbId))
+ {
+ remoteResult.SetProviderId(MetadataProvider.Imdb, movie.ImdbId);
+ }
+
+ return new[] { remoteResult };
}
- public async Task> GetMovieSearchResults(ItemLookupInfo searchInfo, CancellationToken cancellationToken)
+ public async Task> GetMetadata(MovieInfo info, CancellationToken cancellationToken)
{
- var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb);
+ var tmdbId = info.GetProviderId(MetadataProvider.Tmdb);
+ var imdbId = info.GetProviderId(MetadataProvider.Imdb);
- if (!string.IsNullOrEmpty(tmdbId))
+ if (string.IsNullOrEmpty(tmdbId) && string.IsNullOrEmpty(imdbId))
{
- cancellationToken.ThrowIfCancellationRequested();
+ // 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);
- await EnsureMovieInfo(tmdbId, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
-
- var dataFilePath = GetDataFilePath(tmdbId, searchInfo.MetadataLanguage);
-
- var obj = _jsonSerializer.DeserializeFromFile(dataFilePath);
-
- var tmdbSettings = await GetTmdbSettings(cancellationToken).ConfigureAwait(false);
-
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
-
- var remoteResult = new RemoteSearchResult
+ if (searchResults.Count > 0)
{
- Name = obj.GetTitle(),
- SearchProviderName = Name,
- ImageUrl = string.IsNullOrWhiteSpace(obj.Poster_Path) ? null : tmdbImageUrl + obj.Poster_Path
+ tmdbId = searchResults[0].Id.ToString(CultureInfo.InvariantCulture);
+ }
+ }
+
+ if (string.IsNullOrEmpty(tmdbId))
+ {
+ return new MetadataResult();
+ }
+
+ var movieResult = await _tmdbClientManager
+ .GetMovieAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
+ .ConfigureAwait(false);
+
+ 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
+ {
+ 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;
+ }
+
+ movie.CommunityRating = Convert.ToSingle(movieResult.VoteAverage);
+
+ if (movieResult.Releases?.Countries != null)
+ {
+ var releases = movieResult.Releases.Countries.Where(i => !string.IsNullOrWhiteSpace(i.Certification)).ToList();
+
+ 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 (ourRelease != null)
+ {
+ var ratingPrefix = string.Equals(info.MetadataCountryCode, "us", StringComparison.OrdinalIgnoreCase) ? string.Empty : info.MetadataCountryCode + "-";
+ var newRating = ratingPrefix + ourRelease.Certification;
+
+ newRating = newRating.Replace("de-", "FSK-", StringComparison.OrdinalIgnoreCase);
+
+ movie.OfficialRating = newRating;
+ }
+ else if (usRelease != null)
+ {
+ movie.OfficialRating = usRelease.Certification;
+ }
+ }
+
+ movie.PremiereDate = movieResult.ReleaseDate;
+ movie.ProductionYear = movieResult.ReleaseDate?.Year;
+
+ if (movieResult.ProductionCompanies != null)
+ {
+ movie.SetStudios(movieResult.ProductionCompanies.Select(c => c.Name));
+ }
+
+ var genres = movieResult.Genres;
+
+ foreach (var genre in genres.Select(g => g.Name))
+ {
+ movie.AddGenre(genre);
+ }
+
+ if (movieResult.Keywords?.Keywords != null)
+ {
+ for (var i = 0; i < movieResult.Keywords.Keywords.Count; i++)
+ {
+ movie.AddTag(movieResult.Keywords.Keywords[i].Name);
+ }
+ }
+
+ if (movieResult.Credits?.Cast != null)
+ {
+ // 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
+ };
+
+ if (!string.IsNullOrWhiteSpace(actor.ProfilePath))
+ {
+ personInfo.ImageUrl = _tmdbClientManager.GetProfileUrl(actor.ProfilePath);
+ }
+
+ if (actor.Id > 0)
+ {
+ personInfo.SetProviderId(MetadataProvider.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture));
+ }
+
+ metadataResult.AddPerson(personInfo);
+ }
+ }
+
+ if (movieResult.Credits?.Crew != null)
+ {
+ var keepTypes = new[]
+ {
+ PersonType.Director,
+ PersonType.Writer,
+ PersonType.Producer
};
- if (!string.IsNullOrWhiteSpace(obj.Release_Date))
+ foreach (var person in movieResult.Credits.Crew)
{
- // These dates are always in this exact format
- if (DateTime.TryParse(obj.Release_Date, _usCulture, DateTimeStyles.None, out var r))
+ // Normalize this
+ var type = TmdbUtils.MapCrewToPersonType(person);
+
+ if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) &&
+ !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
{
- remoteResult.PremiereDate = r.ToUniversalTime();
- remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year;
+ continue;
}
+
+ var personInfo = new PersonInfo
+ {
+ Name = person.Name.Trim(),
+ Role = person.Job,
+ Type = type
+ };
+
+ if (!string.IsNullOrWhiteSpace(person.ProfilePath))
+ {
+ personInfo.ImageUrl = _tmdbClientManager.GetPosterUrl(person.ProfilePath);
+ }
+
+ if (person.Id > 0)
+ {
+ personInfo.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
+ }
+
+ metadataResult.AddPerson(personInfo);
}
+ }
- remoteResult.SetProviderId(MetadataProvider.Tmdb, obj.Id.ToString(_usCulture));
- if (!string.IsNullOrWhiteSpace(obj.Imdb_Id))
+ if (movieResult.Videos?.Results != null)
+ {
+ var trailers = new List();
+ for (var i = 0; i < movieResult.Videos.Results.Count; i++)
{
- remoteResult.SetProviderId(MetadataProvider.Imdb, obj.Imdb_Id);
+ var video = movieResult.Videos.Results[0];
+ if (!TmdbUtils.IsTrailerType(video))
+ {
+ continue;
+ }
+
+ trailers.Add(new MediaUrl
+ {
+ Url = string.Format(CultureInfo.InvariantCulture, "https://www.youtube.com/watch?v={0}", video.Key),
+ Name = video.Name
+ });
}
- return new[] { remoteResult };
+ movie.RemoteTrailers = trailers;
}
- return await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetMovieSearchResults(searchInfo, cancellationToken).ConfigureAwait(false);
- }
-
- public Task> GetMetadata(MovieInfo info, CancellationToken cancellationToken)
- {
- return GetItemMetadata(info, cancellationToken);
- }
-
- public Task> GetItemMetadata(ItemLookupInfo id, CancellationToken cancellationToken)
- where T : BaseItem, new()
- {
- var movieDb = new GenericTmdbMovieInfo(_logger, _jsonSerializer, _libraryManager, _fileSystem);
-
- return movieDb.GetMetadata(id, cancellationToken);
- }
-
- ///
- /// Gets the TMDB settings.
- ///
- /// Task{TmdbSettingsResult}.
- internal async Task GetTmdbSettings(CancellationToken cancellationToken)
- {
- if (_tmdbSettings != null)
- {
- return _tmdbSettings;
- }
-
- using var requestMessage = new HttpRequestMessage(HttpMethod.Get, string.Format(CultureInfo.InvariantCulture, TmdbConfigUrl, TmdbUtils.ApiKey));
- foreach (var header in TmdbUtils.AcceptHeaders)
- {
- requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
-
- using var response = await GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
- await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
- _tmdbSettings = await _jsonSerializer.DeserializeFromStreamAsync(stream).ConfigureAwait(false);
- return _tmdbSettings;
- }
-
- ///
- /// Gets the movie data path.
- ///
- /// The app paths.
- /// The TMDB id.
- /// System.String.
- internal static string GetMovieDataPath(IApplicationPaths appPaths, string tmdbId)
- {
- var dataPath = GetMoviesDataPath(appPaths);
-
- return Path.Combine(dataPath, tmdbId);
- }
-
- internal static string GetMoviesDataPath(IApplicationPaths appPaths)
- {
- var dataPath = Path.Combine(appPaths.CachePath, "tmdb-movies2");
-
- return dataPath;
- }
-
- ///
- /// Downloads the movie info.
- ///
- /// The id.
- /// The preferred metadata language.
- /// The cancellation token.
- /// Task.
- internal async Task DownloadMovieInfo(string id, string preferredMetadataLanguage, CancellationToken cancellationToken)
- {
- var mainResult = await FetchMainResult(id, true, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false);
-
- if (mainResult == null)
- {
- return;
- }
-
- var dataFilePath = GetDataFilePath(id, preferredMetadataLanguage);
-
- Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath));
-
- _jsonSerializer.SerializeToFile(mainResult, dataFilePath);
- }
-
- internal Task EnsureMovieInfo(string tmdbId, string language, CancellationToken cancellationToken)
- {
- if (string.IsNullOrEmpty(tmdbId))
- {
- throw new ArgumentNullException(nameof(tmdbId));
- }
-
- var path = GetDataFilePath(tmdbId, 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 DownloadMovieInfo(tmdbId, language, cancellationToken);
- }
-
- internal string GetDataFilePath(string tmdbId, string preferredLanguage)
- {
- if (string.IsNullOrEmpty(tmdbId))
- {
- throw new ArgumentNullException(nameof(tmdbId));
- }
-
- var path = GetMovieDataPath(_configurationManager.ApplicationPaths, tmdbId);
-
- if (string.IsNullOrWhiteSpace(preferredLanguage))
- {
- preferredLanguage = "alllang";
- }
-
- var filename = string.Format(CultureInfo.InvariantCulture, "all-{0}.json", preferredLanguage);
-
- return Path.Combine(path, filename);
- }
-
- public static string GetImageLanguagesParam(string preferredLanguage)
- {
- var languages = new List();
-
- 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);
- }
-
- public static string NormalizeLanguage(string language)
- {
- if (!string.IsNullOrEmpty(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;
- }
-
- 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;
- }
-
- ///
- /// Fetches the main result.
- ///
- /// The id.
- /// if set to true [is TMDB identifier].
- /// The language.
- /// The cancellation token.
- /// Task{CompleteMovieData}.
- internal async Task FetchMainResult(string id, bool isTmdbId, string language, CancellationToken cancellationToken)
- {
- var url = string.Format(CultureInfo.InvariantCulture, GetMovieInfo3, id, TmdbUtils.ApiKey);
-
- if (!string.IsNullOrEmpty(language))
- {
- url += string.Format(CultureInfo.InvariantCulture, "&language={0}", NormalizeLanguage(language));
-
- // Get images in english and with no language
- url += "&include_image_language=" + GetImageLanguagesParam(language);
- }
-
- 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 mainResponse = await GetMovieDbResponse(requestMessage, cancellationToken).ConfigureAwait(false);
- if (mainResponse.StatusCode == HttpStatusCode.NotFound)
- {
- return null;
- }
-
- await using var stream = await mainResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
- var mainResult = await _jsonSerializer.DeserializeFromStreamAsync(stream).ConfigureAwait(false);
-
- 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("MovieDbProvider couldn't find meta for language " + language + ". Trying English...");
-
- url = string.Format(CultureInfo.InvariantCulture, GetMovieInfo3, id, TmdbUtils.ApiKey) + "&language=en";
-
- if (!string.IsNullOrEmpty(language))
- {
- // Get images in english and with no language
- url += "&include_image_language=" + GetImageLanguagesParam(language);
- }
-
- using var langRequestMessage = new HttpRequestMessage(HttpMethod.Get, url);
- foreach (var header in TmdbUtils.AcceptHeaders)
- {
- langRequestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
- }
-
- using var langResponse = await GetMovieDbResponse(langRequestMessage, cancellationToken).ConfigureAwait(false);
-
- await using var langStream = await langResponse.Content.ReadAsStreamAsync().ConfigureAwait(false);
- var langResult = await _jsonSerializer.DeserializeFromStreamAsync(stream).ConfigureAwait(false);
- mainResult.Overview = langResult.Overview;
- }
-
- return mainResult;
- }
-
- ///
- /// Gets the movie db response.
- ///
- /// A representing the asynchronous operation.
- internal Task GetMovieDbResponse(HttpRequestMessage message, CancellationToken cancellationToken = default)
- {
- message.Headers.UserAgent.ParseAdd(_appHost.ApplicationUserAgent);
- return _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(message, cancellationToken);
+ return metadataResult;
}
///
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbSearch.cs
deleted file mode 100644
index 36a4eef8a3..0000000000
--- 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> GetSearchResults(SeriesInfo idInfo, CancellationToken cancellationToken)
- {
- return GetSearchResults(idInfo, "tv", cancellationToken);
- }
-
- public Task> GetMovieSearchResults(ItemLookupInfo idInfo, CancellationToken cancellationToken)
- {
- return GetSearchResults(idInfo, "movie", cancellationToken);
- }
-
- public Task> GetSearchResults(BoxSetInfo idInfo, CancellationToken cancellationToken)
- {
- return GetSearchResults(idInfo, "collection", cancellationToken);
- }
-
- private async Task> GetSearchResults(ItemLookupInfo idInfo, string searchType, CancellationToken cancellationToken)
- {
- var name = idInfo.Name;
- var year = idInfo.Year;
-
- if (string.IsNullOrWhiteSpace(name))
- {
- return new List();
- }
-
- 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> 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> 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>(stream).ConfigureAwait(false);
-
- var results = searchResults.Results ?? new List();
-
- 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> 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>(stream).ConfigureAwait(false);
-
- var results = searchResults.Results ?? new List();
-
- 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 c7ba974386..0000000000
--- 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 b88ecce87f..0000000000
--- 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
- {
- public string Name => TmdbMovieProvider.Current.Name;
-
- public Task> GetMetadata(MusicVideoInfo info, CancellationToken cancellationToken)
- {
- return TmdbMovieProvider.Current.GetItemMetadata(info, cancellationToken);
- }
-
- public Task> GetSearchResults(MusicVideoInfo searchInfo, CancellationToken cancellationToken)
- {
- return Task.FromResult((IEnumerable)new List());
- }
-
- public Task 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 f2d2c8120e..3f57c4bc4c 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;
-
///
- public string Name => ProviderName;
+ public string Name => TmdbUtils.ProviderName;
///
public int Order => 0;
@@ -56,78 +49,37 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
public async Task> GetImages(BaseItem item, CancellationToken cancellationToken)
{
var person = (Person)item;
- var id = person.GetProviderId(MetadataProvider.Tmdb);
+ var personTmdbId = Convert.ToInt32(person.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
- if (!string.IsNullOrEmpty(id))
+ if (personTmdbId > 0)
{
- await TmdbPersonProvider.Current.EnsurePersonInfo(id, cancellationToken).ConfigureAwait(false);
-
- var dataFilePath = TmdbPersonProvider.GetPersonDataFilePath(_config.ApplicationPaths, id);
-
- var result = _jsonSerializer.DeserializeFromFile(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();
- }
-
- private IEnumerable GetImages(PersonImages images, string preferredLanguage, string baseImageUrl)
- {
- var list = new List();
-
- if (images.Profiles != null)
- {
- 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();
}
- if (!isLanguageEn)
+ var remoteImages = new List();
+ var language = item.GetPreferredMetadataLanguage();
+
+ for (var i = 0; i < personResult.Images.Profiles.Count; i++)
{
- if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
+ var image = personResult.Images.Profiles[i];
+ remoteImages.Add(new RemoteImageInfo
{
- return 2;
- }
+ ProviderName = Name,
+ Type = ImageType.Primary,
+ Width = image.Width,
+ Height = image.Height,
+ Language = TmdbUtils.AdjustImageLanguage(image.Iso_639_1, language),
+ Url = _tmdbClientManager.GetProfileUrl(image.FilePath)
+ });
}
- if (string.IsNullOrEmpty(i.Language))
- {
- return isLanguageEn ? 3 : 2;
- }
+ return remoteImages.OrderByLanguageDescending(language);
+ }
- return 0;
- })
- .ThenByDescending(i => i.CommunityRating ?? 0)
- .ThenByDescending(i => i.VoteCount ?? 0);
- }
-
- private string GetLanguage(Profile profile)
- {
- return profile.Iso_639_1?.ToString();
+ return Enumerable.Empty();
}
public Task GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
index 777ebce492..4384c203e5 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
{
- 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> GetSearchResults(PersonLookupInfo searchInfo, CancellationToken cancellationToken)
{
- var tmdbId = searchInfo.GetProviderId(MetadataProvider.Tmdb);
+ var personTmdbId = Convert.ToInt32(searchInfo.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
-
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
-
- 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(dataFilePath);
-
- IReadOnlyList images = info.Images?.Profiles ?? Array.Empty();
-
- var result = new RemoteSearchResult
+ if (personResult != null)
{
- Name = info.Name,
+ var result = new RemoteSearchResult
+ {
+ Name = personResult.Name,
+ SearchProviderName = Name,
+ Overview = personResult.Biography
+ };
- SearchProviderName = Name,
+ if (personResult.Images?.Profiles != null && personResult.Images.Profiles.Count > 0)
+ {
+ result.ImageUrl = _tmdbClientManager.GetProfileUrl(personResult.Images.Profiles[0].FilePath);
+ }
- ImageUrl = images.Count == 0 ? null : (tmdbImageUrl + images[0].File_Path)
- };
+ result.SetProviderId(MetadataProvider.Tmdb, personResult.Id.ToString(CultureInfo.InvariantCulture));
+ result.SetProviderId(MetadataProvider.Imdb, personResult.ExternalIds.ImdbId);
- result.SetProviderId(MetadataProvider.Tmdb, info.Id.ToString(_usCulture));
- result.SetProviderId(MetadataProvider.Imdb, info.Imdb_Id);
-
- 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();
+ return Enumerable.Empty();
}
- 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();
+ for (var i = 0; i < personSearchResult.Count; i++)
{
- requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(header));
+ var person = personSearchResult[i];
+ var remoteSearchResult = new RemoteSearchResult
+ {
+ SearchProviderName = Name,
+ Name = person.Name,
+ ImageUrl = _tmdbClientManager.GetProfileUrl(person.ProfilePath)
+ };
+
+ remoteSearchResult.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
+ remoteSearchResults.Add(remoteSearchResult);
}
- 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>(stream).ConfigureAwait(false)
- ?? new TmdbSearchResult();
-
- 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
- };
-
- result.SetProviderId(MetadataProvider.Tmdb, i.Id.ToString(_usCulture));
-
- return result;
+ return remoteSearchResults;
}
public async Task> 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();
- 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;
- }
+ var person = await _tmdbClientManager.GetPersonAsync(personTmdbId, cancellationToken).ConfigureAwait(false);
- throw;
- }
-
- var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, tmdbId);
-
- var info = _jsonSerializer.DeserializeFromFile(dataFilePath);
-
- 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 };
+ // 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 (!string.IsNullOrWhiteSpace(person.PlaceOfBirth))
+ {
+ item.ProductionLocations = new[] { person.PlaceOfBirth };
}
- item.Overview = info.Biography;
+ item.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
- if (DateTime.TryParseExact(info.Birthday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out var date))
+ if (!string.IsNullOrEmpty(person.ImdbId))
{
- item.PremiereDate = date.ToUniversalTime();
- }
-
- if (DateTime.TryParseExact(info.Deathday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out date))
- {
- item.EndDate = date.ToUniversalTime();
- }
-
- item.SetProviderId(MetadataProvider.Tmdb, info.Id.ToString(_usCulture));
-
- if (!string.IsNullOrEmpty(info.Imdb_Id))
- {
- 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;
}
- ///
- /// Gets the TMDB id.
- ///
- /// The information.
- /// The cancellation token.
- /// Task{System.String}.
- private async Task 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 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 c56774f8e7..3b7a0b254e 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 string Name => TmdbUtils.ProviderName;
+ public TmdbEpisodeImageProvider(IHttpClientFactory httpClientFactory, TmdbClientManager tmdbClientManager)
+ {
+ _httpClientFactory = httpClientFactory;
+ _tmdbClientManager = tmdbClientManager;
+ }
// After TheTvDb
public int Order => 1;
+ public string Name => TmdbUtils.ProviderName;
+
public IEnumerable GetSupportedImages(BaseItem item)
{
return new List
@@ -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();
-
- if (string.IsNullOrEmpty(seriesId))
+ if (seriesTmdbId <= 0)
{
- return list;
+ return Enumerable.Empty();
}
var seasonNumber = episode.ParentIndexNumber;
@@ -63,71 +58,45 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (!seasonNumber.HasValue || !episodeNumber.HasValue)
{
- return list;
+ return Enumerable.Empty();
}
var language = item.GetPreferredMetadataLanguage();
- var response = await GetEpisodeInfo(
- seriesId,
- seasonNumber.Value,
- episodeNumber.Value,
- language,
- cancellationToken).ConfigureAwait(false);
+ var episodeResult = await _tmdbClientManager
+ .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken)
+ .ConfigureAwait(false);
- var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false);
-
- var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original");
-
- list.AddRange(GetPosters(response.Images).Select(i => new RemoteImageInfo
+ var stills = episodeResult?.Images?.Stills;
+ if (stills == null)
{
- 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
- }));
+ return Enumerable.Empty();
+ }
- var isLanguageEn = string.Equals(language, "en", StringComparison.OrdinalIgnoreCase);
-
- return list.OrderByDescending(i =>
+ var remoteImages = new RemoteImageInfo[stills.Count];
+ for (var i = 0; i < stills.Count; i++)
{
- if (string.Equals(language, i.Language, StringComparison.OrdinalIgnoreCase))
+ var image = stills[i];
+ remoteImages[i] = new RemoteImageInfo
{
- return 3;
- }
+ 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
+ };
+ }
- 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 IEnumerable GetPosters(StillImages images)
- {
- return images.Stills ?? new List();
+ return remoteImages.OrderByLanguageDescending(language);
}
public Task 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 a7e3a03fe3..93998a1102 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,
- IHasOrder
+ public class TmdbEpisodeProvider : IRemoteMetadataProvider, 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,21 +34,24 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public async Task> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
{
- var list = new List();
-
// The search query must either provide an episode number or date
if (!searchInfo.IndexNumber.HasValue || !searchInfo.ParentIndexNumber.HasValue)
{
- return list;
+ return Enumerable.Empty();
}
var metadataResult = await GetMetadata(searchInfo, cancellationToken).ConfigureAwait(false);
- if (metadataResult.HasMetadata)
+ if (!metadataResult.HasMetadata)
{
- var item = metadataResult.Item;
+ return Enumerable.Empty();
+ }
- list.Add(new RemoteSearchResult
+ var item = metadataResult.Item;
+
+ return new[]
+ {
+ new RemoteSearchResult
{
IndexNumber = item.IndexNumber,
Name = item.Name,
@@ -63,27 +61,26 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
ProviderIds = item.ProviderIds,
SearchProviderName = Name,
IndexNumberEnd = item.IndexNumberEnd
- });
- }
-
- return list;
+ }
+ };
}
public async Task> GetMetadata(EpisodeInfo info, CancellationToken cancellationToken)
{
- var result = new MetadataResult();
+ var metadataResult = new MetadataResult();
// 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 +88,120 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
if (!seasonNumber.HasValue || !episodeNumber.HasValue)
{
- return result;
+ return metadataResult;
}
- try
+ var episodeResult = await _tmdbClientManager
+ .GetEpisodeAsync(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
+ .ConfigureAwait(false);
+
+ if (episodeResult == null)
{
- var response = await GetEpisodeInfo(seriesTmdbId, seasonNumber.Value, episodeNumber.Value, info.MetadataLanguage, cancellationToken).ConfigureAwait(false);
-
- result.HasMetadata = true;
- result.QueriedById = true;
-
- if (!string.IsNullOrEmpty(response.Overview))
- {
- // if overview is non-empty, we can assume that localized data was returned
- result.ResultLanguage = info.MetadataLanguage;
- }
-
- var item = new Episode();
- result.Item = item;
-
- item.Name = info.Name;
- item.IndexNumber = info.IndexNumber;
- item.ParentIndexNumber = info.ParentIndexNumber;
- item.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));
- }
-
- item.PremiereDate = response.Air_Date;
- item.ProductionYear = result.Item.PremiereDate.Value.Year;
-
- item.Name = response.Name;
- item.Overview = response.Overview;
-
- item.CommunityRating = (float)response.Vote_Average;
-
- if (response.Videos?.Results != null)
- {
- foreach (var video in response.Videos.Results)
- {
- 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);
- }
- }
- }
- }
-
- result.ResetPeople();
-
- var credits = response.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.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 });
- }
- }
-
- // and the rest from crew
- if (credits.Crew != null)
- {
- 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 });
- }
- }
- }
+ return metadataResult;
}
- catch (HttpException ex)
+
+ metadataResult.HasMetadata = true;
+ metadataResult.QueriedById = true;
+
+ if (!string.IsNullOrEmpty(episodeResult.Overview))
{
- if (ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound)
- {
- return result;
- }
-
- throw;
+ // if overview is non-empty, we can assume that localized data was returned
+ metadataResult.ResultLanguage = info.MetadataLanguage;
}
- return result;
+ var item = new Episode
+ {
+ Name = info.Name,
+ IndexNumber = info.IndexNumber,
+ ParentIndexNumber = info.ParentIndexNumber,
+ IndexNumberEnd = info.IndexNumberEnd
+ };
+
+ if (!string.IsNullOrEmpty(episodeResult.ExternalIds?.TvdbId))
+ {
+ item.SetProviderId(MetadataProvider.Tvdb, episodeResult.ExternalIds.TvdbId);
+ }
+
+ item.PremiereDate = episodeResult.AirDate;
+ item.ProductionYear = episodeResult.AirDate?.Year;
+
+ item.Name = episodeResult.Name;
+ item.Overview = episodeResult.Overview;
+
+ item.CommunityRating = Convert.ToSingle(episodeResult.VoteAverage);
+
+ if (episodeResult.Videos?.Results != null)
+ {
+ foreach (var video in episodeResult.Videos.Results)
+ {
+ if (TmdbUtils.IsTrailerType(video))
+ {
+ item.AddTrailerUrl("https://www.youtube.com/watch?v=" + video.Key);
+ }
+ }
+ }
+
+ var credits = episodeResult.Credits;
+
+ if (credits?.Cast != null)
+ {
+ foreach (var actor in credits.Cast.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers))
+ {
+ metadataResult.AddPerson(new PersonInfo
+ {
+ Name = actor.Name.Trim(),
+ Role = actor.Character,
+ Type = PersonType.Actor,
+ SortOrder = actor.Order
+ });
+ }
+ }
+
+ if (credits?.GuestStars != null)
+ {
+ foreach (var guest in credits.GuestStars.OrderBy(a => a.Order).Take(TmdbUtils.MaxCastMembers))
+ {
+ metadataResult.AddPerson(new PersonInfo
+ {
+ Name = guest.Name.Trim(),
+ Role = guest.Character,
+ Type = PersonType.GuestStar,
+ SortOrder = guest.Order
+ });
+ }
+ }
+
+ // and the rest from crew
+ if (credits?.Crew != null)
+ {
+ foreach (var person in credits.Crew)
+ {
+ // Normalize this
+ var type = TmdbUtils.MapCrewToPersonType(person);
+
+ if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
+ && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ metadataResult.AddPerson(new PersonInfo
+ {
+ Name = person.Name.Trim(),
+ Role = person.Job,
+ Type = type
+ });
+ }
+ }
+
+ metadataResult.Item = item;
+
+ return metadataResult;
}
public Task 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 34d2424a34..0000000000
--- 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 _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();
- }
-
- protected ILogger Logger => _logger;
-
- protected async Task 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(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 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(stream).ConfigureAwait(false);
- }
-
- protected Task 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 dcc7f87002..f4ed480aef 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 GetImageResponse(string url, CancellationToken cancellationToken)
{
@@ -45,87 +41,46 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
public async Task> GetImages(BaseItem item, CancellationToken cancellationToken)
{
var season = (Season)item;
- var series = season.Series;
+ var series = season?.Series;
- var seriesId = series?.GetProviderId(MetadataProvider.Tmdb);
+ var seriesTmdbId = Convert.ToInt32(series?.GetProviderId(MetadataProvider.Tmdb), CultureInfo.InvariantCulture);
- if (string.IsNullOrEmpty(seriesId))
- {
- return Enumerable.Empty();
- }
-
- var seasonNumber = season.IndexNumber;
-
- if (!seasonNumber.HasValue)
+ if (seriesTmdbId <= 0 || season?.IndexNumber == null)
{
return Enumerable.Empty();
}
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
+ var posters = seasonResult?.Images?.Posters;
+ if (posters == null)
{
- 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 (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> 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);
-
- if (!string.IsNullOrEmpty(path))
- {
- if (File.Exists(path))
- {
- return _jsonSerializer.DeserializeFromFile(path).Images.Posters;
- }
+ return Enumerable.Empty();
}
- return null;
+ var remoteImages = new RemoteImageInfo[posters.Count];
+ for (var i = 0; i < posters.Count; i++)
+ {
+ var image = posters[i];
+ remoteImages[i] = new RemoteImageInfo
+ {
+ 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 remoteImages.OrderByLanguageDescending(language);
}
public IEnumerable GetSupportedImages(BaseItem item)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
index c9b257fcc2..6ca462474a 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
{
- 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 _logger;
+ private readonly TmdbClientManager _tmdbClientManager;
- internal static TmdbSeasonProvider Current { get; private set; }
-
- public TmdbSeasonProvider(
- IHttpClientFactory httpClientFactory,
- IServerConfigurationManager configurationManager,
- IFileSystem fileSystem,
- IJsonSerializer jsonSerializer,
- ILogger logger)
+ 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
+ return result;
+ }
+
+ var seasonResult = await _tmdbClientManager
+ .GetSeasonAsync(Convert.ToInt32(seriesTmdbId, CultureInfo.InvariantCulture), seasonNumber.Value, info.MetadataLanguage, TmdbUtils.GetImageLanguagesParam(info.MetadataLanguage), cancellationToken)
+ .ConfigureAwait(false);
+
+ if (seasonResult == null)
+ {
+ return result;
+ }
+
+ result.HasMetadata = true;
+ result.Item = new Season
+ {
+ Name = info.Name,
+ IndexNumber = seasonNumber,
+ Overview = seasonResult?.Overview
+ };
+
+ if (!string.IsNullOrEmpty(seasonResult.ExternalIds?.TvdbId))
+ {
+ result.Item.SetProviderId(MetadataProvider.Tvdb, seasonResult.ExternalIds.TvdbId);
+ }
+
+ // TODO why was this disabled?
+ var credits = seasonResult.Credits;
+ if (credits?.Cast != null)
+ {
+ var cast = credits.Cast.OrderBy(c => c.Order).Take(TmdbUtils.MaxCastMembers).ToList();
+ for (var i = 0; i < cast.Count; i++)
{
- 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.AddPerson(new PersonInfo
{
- 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;
+ Name = cast[i].Name.Trim(),
+ Role = cast[i].Character,
+ Type = PersonType.Actor,
+ SortOrder = cast[i].Order
+ });
}
}
+ if (credits?.Crew != null)
+ {
+ foreach (var person in credits.Crew)
+ {
+ // Normalize this
+ var type = TmdbUtils.MapCrewToPersonType(person);
+
+ if (!TmdbUtils.WantedCrewTypes.Contains(type, StringComparer.OrdinalIgnoreCase)
+ && !TmdbUtils.WantedCrewTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ result.AddPerson(new PersonInfo
+ {
+ Name = person.Name.Trim(),
+ Role = person.Job,
+ Type = type
+ });
+ }
+ }
+
+ result.Item.PremiereDate = seasonResult.AirDate;
+ result.Item.ProductionYear = seasonResult.AirDate?.Year;
+
return result;
}
public Task> GetSearchResults(SeasonInfo searchInfo, CancellationToken cancellationToken)
{
- return Task.FromResult>(new List());
+ return Task.FromResult(Enumerable.Empty());
}
public Task GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
}
-
- private async Task 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(dataFilePath);
- }
-
- internal Task EnsureSeasonInfo(string tmdbId, int seasonNumber, 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, 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 DownloadSeasonInfo(tmdbId, seasonNumber, language, cancellationToken);
- }
-
- internal string GetDataFilePath(string tmdbId, int seasonNumber, 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}-{1}.json",
- seasonNumber.ToString(CultureInfo.InvariantCulture),
- preferredLanguage);
-
- return Path.Combine(path, filename);
- }
-
- internal async Task DownloadSeasonInfo(string id, int seasonNumber, string preferredMetadataLanguage, 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);
- }
-
- internal async Task FetchMainResult(string id, int seasonNumber, string language, 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(stream).ConfigureAwait(false);
- }
}
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
index 179ceb825d..d0c6b8b886 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;
@@ -54,107 +49,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
}
public async Task> GetImages(BaseItem item, CancellationToken cancellationToken)
- {
- var list = new List();
-
- var results = await FetchImages(item, null, cancellationToken).ConfigureAwait(false);
-
- if (results == null)
- {
- return list;
- }
-
- 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);
- }
-
- ///
- /// Gets the posters.
- ///
- /// The images.
- private IEnumerable GetPosters(Images images)
- {
- return images.Posters ?? new List();
- }
-
- ///
- /// Gets the backdrops.
- ///
- /// The images.
- private IEnumerable GetBackdrops(Images images)
- {
- var eligibleBackdrops = images.Backdrops ?? new List();
-
- return eligibleBackdrops.OrderByDescending(i => i.Vote_Average)
- .ThenByDescending(i => i.Vote_Count);
- }
-
- ///
- /// Fetches the images.
- ///
- /// The item.
- /// The language.
- /// The cancellation token.
- /// Task{MovieImages}.
- private async Task FetchImages(
- BaseItem item,
- string language,
- CancellationToken cancellationToken)
{
var tmdbId = item.GetProviderId(MetadataProvider.Tmdb);
@@ -163,16 +57,56 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return null;
}
- await TmdbSeriesProvider.Current.EnsureSeriesInfo(tmdbId, language, cancellationToken).ConfigureAwait(false);
+ var language = item.GetPreferredMetadataLanguage();
- var path = TmdbSeriesProvider.Current.GetDataFilePath(tmdbId, language);
+ var series = await _tmdbClientManager
+ .GetSeriesAsync(Convert.ToInt32(tmdbId, CultureInfo.InvariantCulture), language, TmdbUtils.GetImageLanguagesParam(language), cancellationToken)
+ .ConfigureAwait(false);
- if (!string.IsNullOrEmpty(path) && File.Exists(path))
+ if (series?.Images == null)
{
- return _jsonSerializer.DeserializeFromFile(path).Images;
+ return Enumerable.Empty();
}
- return null;
+ var posters = series.Images.Posters;
+ var backdrops = series.Images.Backdrops;
+
+ var remoteImages = new RemoteImageInfo[posters.Count + backdrops.Count];
+
+ for (var i = 0; i < posters.Count; i++)
+ {
+ var poster = posters[i];
+ remoteImages[i] = 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 < backdrops.Count; i++)
+ {
+ var backdrop = series.Images.Backdrops[i];
+ remoteImages[posters.Count + i] = 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 remoteImages.OrderByLanguageDescending(language);
}
public Task GetImageResponse(string url, CancellationToken cancellationToken)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
index 287ebca8c9..942c85b90d 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
@@ -3,107 +3,81 @@
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, 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 _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 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