diff options
94 files changed, 1095 insertions, 648 deletions
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml index b34253190..272111299 100644 --- a/.ci/azure-pipelines-package.yml +++ b/.ci/azure-pipelines-package.yml @@ -35,7 +35,6 @@ jobs: steps: - script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment' displayName: 'Build Dockerfile' - condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) - script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)' displayName: 'Run Dockerfile (unstable)' @@ -47,14 +46,19 @@ jobs: - task: PublishPipelineArtifact@1 displayName: 'Publish Release' - condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) inputs: targetPath: '$(Build.SourcesDirectory)/deployment/dist' artifactName: 'jellyfin-server-$(BuildConfiguration)' + - task: SSH@0 + displayName: 'Create target directory on repository server' + inputs: + sshEndpoint: repository + runOptions: 'inline' + inline: 'mkdir -p /srv/repository/incoming/azure/$(Build.BuildNumber)/$(BuildConfiguration)' + - task: CopyFilesOverSSH@0 displayName: 'Upload artifacts to repository server' - condition: or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) inputs: sshEndpoint: repository sourceFolder: '$(Build.SourcesDirectory)/deployment/dist' @@ -120,7 +124,10 @@ jobs: inputs: sshEndpoint: repository runOptions: 'inline' - inline: 'sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable' + inline: | + sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable + rm $0 + exit - task: SSH@0 displayName: 'Update Stable Repository' @@ -128,4 +135,29 @@ jobs: inputs: sshEndpoint: repository runOptions: 'inline' - inline: 'sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber)' + inline: | + sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) + rm $0 + exit + +- job: PublishNuget + displayName: 'Publish NuGet packages' + dependsOn: + - BuildPackage + condition: and(succeeded('BuildPackage'), startsWith(variables['Build.SourceBranch'], 'refs/tags')) + + pool: + vmImage: 'ubuntu-latest' + + steps: + - task: NuGetCommand@2 + inputs: + command: 'pack' + packagesToPack: Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj + packDestination: '$(Build.ArtifactStagingDirectory)' + + - task: NuGetCommand@2 + inputs: + command: 'push' + packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg' + includeNugetOrg: 'true' diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml index c9013b3b8..0c86c0171 100644 --- a/.ci/azure-pipelines.yml +++ b/.ci/azure-pipelines.yml @@ -15,11 +15,13 @@ trigger: batch: true jobs: +- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}: - template: azure-pipelines-main.yml parameters: LinuxImage: 'ubuntu-latest' RestoreBuildProjects: $(RestoreBuildProjects) +- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}: - template: azure-pipelines-test.yml parameters: ImageNames: @@ -27,6 +29,7 @@ jobs: Windows: 'windows-latest' macOS: 'macos-latest' +- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}: - template: azure-pipelines-abi.yml parameters: Packages: @@ -44,4 +47,5 @@ jobs: AssemblyFileName: MediaBrowser.Common.dll LinuxImage: 'ubuntu-latest' +- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}: - template: azure-pipelines-package.yml diff --git a/Emby.Dlna/Api/DlnaServerService.cs b/Emby.Dlna/Api/DlnaServerService.cs index 7fba2184a..d9c1669b0 100644 --- a/Emby.Dlna/Api/DlnaServerService.cs +++ b/Emby.Dlna/Api/DlnaServerService.cs @@ -11,6 +11,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; namespace Emby.Dlna.Api { @@ -108,7 +109,7 @@ namespace Emby.Dlna.Api public string Filename { get; set; } } - public class DlnaServerService : IService, IRequiresRequest + public class DlnaServerService : IService { private const string XMLContentType = "text/xml; charset=UTF-8"; @@ -127,11 +128,13 @@ namespace Emby.Dlna.Api public DlnaServerService( IDlnaManager dlnaManager, IHttpResultFactory httpResultFactory, - IServerConfigurationManager configurationManager) + IServerConfigurationManager configurationManager, + IHttpContextAccessor httpContextAccessor) { _dlnaManager = dlnaManager; _resultFactory = httpResultFactory; _configurationManager = configurationManager; + Request = httpContextAccessor?.HttpContext.GetServiceStackRequest() ?? throw new ArgumentNullException(nameof(httpContextAccessor)); } private string GetHeader(string name) diff --git a/Emby.Dlna/Profiles/DefaultProfile.cs b/Emby.Dlna/Profiles/DefaultProfile.cs index 2347ebd0d..90a23a4a2 100644 --- a/Emby.Dlna/Profiles/DefaultProfile.cs +++ b/Emby.Dlna/Profiles/DefaultProfile.cs @@ -164,7 +164,7 @@ namespace Emby.Dlna.Profiles public void AddXmlRootAttribute(string name, string value) { - var atts = XmlRootAttributes ?? new XmlAttribute[] { }; + var atts = XmlRootAttributes ?? System.Array.Empty<XmlAttribute>(); var list = atts.ToList(); list.Add(new XmlAttribute diff --git a/Emby.Dlna/Profiles/DenonAvrProfile.cs b/Emby.Dlna/Profiles/DenonAvrProfile.cs index 73a87c499..a5ba0f36c 100644 --- a/Emby.Dlna/Profiles/DenonAvrProfile.cs +++ b/Emby.Dlna/Profiles/DenonAvrProfile.cs @@ -28,7 +28,7 @@ namespace Emby.Dlna.Profiles }, }; - ResponseProfiles = new ResponseProfile[] { }; + ResponseProfiles = System.Array.Empty<ResponseProfile>(); } } } diff --git a/Emby.Dlna/Profiles/DirectTvProfile.cs b/Emby.Dlna/Profiles/DirectTvProfile.cs index 5ca388167..f6f98b07d 100644 --- a/Emby.Dlna/Profiles/DirectTvProfile.cs +++ b/Emby.Dlna/Profiles/DirectTvProfile.cs @@ -123,7 +123,7 @@ namespace Emby.Dlna.Profiles } }; - ResponseProfiles = new ResponseProfile[] { }; + ResponseProfiles = System.Array.Empty<ResponseProfile>(); } } } diff --git a/Emby.Dlna/Profiles/Foobar2000Profile.cs b/Emby.Dlna/Profiles/Foobar2000Profile.cs index ea3de686a..68e959770 100644 --- a/Emby.Dlna/Profiles/Foobar2000Profile.cs +++ b/Emby.Dlna/Profiles/Foobar2000Profile.cs @@ -72,7 +72,7 @@ namespace Emby.Dlna.Profiles } }; - ResponseProfiles = new ResponseProfile[] { }; + ResponseProfiles = System.Array.Empty<ResponseProfile>(); } } } diff --git a/Emby.Dlna/Profiles/MarantzProfile.cs b/Emby.Dlna/Profiles/MarantzProfile.cs index 6cfcc3b82..24078ab29 100644 --- a/Emby.Dlna/Profiles/MarantzProfile.cs +++ b/Emby.Dlna/Profiles/MarantzProfile.cs @@ -37,7 +37,7 @@ namespace Emby.Dlna.Profiles }, }; - ResponseProfiles = new ResponseProfile[] { }; + ResponseProfiles = System.Array.Empty<ResponseProfile>(); } } } diff --git a/Emby.Dlna/Profiles/MediaMonkeyProfile.cs b/Emby.Dlna/Profiles/MediaMonkeyProfile.cs index 7161af738..28d639b6d 100644 --- a/Emby.Dlna/Profiles/MediaMonkeyProfile.cs +++ b/Emby.Dlna/Profiles/MediaMonkeyProfile.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using System; using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -37,7 +38,7 @@ namespace Emby.Dlna.Profiles } }; - ResponseProfiles = new ResponseProfile[] { }; + ResponseProfiles = Array.Empty<ResponseProfile>(); } } } diff --git a/Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs b/Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs index 42b066d52..238fe9f6b 100644 --- a/Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs +++ b/Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using System; using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -223,7 +224,7 @@ namespace Emby.Dlna.Profiles } }; - ResponseProfiles = new ResponseProfile[] { }; + ResponseProfiles = Array.Empty<ResponseProfile>(); } } } diff --git a/Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs b/Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs index fbdf2c18e..812a48151 100644 --- a/Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs +++ b/Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using System; using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -223,7 +224,7 @@ namespace Emby.Dlna.Profiles } }; - ResponseProfiles = new ResponseProfile[] { }; + ResponseProfiles = Array.Empty<ResponseProfile>(); } } } diff --git a/Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs b/Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs index ce32179a1..6bfff322e 100644 --- a/Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs +++ b/Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using System; using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -211,7 +212,7 @@ namespace Emby.Dlna.Profiles } }; - ResponseProfiles = new ResponseProfile[] { }; + ResponseProfiles = Array.Empty<ResponseProfile>(); } } } diff --git a/Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs b/Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs index aa1721d39..ec2529574 100644 --- a/Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs +++ b/Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using System; using MediaBrowser.Model.Dlna; namespace Emby.Dlna.Profiles @@ -211,7 +212,7 @@ namespace Emby.Dlna.Profiles } }; - ResponseProfiles = new ResponseProfile[] { }; + ResponseProfiles = Array.Empty<ResponseProfile>(); } } } diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 18b6834ef..8e1bf9766 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -43,9 +43,9 @@ using Emby.Server.Implementations.Security; using Emby.Server.Implementations.Serialization; using Emby.Server.Implementations.Services; using Emby.Server.Implementations.Session; +using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.TV; using Emby.Server.Implementations.Updates; -using Emby.Server.Implementations.SyncPlay; using MediaBrowser.Api; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; @@ -78,8 +78,8 @@ using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Subtitles; -using MediaBrowser.Controller.TV; using MediaBrowser.Controller.SyncPlay; +using MediaBrowser.Controller.TV; using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.Model.Configuration; @@ -192,7 +192,7 @@ namespace Emby.Server.Implementations /// Gets or sets the application paths. /// </summary> /// <value>The application paths.</value> - protected ServerApplicationPaths ApplicationPaths { get; set; } + protected IServerApplicationPaths ApplicationPaths { get; set; } /// <summary> /// Gets or sets all concrete types. @@ -236,7 +236,7 @@ namespace Emby.Server.Implementations /// Initializes a new instance of the <see cref="ApplicationHost" /> class. /// </summary> public ApplicationHost( - ServerApplicationPaths applicationPaths, + IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IStartupOptions options, IFileSystem fileSystem, @@ -484,12 +484,10 @@ namespace Emby.Server.Implementations foreach (var plugin in Plugins) { - pluginBuilder.AppendLine( - string.Format( - CultureInfo.InvariantCulture, - "{0} {1}", - plugin.Name, - plugin.Version)); + pluginBuilder.Append(plugin.Name) + .Append(' ') + .Append(plugin.Version) + .AppendLine(); } Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString()); @@ -871,6 +869,11 @@ namespace Emby.Server.Implementations Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName); continue; } + catch (TypeLoadException ex) + { + Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName); + continue; + } foreach (Type type in exportedTypes) { @@ -1153,7 +1156,7 @@ namespace Emby.Server.Implementations return null; } - return GetLocalApiUrl(addresses.First()); + return GetLocalApiUrl(addresses[0]); } catch (Exception ex) { @@ -1226,7 +1229,7 @@ namespace Emby.Server.Implementations var addresses = ServerConfigurationManager .Configuration .LocalNetworkAddresses - .Select(NormalizeConfiguredLocalAddress) + .Select(x => NormalizeConfiguredLocalAddress(x)) .Where(i => i != null) .ToList(); @@ -1247,8 +1250,7 @@ namespace Emby.Server.Implementations } } - var valid = await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false); - if (valid) + if (await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false)) { resultList.Add(address); @@ -1262,13 +1264,12 @@ namespace Emby.Server.Implementations return resultList; } - public IPAddress NormalizeConfiguredLocalAddress(string address) + public IPAddress NormalizeConfiguredLocalAddress(ReadOnlySpan<char> address) { var index = address.Trim('/').IndexOf('/'); - if (index != -1) { - address = address.Substring(index + 1); + address = address.Slice(index + 1); } if (IPAddress.TryParse(address.Trim('/'), out IPAddress result)) diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index f7ad59c10..4351b9aa5 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -34,10 +34,10 @@ <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.5" /> - <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.5" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" /> - <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.5" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.6" /> + <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" /> + <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" /> <PackageReference Include="Mono.Nat" Version="2.0.1" /> <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" /> <PackageReference Include="ServiceStack.Text.Core" Version="5.9.0" /> diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index b207397bd..9486874d5 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -1,3 +1,4 @@ +using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Udp; @@ -48,8 +49,16 @@ namespace Emby.Server.Implementations.EntryPoints /// <inheritdoc /> public Task RunAsync() { - _udpServer = new UdpServer(_logger, _appHost, _config); - _udpServer.Start(PortNumber, _cancellationTokenSource.Token); + try + { + _udpServer = new UdpServer(_logger, _appHost, _config); + _udpServer.Start(PortNumber, _cancellationTokenSource.Token); + } + catch (SocketException ex) + { + _logger.LogWarning(ex, "Unable to start AutoDiscovery listener on UDP port {PortNumber}", PortNumber); + } + return Task.CompletedTask; } diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index a3a3f91b7..ab6483bf9 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -245,6 +245,16 @@ namespace Emby.Server.Implementations.IO if (info is FileInfo fileInfo) { result.Length = fileInfo.Length; + + // Issue #2354 get the size of files behind symbolic links + if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint)) + { + using (Stream thisFileStream = File.OpenRead(fileInfo.FullName)) + { + result.Length = thisFileStream.Length; + } + } + result.DirectoryName = fileInfo.DirectoryName; } diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 77d44e131..51c19fde7 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -341,7 +341,7 @@ namespace Emby.Server.Implementations.Library if (item is LiveTvProgram) { _logger.LogDebug( - "Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", + "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", item.GetType().Name, item.Name ?? "Unknown name", item.Path ?? string.Empty, @@ -350,7 +350,7 @@ namespace Emby.Server.Implementations.Library else { _logger.LogInformation( - "Deleting item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", + "Removing item, Type: {0}, Name: {1}, Path: {2}, Id: {3}", item.GetType().Name, item.Name ?? "Unknown name", item.Path ?? string.Empty, @@ -368,7 +368,12 @@ namespace Emby.Server.Implementations.Library continue; } - _logger.LogDebug("Deleting path {MetadataPath}", metadataPath); + _logger.LogDebug( + "Deleting metadata path, Type: {0}, Name: {1}, Path: {2}, Id: {3}", + item.GetType().Name, + item.Name ?? "Unknown name", + metadataPath, + item.Id); try { @@ -392,7 +397,13 @@ namespace Emby.Server.Implementations.Library { try { - _logger.LogDebug("Deleting path {path}", fileSystemInfo.FullName); + _logger.LogInformation( + "Deleting item path, Type: {0}, Name: {1}, Path: {2}, Id: {3}", + item.GetType().Name, + item.Name ?? "Unknown name", + fileSystemInfo.FullName, + item.Id); + if (fileSystemInfo.IsDirectory) { Directory.Delete(fileSystemInfo.FullName, true); @@ -1865,7 +1876,8 @@ namespace Emby.Server.Implementations.Library } var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray(); - if (outdated.Length == 0) + // Skip image processing if current or live tv source + if (outdated.Length == 0 || item.SourceType != SourceType.Library) { RegisterItem(item); return; @@ -3000,21 +3012,6 @@ namespace Emby.Server.Implementations.Library }); } - private static bool ValidateNetworkPath(string path) - { - // if (Environment.OSVersion.Platform == PlatformID.Win32NT) - //{ - // // We can't validate protocol-based paths, so just allow them - // if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) == -1) - // { - // return Directory.Exists(path); - // } - //} - - // Without native support for unc, we cannot validate this when running under mono - return true; - } - private const string ShortcutFileExtension = ".mblink"; public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo) @@ -3041,11 +3038,6 @@ namespace Emby.Server.Implementations.Library throw new FileNotFoundException("The path does not exist."); } - if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath)) - { - throw new FileNotFoundException("The network path does not exist."); - } - var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); @@ -3084,11 +3076,6 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(pathInfo)); } - if (!string.IsNullOrWhiteSpace(pathInfo.NetworkPath) && !ValidateNetworkPath(pathInfo.NetworkPath)) - { - throw new FileNotFoundException("The network path does not exist."); - } - var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index 4c1de3bcc..1b075d86a 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Library; @@ -28,7 +29,6 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; using Episode = MediaBrowser.Controller.Entities.TV.Episode; @@ -54,7 +54,6 @@ namespace Emby.Server.Implementations.LiveTv private readonly ILibraryManager _libraryManager; private readonly ITaskManager _taskManager; private readonly ILocalizationManager _localization; - private readonly IJsonSerializer _jsonSerializer; private readonly IFileSystem _fileSystem; private readonly IChannelManager _channelManager; private readonly LiveTvDtoService _tvDtoService; @@ -73,7 +72,6 @@ namespace Emby.Server.Implementations.LiveTv ILibraryManager libraryManager, ITaskManager taskManager, ILocalizationManager localization, - IJsonSerializer jsonSerializer, IFileSystem fileSystem, IChannelManager channelManager, LiveTvDtoService liveTvDtoService) @@ -85,7 +83,6 @@ namespace Emby.Server.Implementations.LiveTv _libraryManager = libraryManager; _taskManager = taskManager; _localization = localization; - _jsonSerializer = jsonSerializer; _fileSystem = fileSystem; _dtoService = dtoService; _userDataManager = userDataManager; @@ -2234,7 +2231,7 @@ namespace Emby.Server.Implementations.LiveTv public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true) { - info = _jsonSerializer.DeserializeFromString<TunerHostInfo>(_jsonSerializer.SerializeToString(info)); + info = JsonSerializer.Deserialize<TunerHostInfo>(JsonSerializer.Serialize(info)); var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); @@ -2278,7 +2275,7 @@ namespace Emby.Server.Implementations.LiveTv { // Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider // ServerConfiguration.SaveConfiguration crashes during xml serialization for AddListingProvider - info = _jsonSerializer.DeserializeFromString<ListingsProviderInfo>(_jsonSerializer.SerializeToString(info)); + info = JsonSerializer.Deserialize<ListingsProviderInfo>(JsonSerializer.Serialize(info)); var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json index 20447347b..e587c37d5 100644 --- a/Emby.Server.Implementations/Localization/Core/af.json +++ b/Emby.Server.Implementations/Localization/Core/af.json @@ -19,8 +19,8 @@ "Sync": "Sinkroniseer", "HeaderFavoriteSongs": "Gunsteling Liedjies", "Songs": "Liedjies", - "DeviceOnlineWithName": "{0} is verbind", - "DeviceOfflineWithName": "{0} het afgesluit", + "DeviceOnlineWithName": "{0} gekoppel is", + "DeviceOfflineWithName": "{0} is ontkoppel", "Collections": "Versamelings", "Inherit": "Ontvang", "HeaderLiveTV": "Live TV", @@ -91,5 +91,9 @@ "ChapterNameValue": "Hoofstuk", "CameraImageUploadedFrom": "'n Nuwe kamera photo opgelaai van {0}", "AuthenticationSucceededWithUserName": "{0} suksesvol geverifieer", - "Albums": "Albums" + "Albums": "Albums", + "TasksChannelsCategory": "Internet kanale", + "TasksApplicationCategory": "aansoek", + "TasksLibraryCategory": "biblioteek", + "TasksMaintenanceCategory": "onderhoud" } diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index 82df43be1..a6e9779c9 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -3,7 +3,7 @@ "AppDeviceValues": "App: {0}, Gerät: {1}", "Application": "Anwendung", "Artists": "Interpreten", - "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich authentifiziert", + "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich angemeldet", "Books": "Bücher", "CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen", "Channels": "Kanäle", diff --git a/Emby.Server.Implementations/Localization/Core/mr.json b/Emby.Server.Implementations/Localization/Core/mr.json index 50b6360d8..b6db2b0f2 100644 --- a/Emby.Server.Implementations/Localization/Core/mr.json +++ b/Emby.Server.Implementations/Localization/Core/mr.json @@ -57,5 +57,7 @@ "HeaderCameraUploads": "कॅमेरा अपलोड", "CameraImageUploadedFrom": "एक नवीन कॅमेरा चित्र {0} येथून अपलोड केले आहे", "Application": "अॅप्लिकेशन", - "AppDeviceValues": "अॅप: {0}, यंत्र: {1}" + "AppDeviceValues": "अॅप: {0}, यंत्र: {1}", + "Collections": "संग्रह", + "ChapterNameValue": "धडा {0}" } diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json index 79d078d4a..7f8df1289 100644 --- a/Emby.Server.Implementations/Localization/Core/ms.json +++ b/Emby.Server.Implementations/Localization/Core/ms.json @@ -5,47 +5,47 @@ "Artists": "Artis", "AuthenticationSucceededWithUserName": "{0} berjaya disahkan", "Books": "Buku-buku", - "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "CameraImageUploadedFrom": "Ada gambar dari kamera yang baru dimuat naik melalui {0}", "Channels": "Saluran", - "ChapterNameValue": "Chapter {0}", + "ChapterNameValue": "Bab {0}", "Collections": "Koleksi", - "DeviceOfflineWithName": "{0} has disconnected", - "DeviceOnlineWithName": "{0} is connected", + "DeviceOfflineWithName": "{0} telah diputuskan sambungan", + "DeviceOnlineWithName": "{0} telah disambung", "FailedLoginAttemptWithUserName": "Cubaan log masuk gagal dari {0}", - "Favorites": "Favorites", - "Folders": "Folders", + "Favorites": "Kegemaran", + "Folders": "Fail-fail", "Genres": "Genre-genre", - "HeaderAlbumArtists": "Album Artists", + "HeaderAlbumArtists": "Album Artis-artis", "HeaderCameraUploads": "Muatnaik Kamera", "HeaderContinueWatching": "Terus Menonton", - "HeaderFavoriteAlbums": "Favorite Albums", - "HeaderFavoriteArtists": "Favorite Artists", - "HeaderFavoriteEpisodes": "Favorite Episodes", - "HeaderFavoriteShows": "Favorite Shows", - "HeaderFavoriteSongs": "Favorite Songs", - "HeaderLiveTV": "Live TV", - "HeaderNextUp": "Next Up", - "HeaderRecordingGroups": "Recording Groups", - "HomeVideos": "Home videos", - "Inherit": "Inherit", - "ItemAddedWithName": "{0} was added to the library", - "ItemRemovedWithName": "{0} was removed from the library", + "HeaderFavoriteAlbums": "Album-album Kegemaran", + "HeaderFavoriteArtists": "Artis-artis Kegemaran", + "HeaderFavoriteEpisodes": "Episod-episod Kegemaran", + "HeaderFavoriteShows": "Rancangan-rancangan Kegemaran", + "HeaderFavoriteSongs": "Lagu-lagu Kegemaran", + "HeaderLiveTV": "TV Siaran Langsung", + "HeaderNextUp": "Seterusnya", + "HeaderRecordingGroups": "Kumpulan-kumpulan Rakaman", + "HomeVideos": "Video Personal", + "Inherit": "Mewarisi", + "ItemAddedWithName": "{0} telah ditambahkan ke dalam pustaka", + "ItemRemovedWithName": "{0} telah dibuang daripada pustaka", "LabelIpAddressValue": "Alamat IP: {0}", - "LabelRunningTimeValue": "Running time: {0}", - "Latest": "Latest", - "MessageApplicationUpdated": "Jellyfin Server has been updated", - "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", - "MessageServerConfigurationUpdated": "Server configuration has been updated", - "MixedContent": "Mixed content", - "Movies": "Movies", + "LabelRunningTimeValue": "Masa berjalan: {0}", + "Latest": "Terbaru", + "MessageApplicationUpdated": "Jellyfin Server telah dikemas kini", + "MessageApplicationUpdatedTo": "Jellyfin Server telah dikemas kini ke {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "Konfigurasi pelayan di bahagian {0} telah dikemas kini", + "MessageServerConfigurationUpdated": "Konfigurasi pelayan telah dikemas kini", + "MixedContent": "Kandungan campuran", + "Movies": "Filem", "Music": "Muzik", "MusicVideos": "Video muzik", - "NameInstallFailed": "{0} installation failed", - "NameSeasonNumber": "Season {0}", - "NameSeasonUnknown": "Season Unknown", - "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", - "NotificationOptionApplicationUpdateAvailable": "Application update available", + "NameInstallFailed": "{0} pemasangan gagal", + "NameSeasonNumber": "Musim {0}", + "NameSeasonUnknown": "Musim Tidak Diketahui", + "NewVersionIsAvailable": "Versi terbaru Jellyfin Server bersedia untuk dimuat turunkan.", + "NotificationOptionApplicationUpdateAvailable": "Kemas kini aplikasi telah sedia", "NotificationOptionApplicationUpdateInstalled": "Application update installed", "NotificationOptionAudioPlayback": "Audio playback started", "NotificationOptionAudioPlaybackStopped": "Audio playback stopped", diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index 5365fff23..b534d0bbe 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -104,5 +104,14 @@ "TaskRefreshChapterImagesDescription": "Cria miniaturas para vídeos que têm capítulos.", "TaskCleanCacheDescription": "Apaga ficheiros em cache que já não são usados pelo sistema.", "TasksChannelsCategory": "Canais de Internet", - "TaskRefreshChapterImages": "Extrair Imagens do Capítulo" + "TaskRefreshChapterImages": "Extrair Imagens do Capítulo", + "TaskDownloadMissingSubtitlesDescription": "Pesquisa na Internet as legendas em falta com base na configuração de metadados.", + "TaskDownloadMissingSubtitles": "Download das legendas em falta", + "TaskRefreshChannelsDescription": "Atualiza as informações do canal da Internet.", + "TaskCleanTranscodeDescription": "Apagar os ficheiros com mais de um dia, de Transcode.", + "TaskCleanTranscode": "Limpar o diretório de Transcode", + "TaskUpdatePluginsDescription": "Download e instala as atualizações para plug-ins configurados para atualização automática.", + "TaskRefreshPeopleDescription": "Atualiza os metadados para atores e diretores na tua biblioteca de media.", + "TaskRefreshPeople": "Atualizar pessoas", + "TaskRefreshLibraryDescription": "Pesquisa a tua biblioteca de media por novos ficheiros e atualiza os metadados." } diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json index 32538ac03..576aaeb1b 100644 --- a/Emby.Server.Implementations/Localization/Core/th.json +++ b/Emby.Server.Implementations/Localization/Core/th.json @@ -67,5 +67,7 @@ "Artists": "นักแสดง", "Application": "แอปพลิเคชั่น", "AppDeviceValues": "App: {0}, อุปกรณ์: {1}", - "Albums": "อัลบั้ม" + "Albums": "อัลบั้ม", + "ScheduledTaskStartedWithName": "{0} เริ่มต้น", + "ScheduledTaskFailedWithName": "{0} ล้มเหลว" } diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs index 6aa1dfbc9..50cb44b28 100644 --- a/Emby.Server.Implementations/Networking/NetworkManager.cs +++ b/Emby.Server.Implementations/Networking/NetworkManager.cs @@ -152,7 +152,12 @@ namespace Emby.Server.Implementations.Networking return true; } - byte[] octet = IPAddress.Parse(endpoint).GetAddressBytes(); + if (!IPAddress.TryParse(endpoint, out var ipAddress)) + { + return false; + } + + byte[] octet = ipAddress.GetAddressBytes(); if ((octet[0] == 10) || (octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918 @@ -268,6 +273,12 @@ namespace Emby.Server.Implementations.Networking string excludeAddress = "[" + addressString + "]"; var subnets = LocalSubnetsFn(); + // Include any address if LAN subnets aren't specified + if (subnets.Length == 0) + { + return true; + } + // Exclude any addresses if they appear in the LAN list in [ ] if (Array.IndexOf(subnets, excludeAddress) != -1) { diff --git a/Emby.Server.Implementations/Services/ServiceController.cs b/Emby.Server.Implementations/Services/ServiceController.cs index 857df591a..d884d4f37 100644 --- a/Emby.Server.Implementations/Services/ServiceController.cs +++ b/Emby.Server.Implementations/Services/ServiceController.cs @@ -189,5 +189,4 @@ namespace Emby.Server.Implementations.Services return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName()); } } - } diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs index a42f88ea0..3d4e1ca77 100644 --- a/Emby.Server.Implementations/Services/ServiceHandler.cs +++ b/Emby.Server.Implementations/Services/ServiceHandler.cs @@ -6,6 +6,7 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.HttpServer; +using MediaBrowser.Common.Extensions; using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -78,7 +79,8 @@ namespace Emby.Server.Implementations.Services var request = await CreateRequest(httpHost, httpReq, _restPath, logger).ConfigureAwait(false); httpHost.ApplyRequestFilters(httpReq, httpRes, request); - + + httpRes.HttpContext.SetServiceStackRequest(httpReq); var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false); // Apply response filters diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index d069d1ada..ca9f95c70 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -296,7 +296,7 @@ namespace Emby.Server.Implementations.Session } catch (DbUpdateConcurrencyException e) { - _logger.LogWarning(e, "Error updating user's last activity date."); + _logger.LogDebug(e, "Error updating user's last activity date."); } } } diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs index b1f8fd330..39d17833f 100644 --- a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs @@ -194,26 +194,24 @@ namespace Emby.Server.Implementations.SyncPlay } /// <inheritdoc /> - public void InitGroup(SessionInfo session, CancellationToken cancellationToken) + public void CreateGroup(SessionInfo session, CancellationToken cancellationToken) { _group.AddSession(session); _syncPlayManager.AddSessionToGroup(session, this); _group.PlayingItem = session.FullNowPlayingItem; - _group.IsPaused = true; + _group.IsPaused = session.PlayState.IsPaused; _group.PositionTicks = session.PlayState.PositionTicks ?? 0; _group.LastActivity = DateTime.UtcNow; var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow)); SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken); - var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause); - SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken); } /// <inheritdoc /> public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken) { - if (session.NowPlayingItem?.Id == _group.PlayingItem.Id && request.PlayingItemId == _group.PlayingItem.Id) + if (session.NowPlayingItem?.Id == _group.PlayingItem.Id) { _group.AddSession(session); _syncPlayManager.AddSessionToGroup(session, this); @@ -224,7 +222,7 @@ namespace Emby.Server.Implementations.SyncPlay var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName); SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken); - // Client join and play, syncing will happen client side + // Syncing will happen client-side if (!_group.IsPaused) { var playCommand = NewSyncPlayCommand(SendCommandType.Play); @@ -262,10 +260,9 @@ namespace Emby.Server.Implementations.SyncPlay /// <inheritdoc /> public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken) { - // The server's job is to mantain a consistent state to which clients refer to, - // as also to notify clients of state changes. - // The actual syncing of media playback happens client side. - // Clients are aware of the server's time and use it to sync. + // The server's job is to maintain a consistent state for clients to reference + // and notify clients of state changes. The actual syncing of media playback + // happens client side. Clients are aware of the server's time and use it to sync. switch (request.Type) { case PlaybackRequestType.Play: @@ -277,13 +274,13 @@ namespace Emby.Server.Implementations.SyncPlay case PlaybackRequestType.Seek: HandleSeekRequest(session, request, cancellationToken); break; - case PlaybackRequestType.Buffering: + case PlaybackRequestType.Buffer: HandleBufferingRequest(session, request, cancellationToken); break; - case PlaybackRequestType.BufferingDone: + case PlaybackRequestType.Ready: HandleBufferingDoneRequest(session, request, cancellationToken); break; - case PlaybackRequestType.UpdatePing: + case PlaybackRequestType.Ping: HandlePingUpdateRequest(session, request); break; } @@ -301,7 +298,7 @@ namespace Emby.Server.Implementations.SyncPlay { // Pick a suitable time that accounts for latency var delay = _group.GetHighestPing() * 2; - delay = delay < _group.DefaulPing ? _group.DefaulPing : delay; + delay = delay < _group.DefaultPing ? _group.DefaultPing : delay; // Unpause group and set starting point in future // Clients will start playback at LastActivity (datetime) from PositionTicks (playback position) @@ -337,8 +334,9 @@ namespace Emby.Server.Implementations.SyncPlay var currentTime = DateTime.UtcNow; var elapsedTime = currentTime - _group.LastActivity; _group.LastActivity = currentTime; + // Seek only if playback actually started - // (a pause request may be issued during the delay added to account for latency) + // Pause request may be issued during the delay added to account for latency _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0; var command = NewSyncPlayCommand(SendCommandType.Pause); @@ -451,7 +449,7 @@ namespace Emby.Server.Implementations.SyncPlay { // Client, that was buffering, resumed playback but did not update others in time delay = _group.GetHighestPing() * 2; - delay = delay < _group.DefaulPing ? _group.DefaulPing : delay; + delay = delay < _group.DefaultPing ? _group.DefaultPing : delay; _group.LastActivity = currentTime.AddMilliseconds( delay); @@ -495,7 +493,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.DefaulPing); + _group.UpdatePing(session, request.Ping ?? _group.DefaultPing); } /// <inheritdoc /> diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs index 45a43fd78..966ed5024 100644 --- a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs @@ -170,10 +170,11 @@ namespace Emby.Server.Implementations.SyncPlay { _logger.LogWarning("NewGroup: {0} does not have permission to create groups.", session.Id); - var error = new GroupUpdate<string>() + var error = new GroupUpdate<string> { Type = GroupUpdateType.CreateGroupDenied }; + _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); return; } @@ -188,7 +189,7 @@ namespace Emby.Server.Implementations.SyncPlay var group = new SyncPlayController(_sessionManager, this); _groups[group.GetGroupId()] = group; - group.InitGroup(session, cancellationToken); + group.CreateGroup(session, cancellationToken); } } @@ -205,6 +206,7 @@ namespace Emby.Server.Implementations.SyncPlay { Type = GroupUpdateType.JoinGroupDenied }; + _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); return; } @@ -300,9 +302,9 @@ namespace Emby.Server.Implementations.SyncPlay group => group.GetPlayingItemId().Equals(filterItemId) && HasAccessToItem(user, group.GetPlayingItemId())).Select( group => group.GetInfo()).ToList(); } - // Otherwise show all available groups else { + // Otherwise show all available groups return _groups.Values.Where( group => HasAccessToItem(user, group.GetPlayingItemId())).Select( group => group.GetInfo()).ToList(); @@ -322,6 +324,7 @@ namespace Emby.Server.Implementations.SyncPlay { Type = GroupUpdateType.JoinGroupDenied }; + _sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None); return; } @@ -366,7 +369,6 @@ namespace Emby.Server.Implementations.SyncPlay } _sessionToGroupMap.Remove(session.Id, out var tempGroup); - if (!tempGroup.GetGroupId().Equals(group.GetGroupId())) { throw new InvalidOperationException("Session was in wrong group!"); diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index 21c12ae79..d1818deff 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -117,23 +117,20 @@ namespace Emby.Server.Implementations.TV limit = limit.Value + 10; } - var items = _libraryManager.GetItemList(new InternalItemsQuery(user) - { - IncludeItemTypes = new[] { typeof(Episode).Name }, - OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) }, - SeriesPresentationUniqueKey = presentationUniqueKey, - Limit = limit, - DtoOptions = new DtoOptions - { - Fields = new ItemFields[] + var items = _libraryManager + .GetItemList( + new InternalItemsQuery(user) { - ItemFields.SeriesPresentationUniqueKey - }, - EnableImages = false - }, - GroupBySeriesPresentationUniqueKey = true - - }, parentsFolders.ToList()).Cast<Episode>().Select(GetUniqueSeriesKey); + IncludeItemTypes = new[] { typeof(Episode).Name }, + OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) }, + SeriesPresentationUniqueKey = presentationUniqueKey, + Limit = limit, + DtoOptions = new DtoOptions { Fields = new[] { ItemFields.SeriesPresentationUniqueKey }, EnableImages = false }, + GroupBySeriesPresentationUniqueKey = true + }, parentsFolders.ToList()) + .Cast<Episode>() + .Where(episode => !string.IsNullOrEmpty(episode.SeriesPresentationUniqueKey)) + .Select(GetUniqueSeriesKey); // Avoid implicitly captured closure var episodes = GetNextUpEpisodes(request, user, items, dtoOptions); diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 146ebaf25..4f54c06dd 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -148,6 +148,11 @@ namespace Emby.Server.Implementations.Updates _logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest); return Array.Empty<PackageInfo>(); } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest); + return Array.Empty<PackageInfo>(); + } } /// <inheritdoc /> diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 93c4612b6..3693d5122 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -14,7 +14,7 @@ <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" /> - <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.5" /> + <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.6" /> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" /> </ItemGroup> diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 282ea511c..8ce0f3848 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -19,9 +19,8 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.5" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.5" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="3.1.5" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.6" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.6" /> </ItemGroup> </Project> diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index dcac1b34b..21748ca19 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -24,11 +24,11 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.5"> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.6"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.5"> + <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.6"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index 53120a763..acc6eec1c 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using System; using System.Linq; using Jellyfin.Data; using Jellyfin.Data.Entities; @@ -133,6 +134,18 @@ namespace Jellyfin.Server.Implementations return base.SaveChanges(); } + /// <inheritdoc/> + public override void Dispose() + { + foreach (var entry in ChangeTracker.Entries()) + { + entry.State = EntityState.Detached; + } + + GC.SuppressFinalize(this); + base.Dispose(); + } + /// <inheritdoc /> protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index ace9c4af0..c5c89d37d 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -23,6 +23,7 @@ using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Events; using MediaBrowser.Model.Users; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Implementations.Users @@ -86,10 +87,31 @@ namespace Jellyfin.Server.Implementations.Users public event EventHandler<GenericEventArgs<User>>? OnUserLockedOut; /// <inheritdoc/> - public IEnumerable<User> Users => _dbProvider.CreateContext().Users; + public IEnumerable<User> Users + { + get + { + using var dbContext = _dbProvider.CreateContext(); + return dbContext.Users + .Include(user => user.Permissions) + .Include(user => user.Preferences) + .Include(user => user.AccessSchedules) + .Include(user => user.ProfileImage) + .ToList(); + } + } /// <inheritdoc/> - public IEnumerable<Guid> UsersIds => _dbProvider.CreateContext().Users.Select(u => u.Id); + public IEnumerable<Guid> UsersIds + { + get + { + using var dbContext = _dbProvider.CreateContext(); + return dbContext.Users + .Select(user => user.Id) + .ToList(); + } + } /// <inheritdoc/> public User? GetUserById(Guid id) @@ -99,7 +121,13 @@ namespace Jellyfin.Server.Implementations.Users throw new ArgumentException("Guid can't be empty", nameof(id)); } - return _dbProvider.CreateContext().Users.Find(id); + using var dbContext = _dbProvider.CreateContext(); + return dbContext.Users + .Include(user => user.Permissions) + .Include(user => user.Preferences) + .Include(user => user.AccessSchedules) + .Include(user => user.ProfileImage) + .FirstOrDefault(user => user.Id == id); } /// <inheritdoc/> @@ -110,9 +138,14 @@ namespace Jellyfin.Server.Implementations.Users throw new ArgumentException("Invalid username", nameof(name)); } - // This can't use an overload with StringComparer because that would cause the query to - // have to be evaluated client-side. - return _dbProvider.CreateContext().Users.FirstOrDefault(u => string.Equals(u.Username, name)); + using var dbContext = _dbProvider.CreateContext(); + return dbContext.Users + .Include(user => user.Permissions) + .Include(user => user.Preferences) + .Include(user => user.AccessSchedules) + .Include(user => user.ProfileImage) + .AsEnumerable() + .FirstOrDefault(u => string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase)); } /// <inheritdoc/> @@ -133,7 +166,7 @@ namespace Jellyfin.Server.Implementations.Users throw new ArgumentException("The new and old names must be different."); } - if (Users.Any(u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.OrdinalIgnoreCase))) + if (Users.Any(u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.Ordinal))) { throw new ArgumentException(string.Format( CultureInfo.InvariantCulture, @@ -150,7 +183,7 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public void UpdateUser(User user) { - var dbContext = _dbProvider.CreateContext(); + using var dbContext = _dbProvider.CreateContext(); dbContext.Users.Update(user); dbContext.SaveChanges(); } @@ -158,7 +191,7 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public async Task UpdateUserAsync(User user) { - var dbContext = _dbProvider.CreateContext(); + await using var dbContext = _dbProvider.CreateContext(); dbContext.Users.Update(user); await dbContext.SaveChangesAsync().ConfigureAwait(false); @@ -172,7 +205,7 @@ namespace Jellyfin.Server.Implementations.Users throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)"); } - var dbContext = _dbProvider.CreateContext(); + using var dbContext = _dbProvider.CreateContext(); // TODO: Remove after user item data is migrated. var max = dbContext.Users.Any() ? dbContext.Users.Select(u => u.InternalId).Max() : 0; @@ -195,8 +228,13 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public void DeleteUser(Guid userId) { - var dbContext = _dbProvider.CreateContext(); - var user = dbContext.Users.Find(userId); + using var dbContext = _dbProvider.CreateContext(); + var user = dbContext.Users + .Include(u => u.Permissions) + .Include(u => u.Preferences) + .Include(u => u.AccessSchedules) + .Include(u => u.ProfileImage) + .FirstOrDefault(u => u.Id == userId); if (user == null) { throw new ResourceNotFoundException(nameof(userId)); @@ -381,7 +419,7 @@ namespace Jellyfin.Server.Implementations.Users throw new ArgumentNullException(nameof(username)); } - var user = Users.ToList().FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); + var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); bool success; IAuthenticationProvider? authenticationProvider; @@ -409,8 +447,7 @@ namespace Jellyfin.Server.Implementations.Users // Search the database for the user again // the authentication provider might have created it - user = Users - .ToList().FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); + user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy) { @@ -484,7 +521,7 @@ namespace Jellyfin.Server.Implementations.Users } else { - IncrementInvalidLoginAttemptCount(user); + await IncrementInvalidLoginAttemptCount(user).ConfigureAwait(false); _logger.LogInformation( "Authentication request for {UserName} has been denied (IP: {IP}).", user.Username, @@ -547,7 +584,7 @@ namespace Jellyfin.Server.Implementations.Users public void Initialize() { // TODO: Refactor the startup wizard so that it doesn't require a user to already exist. - var dbContext = _dbProvider.CreateContext(); + using var dbContext = _dbProvider.CreateContext(); if (dbContext.Users.Any()) { @@ -609,8 +646,15 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public void UpdateConfiguration(Guid userId, UserConfiguration config) { - var dbContext = _dbProvider.CreateContext(); - var user = dbContext.Users.Find(userId) ?? throw new ArgumentException("No user exists with given Id!"); + using var dbContext = _dbProvider.CreateContext(); + var user = dbContext.Users + .Include(u => u.Permissions) + .Include(u => u.Preferences) + .Include(u => u.AccessSchedules) + .Include(u => u.ProfileImage) + .FirstOrDefault(u => u.Id == userId) + ?? throw new ArgumentException("No user exists with given Id!"); + user.SubtitleMode = config.SubtitleMode; user.HidePlayedInLatest = config.HidePlayedInLatest; user.EnableLocalPassword = config.EnableLocalPassword; @@ -635,8 +679,14 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public void UpdatePolicy(Guid userId, UserPolicy policy) { - var dbContext = _dbProvider.CreateContext(); - var user = dbContext.Users.Find(userId) ?? throw new ArgumentException("No user exists with given Id!"); + using var dbContext = _dbProvider.CreateContext(); + var user = dbContext.Users + .Include(u => u.Permissions) + .Include(u => u.Preferences) + .Include(u => u.AccessSchedules) + .Include(u => u.ProfileImage) + .FirstOrDefault(u => u.Id == userId) + ?? throw new ArgumentException("No user exists with given Id!"); // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0" int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch @@ -699,7 +749,7 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public void ClearProfileImage(User user) { - var dbContext = _dbProvider.CreateContext(); + using var dbContext = _dbProvider.CreateContext(); dbContext.Remove(user.ProfileImage); dbContext.SaveChanges(); } @@ -841,7 +891,7 @@ namespace Jellyfin.Server.Implementations.Users } } - private void IncrementInvalidLoginAttemptCount(User user) + private async Task IncrementInvalidLoginAttemptCount(User user) { user.InvalidLoginAttemptCount++; int? maxInvalidLogins = user.LoginAttemptsBeforeLockout; @@ -855,7 +905,7 @@ namespace Jellyfin.Server.Implementations.Users user.InvalidLoginAttemptCount); } - UpdateUser(user); + await UpdateUserAsync(user).ConfigureAwait(false); } } } diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index fe07411a6..71b0fd8f3 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -9,6 +9,7 @@ using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Activity; using Jellyfin.Server.Implementations.Users; using MediaBrowser.Common.Net; +using MediaBrowser.Controller; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Activity; @@ -33,9 +34,9 @@ namespace Jellyfin.Server /// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param> /// <param name="networkManager">The <see cref="INetworkManager" /> to be used by the <see cref="CoreAppHost" />.</param> public CoreAppHost( - ServerApplicationPaths applicationPaths, + IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, - StartupOptions options, + IStartupOptions options, IFileSystem fileSystem, INetworkManager networkManager) : base( @@ -66,8 +67,7 @@ namespace Jellyfin.Server // TODO: Set up scoping and use AddDbContextPool serviceCollection.AddDbContext<JellyfinDb>( options => options - .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}") - .UseLazyLoadingProxies(), + .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"), ServiceLifetime.Transient); serviceCollection.AddSingleton<JellyfinDbProvider>(); diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 6a2d252ab..b1bd38cff 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -41,8 +41,8 @@ <ItemGroup> <PackageReference Include="CommandLineParser" Version="2.8.0" /> - <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.5" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.5" /> + <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.6" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.6" /> <PackageReference Include="prometheus-net" Version="3.6.0" /> <PackageReference Include="prometheus-net.AspNetCore" Version="3.6.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" /> diff --git a/Jellyfin.Server/Migrations/IMigrationRoutine.cs b/Jellyfin.Server/Migrations/IMigrationRoutine.cs index 6b5780a26..c1000eede 100644 --- a/Jellyfin.Server/Migrations/IMigrationRoutine.cs +++ b/Jellyfin.Server/Migrations/IMigrationRoutine.cs @@ -18,6 +18,11 @@ namespace Jellyfin.Server.Migrations public string Name { get; } /// <summary> + /// Gets a value indicating whether to perform migration on a new install. + /// </summary> + public bool PerformOnNewInstall { get; } + + /// <summary> /// Execute the migration routine. /// </summary> public void Perform(); diff --git a/Jellyfin.Server/Migrations/MigrationRunner.cs b/Jellyfin.Server/Migrations/MigrationRunner.cs index d633c554d..98a90500c 100644 --- a/Jellyfin.Server/Migrations/MigrationRunner.cs +++ b/Jellyfin.Server/Migrations/MigrationRunner.cs @@ -21,7 +21,8 @@ namespace Jellyfin.Server.Migrations typeof(Routines.MigrateActivityLogDb), typeof(Routines.RemoveDuplicateExtras), typeof(Routines.AddDefaultPluginRepository), - typeof(Routines.MigrateUserDb) + typeof(Routines.MigrateUserDb), + typeof(Routines.ReaddDefaultPluginRepository) }; /// <summary> @@ -43,9 +44,8 @@ namespace Jellyfin.Server.Migrations // If startup wizard is not finished, this is a fresh install. // Don't run any migrations, just mark all of them as applied. logger.LogInformation("Marking all known migrations as applied because this is a fresh install"); - migrationOptions.Applied.AddRange(migrations.Select(m => (m.Id, m.Name))); + migrationOptions.Applied.AddRange(migrations.Where(m => !m.PerformOnNewInstall).Select(m => (m.Id, m.Name))); host.ServerConfigurationManager.SaveConfiguration(MigrationsListStore.StoreKey, migrationOptions); - return; } var appliedMigrationIds = migrationOptions.Applied.Select(m => m.Id).ToHashSet(); diff --git a/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs index a9d5ad16a..f6d8c9cc0 100644 --- a/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs +++ b/Jellyfin.Server/Migrations/Routines/AddDefaultPluginRepository.cs @@ -33,6 +33,9 @@ namespace Jellyfin.Server.Migrations.Routines public string Name => "AddDefaultPluginRepository"; /// <inheritdoc/> + public bool PerformOnNewInstall => true; + + /// <inheritdoc/> public void Perform() { _serverConfigurationManager.Configuration.PluginRepositories.Add(_defaultRepositoryInfo); diff --git a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs index b15e09290..6821630db 100644 --- a/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs +++ b/Jellyfin.Server/Migrations/Routines/CreateUserLoggingConfigFile.cs @@ -49,6 +49,9 @@ namespace Jellyfin.Server.Migrations.Routines public string Name => "CreateLoggingConfigHeirarchy"; /// <inheritdoc/> + public bool PerformOnNewInstall => false; + + /// <inheritdoc/> public void Perform() { var logDirectory = _appPaths.ConfigurationDirectoryPath; diff --git a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs index c18aa1629..0925a87b5 100644 --- a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs +++ b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs @@ -26,6 +26,9 @@ namespace Jellyfin.Server.Migrations.Routines public string Name => "DisableTranscodingThrottling"; /// <inheritdoc/> + public bool PerformOnNewInstall => false; + + /// <inheritdoc/> public void Perform() { // Set EnableThrottling to false since it wasn't used before and may introduce issues diff --git a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs index fb3466e13..6048160c6 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateActivityLogDb.cs @@ -42,6 +42,9 @@ namespace Jellyfin.Server.Migrations.Routines public string Name => "MigrateActivityLogDatabase"; /// <inheritdoc/> + public bool PerformOnNewInstall => false; + + /// <inheritdoc/> public void Perform() { var logLevelDictionary = new Dictionary<string, LogLevel>(StringComparer.OrdinalIgnoreCase) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs index 2be10c708..274e6ab73 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs @@ -55,6 +55,9 @@ namespace Jellyfin.Server.Migrations.Routines public string Name => "MigrateUserDatabase"; /// <inheritdoc/> + public bool PerformOnNewInstall => false; + + /// <inheritdoc/> public void Perform() { var dataPath = _paths.DataPath; diff --git a/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs b/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs new file mode 100644 index 000000000..b281b5cc0 --- /dev/null +++ b/Jellyfin.Server/Migrations/Routines/ReaddDefaultPluginRepository.cs @@ -0,0 +1,49 @@ +using System; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.Updates; + +namespace Jellyfin.Server.Migrations.Routines +{ + /// <summary> + /// Migration to initialize system configuration with the default plugin repository. + /// </summary> + public class ReaddDefaultPluginRepository : IMigrationRoutine + { + private readonly IServerConfigurationManager _serverConfigurationManager; + + private readonly RepositoryInfo _defaultRepositoryInfo = new RepositoryInfo + { + Name = "Jellyfin Stable", + Url = "https://repo.jellyfin.org/releases/plugin/manifest-stable.json" + }; + + /// <summary> + /// Initializes a new instance of the <see cref="ReaddDefaultPluginRepository"/> class. + /// </summary> + /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + public ReaddDefaultPluginRepository(IServerConfigurationManager serverConfigurationManager) + { + _serverConfigurationManager = serverConfigurationManager; + } + + /// <inheritdoc/> + public Guid Id => Guid.Parse("5F86E7F6-D966-4C77-849D-7A7B40B68C4E"); + + /// <inheritdoc/> + public string Name => "ReaddDefaultPluginRepository"; + + /// <inheritdoc/> + public bool PerformOnNewInstall => true; + + /// <inheritdoc/> + public void Perform() + { + // Only add if repository list is empty + if (_serverConfigurationManager.Configuration.PluginRepositories.Count == 0) + { + _serverConfigurationManager.Configuration.PluginRepositories.Add(_defaultRepositoryInfo); + _serverConfigurationManager.SaveConfiguration(); + } + } + } +}
\ No newline at end of file diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs index 2ebef0241..6c26e47e1 100644 --- a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs +++ b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs @@ -30,6 +30,9 @@ namespace Jellyfin.Server.Migrations.Routines public string Name => "RemoveDuplicateExtras"; /// <inheritdoc/> + public bool PerformOnNewInstall => false; + + /// <inheritdoc/> public void Perform() { var dataPath = _paths.DataPath; diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 4d898ff5e..ef3ebe90c 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -274,10 +274,10 @@ namespace Jellyfin.Server var addresses = appHost.ServerConfigurationManager .Configuration .LocalNetworkAddresses - .Select(appHost.NormalizeConfiguredLocalAddress) + .Select(x => appHost.NormalizeConfiguredLocalAddress(x)) .Where(i => i != null) .ToHashSet(); - if (addresses.Any() && !addresses.Contains(IPAddress.Any)) + if (addresses.Count > 0 && !addresses.Contains(IPAddress.Any)) { if (!addresses.Contains(IPAddress.Loopback)) { diff --git a/MediaBrowser.Api/FilterService.cs b/MediaBrowser.Api/FilterService.cs index 1b736c77d..dcfdcbfed 100644 --- a/MediaBrowser.Api/FilterService.cs +++ b/MediaBrowser.Api/FilterService.cs @@ -125,7 +125,7 @@ namespace MediaBrowser.Api IncludeItemTypes = request.GetIncludeItemTypes(), DtoOptions = new Controller.Dto.DtoOptions { - Fields = new ItemFields[] { }, + Fields = Array.Empty<ItemFields>(), EnableImages = false, EnableUserData = false }, diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index 8426a9a4f..575b1157a 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -895,6 +895,11 @@ namespace MediaBrowser.Api.Images // Handle image/png; charset=utf-8 mimeType = mimeType.Split(';').FirstOrDefault(); var userDataPath = Path.Combine(ServerConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username); + if (user.ProfileImage != null) + { + _userManager.ClearProfileImage(user); + } + user.ProfileImage = new Jellyfin.Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType))); await _providerManager diff --git a/MediaBrowser.Api/SyncPlay/SyncPlayService.cs b/MediaBrowser.Api/SyncPlay/SyncPlayService.cs index 88cddfff7..daa1b521f 100644 --- a/MediaBrowser.Api/SyncPlay/SyncPlayService.cs +++ b/MediaBrowser.Api/SyncPlay/SyncPlayService.cs @@ -11,15 +11,15 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Api.SyncPlay { - [Route("/SyncPlay/NewGroup", "POST", Summary = "Create a new SyncPlay group")] + [Route("/SyncPlay/New", "POST", Summary = "Create a new SyncPlay group")] [Authenticated] - public class SyncPlayNewGroup : IReturnVoid + public class SyncPlayNew : IReturnVoid { } - [Route("/SyncPlay/JoinGroup", "POST", Summary = "Join an existing SyncPlay group")] + [Route("/SyncPlay/Join", "POST", Summary = "Join an existing SyncPlay group")] [Authenticated] - public class SyncPlayJoinGroup : IReturnVoid + public class SyncPlayJoin : IReturnVoid { /// <summary> /// Gets or sets the Group id. @@ -27,24 +27,17 @@ namespace MediaBrowser.Api.SyncPlay /// <value>The Group id to join.</value> [ApiMember(Name = "GroupId", Description = "Group Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] public string GroupId { get; set; } - - /// <summary> - /// Gets or sets the playing item id. - /// </summary> - /// <value>The client's currently playing item id.</value> - [ApiMember(Name = "PlayingItemId", Description = "Client's playing item id", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string PlayingItemId { get; set; } } - [Route("/SyncPlay/LeaveGroup", "POST", Summary = "Leave joined SyncPlay group")] + [Route("/SyncPlay/Leave", "POST", Summary = "Leave joined SyncPlay group")] [Authenticated] - public class SyncPlayLeaveGroup : IReturnVoid + public class SyncPlayLeave : IReturnVoid { } - [Route("/SyncPlay/ListGroups", "GET", Summary = "List SyncPlay groups")] + [Route("/SyncPlay/List", "GET", Summary = "List SyncPlay groups")] [Authenticated] - public class SyncPlayListGroups : IReturnVoid + public class SyncPlayList : IReturnVoid { /// <summary> /// Gets or sets the filter item id. @@ -54,29 +47,29 @@ namespace MediaBrowser.Api.SyncPlay public string FilterItemId { get; set; } } - [Route("/SyncPlay/PlayRequest", "POST", Summary = "Request play in SyncPlay group")] + [Route("/SyncPlay/Play", "POST", Summary = "Request play in SyncPlay group")] [Authenticated] - public class SyncPlayPlayRequest : IReturnVoid + public class SyncPlayPlay : IReturnVoid { } - [Route("/SyncPlay/PauseRequest", "POST", Summary = "Request pause in SyncPlay group")] + [Route("/SyncPlay/Pause", "POST", Summary = "Request pause in SyncPlay group")] [Authenticated] - public class SyncPlayPauseRequest : IReturnVoid + public class SyncPlayPause : IReturnVoid { } - [Route("/SyncPlay/SeekRequest", "POST", Summary = "Request seek in SyncPlay group")] + [Route("/SyncPlay/Seek", "POST", Summary = "Request seek in SyncPlay group")] [Authenticated] - public class SyncPlaySeekRequest : IReturnVoid + public class SyncPlaySeek : IReturnVoid { [ApiMember(Name = "PositionTicks", IsRequired = true, DataType = "long", ParameterType = "query", Verb = "POST")] public long PositionTicks { get; set; } } - [Route("/SyncPlay/BufferingRequest", "POST", Summary = "Request group wait in SyncPlay group while buffering")] + [Route("/SyncPlay/Buffering", "POST", Summary = "Request group wait in SyncPlay group while buffering")] [Authenticated] - public class SyncPlayBufferingRequest : IReturnVoid + public class SyncPlayBuffering : IReturnVoid { /// <summary> /// Gets or sets the date used to pin PositionTicks in time. @@ -89,16 +82,16 @@ namespace MediaBrowser.Api.SyncPlay public long PositionTicks { get; set; } /// <summary> - /// Gets or sets whether this is a buffering or a buffering-done request. + /// Gets or sets whether this is a buffering or a ready request. /// </summary> /// <value><c>true</c> if buffering is complete; <c>false</c> otherwise.</value> [ApiMember(Name = "BufferingDone", IsRequired = true, DataType = "bool", ParameterType = "query", Verb = "POST")] public bool BufferingDone { get; set; } } - [Route("/SyncPlay/UpdatePing", "POST", Summary = "Update session ping")] + [Route("/SyncPlay/Ping", "POST", Summary = "Update session ping")] [Authenticated] - public class SyncPlayUpdatePing : IReturnVoid + public class SyncPlayPing : IReturnVoid { [ApiMember(Name = "Ping", IsRequired = true, DataType = "double", ParameterType = "query", Verb = "POST")] public double Ping { get; set; } @@ -135,7 +128,7 @@ namespace MediaBrowser.Api.SyncPlay /// Handles the specified request. /// </summary> /// <param name="request">The request.</param> - public void Post(SyncPlayNewGroup request) + public void Post(SyncPlayNew request) { var currentSession = GetSession(_sessionContext); _syncPlayManager.NewGroup(currentSession, CancellationToken.None); @@ -145,30 +138,20 @@ namespace MediaBrowser.Api.SyncPlay /// Handles the specified request. /// </summary> /// <param name="request">The request.</param> - public void Post(SyncPlayJoinGroup request) + public void Post(SyncPlayJoin request) { var currentSession = GetSession(_sessionContext); Guid groupId; - Guid playingItemId = Guid.Empty; - if (!Guid.TryParse(request.GroupId, out groupId)) { Logger.LogError("JoinGroup: {0} is not a valid format for GroupId. Ignoring request.", request.GroupId); return; } - // Both null and empty strings mean that client isn't playing anything - if (!string.IsNullOrEmpty(request.PlayingItemId) && !Guid.TryParse(request.PlayingItemId, out playingItemId)) - { - Logger.LogError("JoinGroup: {0} is not a valid format for PlayingItemId. Ignoring request.", request.PlayingItemId); - return; - } - var joinRequest = new JoinGroupRequest() { - GroupId = groupId, - PlayingItemId = playingItemId + GroupId = groupId }; _syncPlayManager.JoinGroup(currentSession, groupId, joinRequest, CancellationToken.None); @@ -178,7 +161,7 @@ namespace MediaBrowser.Api.SyncPlay /// Handles the specified request. /// </summary> /// <param name="request">The request.</param> - public void Post(SyncPlayLeaveGroup request) + public void Post(SyncPlayLeave request) { var currentSession = GetSession(_sessionContext); _syncPlayManager.LeaveGroup(currentSession, CancellationToken.None); @@ -189,7 +172,7 @@ namespace MediaBrowser.Api.SyncPlay /// </summary> /// <param name="request">The request.</param> /// <value>The requested list of groups.</value> - public List<GroupInfoView> Post(SyncPlayListGroups request) + public List<GroupInfoView> Get(SyncPlayList request) { var currentSession = GetSession(_sessionContext); var filterItemId = Guid.Empty; @@ -206,7 +189,7 @@ namespace MediaBrowser.Api.SyncPlay /// Handles the specified request. /// </summary> /// <param name="request">The request.</param> - public void Post(SyncPlayPlayRequest request) + public void Post(SyncPlayPlay request) { var currentSession = GetSession(_sessionContext); var syncPlayRequest = new PlaybackRequest() @@ -220,7 +203,7 @@ namespace MediaBrowser.Api.SyncPlay /// Handles the specified request. /// </summary> /// <param name="request">The request.</param> - public void Post(SyncPlayPauseRequest request) + public void Post(SyncPlayPause request) { var currentSession = GetSession(_sessionContext); var syncPlayRequest = new PlaybackRequest() @@ -234,7 +217,7 @@ namespace MediaBrowser.Api.SyncPlay /// Handles the specified request. /// </summary> /// <param name="request">The request.</param> - public void Post(SyncPlaySeekRequest request) + public void Post(SyncPlaySeek request) { var currentSession = GetSession(_sessionContext); var syncPlayRequest = new PlaybackRequest() @@ -249,12 +232,12 @@ namespace MediaBrowser.Api.SyncPlay /// Handles the specified request. /// </summary> /// <param name="request">The request.</param> - public void Post(SyncPlayBufferingRequest request) + public void Post(SyncPlayBuffering request) { var currentSession = GetSession(_sessionContext); var syncPlayRequest = new PlaybackRequest() { - Type = request.BufferingDone ? PlaybackRequestType.BufferingDone : PlaybackRequestType.Buffering, + Type = request.BufferingDone ? PlaybackRequestType.Ready : PlaybackRequestType.Buffer, When = DateTime.Parse(request.When), PositionTicks = request.PositionTicks }; @@ -265,12 +248,12 @@ namespace MediaBrowser.Api.SyncPlay /// Handles the specified request. /// </summary> /// <param name="request">The request.</param> - public void Post(SyncPlayUpdatePing request) + public void Post(SyncPlayPing request) { var currentSession = GetSession(_sessionContext); var syncPlayRequest = new PlaybackRequest() { - Type = PlaybackRequestType.UpdatePing, + Type = PlaybackRequestType.Ping, Ping = Convert.ToInt64(request.Ping) }; _syncPlayManager.HandleRequest(currentSession, syncPlayRequest, CancellationToken.None); diff --git a/MediaBrowser.Api/System/ActivityLogService.cs b/MediaBrowser.Api/System/ActivityLogService.cs index 7ca31c21a..4afa6e114 100644 --- a/MediaBrowser.Api/System/ActivityLogService.cs +++ b/MediaBrowser.Api/System/ActivityLogService.cs @@ -56,7 +56,10 @@ namespace MediaBrowser.Api.System DateTime.Parse(request.MinDate, null, DateTimeStyles.RoundtripKind).ToUniversalTime(); var filterFunc = new Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>>( - entries => entries.Where(entry => entry.DateCreated >= minDate)); + entries => entries.Where(entry => entry.DateCreated >= minDate + && (!request.HasUserId.HasValue || (request.HasUserId.Value + ? entry.UserId != Guid.Empty + : entry.UserId == Guid.Empty)))); var result = _activityManager.GetPagedResult(filterFunc, request.StartIndex, request.Limit); diff --git a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs new file mode 100644 index 000000000..d746207c7 --- /dev/null +++ b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs @@ -0,0 +1,33 @@ +using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; + +namespace MediaBrowser.Common.Extensions +{ + /// <summary> + /// Static class containing extension methods for <see cref="HttpContext"/>. + /// </summary> + public static class HttpContextExtensions + { + private const string ServiceStackRequest = "ServiceStackRequest"; + + /// <summary> + /// Set the ServiceStack request. + /// </summary> + /// <param name="httpContext">The HttpContext instance.</param> + /// <param name="request">The service stack request instance.</param> + public static void SetServiceStackRequest(this HttpContext httpContext, IRequest request) + { + httpContext.Items[ServiceStackRequest] = request; + } + + /// <summary> + /// Get the ServiceStack request. + /// </summary> + /// <param name="httpContext">The HttpContext instance.</param> + /// <returns>The service stack request instance.</returns> + public static IRequest GetServiceStackRequest(this HttpContext httpContext) + { + return (IRequest)httpContext.Items[ServiceStackRequest]; + } + } +} diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index c9ca153c7..7380f39fd 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -17,8 +17,8 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.5" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.6" /> <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" /> </ItemGroup> diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 25933bc90..90f62e153 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -613,7 +613,7 @@ namespace MediaBrowser.Controller.Entities { if (!IsFileProtocol) { - return new string[] { }; + return Array.Empty<string>(); } return new[] { Path }; diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index 70c48b6f1..c131c5430 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -198,7 +198,7 @@ namespace MediaBrowser.Controller.Entities.Movies public Guid[] GetLibraryFolderIds() { - var expandedFolders = new List<Guid>() { }; + var expandedFolders = new List<Guid>(); return FlattenItems(this, expandedFolders) .SelectMany(i => LibraryManager.GetCollectionFolders(i)) diff --git a/MediaBrowser.Controller/Library/TVUtils.cs b/MediaBrowser.Controller/Library/TVUtils.cs index fc9b3f1c6..a3aa6019e 100644 --- a/MediaBrowser.Controller/Library/TVUtils.cs +++ b/MediaBrowser.Controller/Library/TVUtils.cs @@ -38,7 +38,7 @@ namespace MediaBrowser.Controller.Library }; } - return new DayOfWeek[] { }; + return Array.Empty<DayOfWeek>(); } return null; diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 73e966344..67f17f7a5 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -13,8 +13,8 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.5" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.6" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index d3fb6a46d..04d03080b 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Text; using System.Threading; using Jellyfin.Data.Enums; @@ -449,41 +450,59 @@ namespace MediaBrowser.Controller.MediaEncoding var arg = new StringBuilder(); var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions) ?? string.Empty; var outputVideoCodec = GetVideoEncoder(state, encodingOptions) ?? string.Empty; - bool isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1; - bool isVaapiEncoder = outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1; - bool isQsvDecoder = videoDecoder.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1; - bool isQsvEncoder = outputVideoCodec.IndexOf("qsv", 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 isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); - if (state.IsVideoRequest - && string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) + if (!IsCopyCodec(outputVideoCodec)) { - if (isVaapiDecoder) + if (state.IsVideoRequest + && _mediaEncoder.SupportsHwaccel("vaapi") + && string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) { - arg.Append("-hwaccel_output_format vaapi ") - .Append("-vaapi_device ") - .Append(encodingOptions.VaapiDevice) - .Append(" "); - } - else if (!isVaapiDecoder && isVaapiEncoder) - { - arg.Append("-vaapi_device ") - .Append(encodingOptions.VaapiDevice) - .Append(" "); + if (isVaapiDecoder) + { + arg.Append("-hwaccel_output_format vaapi ") + .Append("-vaapi_device ") + .Append(encodingOptions.VaapiDevice) + .Append(" "); + } + else if (!isVaapiDecoder && isVaapiEncoder) + { + arg.Append("-vaapi_device ") + .Append(encodingOptions.VaapiDevice) + .Append(" "); + } } - } - - if (state.IsVideoRequest - && string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) - { - var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; - if (!hasTextSubs) + if (state.IsVideoRequest + && string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) { - if (isQsvEncoder) + var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + + if (isQsvEncoder) { if (isQsvDecoder) { - arg.Append("-hwaccel qsv "); + if (isLinux) + { + if (hasGraphicalSubs) + { + arg.Append("-init_hw_device qsv=hw -filter_hw_device hw "); + } + else + { + arg.Append("-hwaccel qsv "); + } + } + + if (isWindows) + { + arg.Append("-hwaccel qsv "); + } } // While using SW decoder else @@ -806,6 +825,34 @@ namespace MediaBrowser.Controller.MediaEncoding break; } } + else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) + { + switch (encodingOptions.EncoderPreset) + { + case "veryslow": + case "slow": + case "slower": + param += "-quality quality"; + break; + + case "medium": + param += "-quality balanced"; + break; + + case "fast": + case "faster": + case "veryfast": + case "superfast": + case "ultrafast": + param += "-quality speed"; + break; + + default: + param += "-quality speed"; + break; + } + } else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // webm { // Values 0-3, 0 being highest quality but slower @@ -1351,7 +1398,7 @@ namespace MediaBrowser.Controller.MediaEncoding transcoderChannelLimit = 6; } - var isTranscodingAudio = !EncodingHelper.IsCopyCodec(codec); + var isTranscodingAudio = !IsCopyCodec(codec); int? resultChannels = state.GetRequestedAudioChannels(codec); if (isTranscodingAudio) @@ -1555,28 +1602,44 @@ namespace MediaBrowser.Controller.MediaEncoding var index = outputSizeParam.IndexOf("hwdownload", StringComparison.OrdinalIgnoreCase); if (index != -1) { - outputSizeParam = "," + outputSizeParam.Substring(index); + outputSizeParam = outputSizeParam.Substring(index); } else { - index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase); + index = outputSizeParam.IndexOf("hwupload=extra_hw_frames", StringComparison.OrdinalIgnoreCase); if (index != -1) { - outputSizeParam = "," + outputSizeParam.Substring(index); + outputSizeParam = outputSizeParam.Substring(index); } else { - index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase); + index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase); if (index != -1) { - outputSizeParam = "," + outputSizeParam.Substring(index); + outputSizeParam = outputSizeParam.Substring(index); } else { - index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase); + index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase); if (index != -1) { - outputSizeParam = "," + outputSizeParam.Substring(index); + outputSizeParam = outputSizeParam.Substring(index); + } + else + { + index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase); + if (index != -1) + { + outputSizeParam = outputSizeParam.Substring(index); + } + else + { + index = outputSizeParam.IndexOf("vpp", StringComparison.OrdinalIgnoreCase); + if (index != -1) + { + outputSizeParam = outputSizeParam.Substring(index); + } + } } } } @@ -1585,43 +1648,30 @@ namespace MediaBrowser.Controller.MediaEncoding var videoSizeParam = string.Empty; var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options) ?? string.Empty; + var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); // Setup subtitle scaling if (state.VideoStream != null && state.VideoStream.Width.HasValue && state.VideoStream.Height.HasValue) { - videoSizeParam = string.Format( - CultureInfo.InvariantCulture, - "scale={0}:{1}", - state.VideoStream.Width.Value, - state.VideoStream.Height.Value); - - // For QSV, feed it into hardware encoder now - if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) - { - videoSizeParam += ",hwupload=extra_hw_frames=64"; - } + // Adjust the size of graphical subtitles to fit the video stream. + var videoStream = state.VideoStream; + var inputWidth = videoStream?.Width; + var inputHeight = videoStream?.Height; + var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight); - // For VAAPI and CUVID decoder - // these encoders cannot automatically adjust the size of graphical subtitles to fit the output video, - // thus needs to be manually adjusted. - if (videoDecoder.IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1 - || (IsVaapiSupported(state) && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) - && (videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1 - || outputVideoCodec.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1))) + if (width.HasValue && height.HasValue) { - var videoStream = state.VideoStream; - var inputWidth = videoStream?.Width; - var inputHeight = videoStream?.Height; - var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight); - - if (width.HasValue && height.HasValue) - { - videoSizeParam = string.Format( + videoSizeParam = string.Format( CultureInfo.InvariantCulture, - "scale={0}:{1}", + "scale={0}x{1}", width.Value, height.Value); - } + } + + // For QSV, feed it into hardware encoder now + if (isLinux && string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + { + videoSizeParam += ",hwupload=extra_hw_frames=64"; } } @@ -1634,7 +1684,10 @@ namespace MediaBrowser.Controller.MediaEncoding : state.SubtitleStream.Index; // Setup default filtergraph utilizing FFMpeg overlay() and FFMpeg scale() (see the return of this function for index reference) - var retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay{3}\""; + // Always put the scaler before the overlay for better performance + var retStr = !string.IsNullOrEmpty(outputSizeParam) ? + " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\"" : + " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\""; // When the input may or may not be hardware VAAPI decodable if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) @@ -1644,12 +1697,11 @@ namespace MediaBrowser.Controller.MediaEncoding [sub]: SW scaling subtitle to FixedOutputSize [base][sub]: SW overlay */ - outputSizeParam = outputSizeParam.TrimStart(','); retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3},hwdownload[base];[base][sub]overlay,format=nv12,hwupload\""; } // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first - else if (IsVaapiSupported(state) && videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1 + else if (_mediaEncoder.SupportsHwaccel("vaapi") && videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1 && string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase)) { /* @@ -1657,7 +1709,6 @@ namespace MediaBrowser.Controller.MediaEncoding [sub]: SW scaling subtitle to FixedOutputSize [base][sub]: SW overlay */ - outputSizeParam = outputSizeParam.TrimStart(','); retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\""; } else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) @@ -1666,14 +1717,13 @@ namespace MediaBrowser.Controller.MediaEncoding QSV in FFMpeg can now setup hardware overlay for transcodes. For software decoding and hardware encoding option, frames must be hwuploaded into hardware with fixed frame size. + Currently only supports linux. */ - if (videoDecoder.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1) + if (isLinux) { - retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv=x=(W-w)/2:y=(H-h)/2{3}\""; - } - else - { - retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]hwupload=extra_hw_frames=64[v];[v][sub]overlay_qsv=x=(W-w)/2:y=(H-h)/2{3}\""; + retStr = !string.IsNullOrEmpty(outputSizeParam) ? + " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay_qsv\"" : + " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv\""; } } @@ -1745,10 +1795,8 @@ namespace MediaBrowser.Controller.MediaEncoding requestedMaxWidth, requestedMaxHeight); - var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; - if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) - || (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) && !hasTextSubs)) + || string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)) && width.HasValue && height.HasValue) { @@ -1757,7 +1805,11 @@ namespace MediaBrowser.Controller.MediaEncoding // output dimensions. Output dimensions are guaranteed to be even. var outputWidth = width.Value; var outputHeight = height.Value; - var vaapi_or_qsv = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) ? "qsv" : "vaapi"; + var qsv_or_vaapi = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase); + var isDeintEnabled = state.DeInterlace("h264", true) + || state.DeInterlace("avc", true) + || state.DeInterlace("h265", true) + || state.DeInterlace("hevc", true); if (!videoWidth.HasValue || outputWidth != videoWidth.Value @@ -1765,17 +1817,24 @@ namespace MediaBrowser.Controller.MediaEncoding || outputHeight != videoHeight.Value) { // Force nv12 pixel format to enable 10-bit to 8-bit colour conversion. + // use vpp_qsv filter to avoid green bar when the fixed output size is requested. filters.Add( string.Format( CultureInfo.InvariantCulture, - "scale_{0}=w={1}:h={2}:format=nv12", - vaapi_or_qsv, + "{0}=w={1}:h={2}:format=nv12{3}", + qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi", outputWidth, - outputHeight)); + outputHeight, + (qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty)); } else { - filters.Add(string.Format(CultureInfo.InvariantCulture, "scale_{0}=format=nv12", vaapi_or_qsv)); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "{0}=format=nv12{1}", + qsv_or_vaapi ? "vpp_qsv" : "scale_vaapi", + (qsv_or_vaapi && isDeintEnabled) ? ":deinterlace=1" : string.Empty)); } } else if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1 @@ -1987,7 +2046,6 @@ namespace MediaBrowser.Controller.MediaEncoding // http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/ var request = state.BaseRequest; - var videoStream = state.VideoStream; var filters = new List<string>(); @@ -1996,32 +2054,34 @@ namespace MediaBrowser.Controller.MediaEncoding var inputHeight = videoStream?.Height; var threeDFormat = state.MediaSource.Video3DFormat; + 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; + var isNvdecH264Decoder = videoDecoder.IndexOf("h264_cuvid", StringComparison.OrdinalIgnoreCase) != -1; + var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", StringComparison.OrdinalIgnoreCase) != -1; + var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; // When the input may or may not be hardware VAAPI decodable - if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + if (isVaapiH264Encoder) { filters.Add("format=nv12|vaapi"); filters.Add("hwupload"); } - // When the input may or may not be hardware QSV decodable - else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + // When burning in graphical subtitles using overlay_qsv, upload videostream to the same qsv context + else if (isLinux && hasGraphicalSubs && isQsvH264Encoder) { - if (!hasTextSubs) - { - filters.Add("format=nv12|qsv"); - filters.Add("hwupload=extra_hw_frames=64"); - } + filters.Add("hwupload=extra_hw_frames=64"); } // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first - else if (videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1 - && string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase)) + else if (IsVaapiSupported(state) && isVaapiDecoder && isLibX264Encoder) { var codec = videoStream.Codec.ToLowerInvariant(); - var isColorDepth10 = !string.IsNullOrEmpty(videoStream.Profile) && (videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase) - || videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase)); + var isColorDepth10 = IsColorDepth10(state); // Assert 10-bit hardware VAAPI decodable if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) @@ -2046,49 +2106,49 @@ namespace MediaBrowser.Controller.MediaEncoding } // Add hardware deinterlace filter before scaling filter - if (state.DeInterlace("h264", true)) + if (state.DeInterlace("h264", true) || state.DeInterlace("avc", true)) { - if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + if (isVaapiH264Encoder) { filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_vaapi")); } - else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) - { - if (!hasTextSubs) - { - filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_qsv")); - } - } } // Add software deinterlace filter before scaling filter - if (((state.DeInterlace("h264", true) || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true)) - && !string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) - && !string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) - || (hasTextSubs && state.DeInterlace("h264", true) && string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))) + if (state.DeInterlace("h264", true) + || state.DeInterlace("avc", true) + || state.DeInterlace("h265", true) + || state.DeInterlace("hevc", true)) { + var deintParam = string.Empty; var inputFramerate = videoStream?.RealFrameRate; // If it is already 60fps then it will create an output framerate that is much too high for roku and others to handle if (string.Equals(options.DeinterlaceMethod, "yadif_bob", StringComparison.OrdinalIgnoreCase) && (inputFramerate ?? 60) <= 30) { - filters.Add("yadif=1:-1:0"); + deintParam = "yadif=1:-1:0"; } else { - filters.Add("yadif=0:-1:0"); + deintParam = "yadif=0:-1:0"; + } + + if (!string.IsNullOrEmpty(deintParam)) + { + if (!isVaapiH264Encoder && !isQsvH264Encoder && !isNvdecH264Decoder) + { + filters.Add(deintParam); + } } } // 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)); - // Add parameters to use VAAPI with burn-in text subttiles (GH issue #642) - if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + // Add parameters to use VAAPI with burn-in text subtitles (GH issue #642) + if (isVaapiH264Encoder) { - if (state.SubtitleStream != null - && state.SubtitleStream.IsTextSubtitleStream - && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) + if (hasTextSubs) { // Test passed on Intel and AMD gfx filters.Add("hwmap=mode=read+write"); @@ -2098,9 +2158,7 @@ namespace MediaBrowser.Controller.MediaEncoding var output = string.Empty; - if (state.SubtitleStream != null - && state.SubtitleStream.IsTextSubtitleStream - && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) + if (hasTextSubs) { var subParam = GetTextSubtitleParam(state); @@ -2108,7 +2166,7 @@ namespace MediaBrowser.Controller.MediaEncoding // Ensure proper filters are passed to ffmpeg in case of hardware acceleration via VA-API // Reference: https://trac.ffmpeg.org/wiki/Hardware/VAAPI - if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + if (isVaapiH264Encoder) { filters.Add("hwmap"); } @@ -2262,7 +2320,7 @@ namespace MediaBrowser.Controller.MediaEncoding flags.Add("+ignidx"); } - if (state.GenPtsInput || EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) + if (state.GenPtsInput || IsCopyCodec(state.OutputVideoCodec)) { flags.Add("+genpts"); } @@ -2288,7 +2346,8 @@ namespace MediaBrowser.Controller.MediaEncoding { inputModifier += " " + videoDecoder; - if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1) + if (!IsCopyCodec(state.OutputVideoCodec) + && (videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1) { var videoStream = state.VideoStream; var inputWidth = videoStream?.Width; @@ -2301,11 +2360,19 @@ namespace MediaBrowser.Controller.MediaEncoding && width.HasValue && height.HasValue) { - inputModifier += string.Format( - CultureInfo.InvariantCulture, - " -resize {0}x{1}", - width.Value, - height.Value); + if (width.HasValue && height.HasValue) + { + inputModifier += string.Format( + CultureInfo.InvariantCulture, + " -resize {0}x{1}", + width.Value, + height.Value); + } + + if (state.DeInterlace("h264", true)) + { + inputModifier += " -deint 1"; + } } } } @@ -2521,21 +2588,21 @@ namespace MediaBrowser.Controller.MediaEncoding } /// <summary> - /// Gets the name of the output video codec. + /// Gets the ffmpeg option string for the hardware accelerated video decoder. /// </summary> + /// <param name="state">The encoding job info.</param> + /// <param name="encodingOptions">The encoding options.</param> + /// <returns>The option string or null if none available.</returns> protected string GetHardwareAcceleratedVideoDecoder(EncodingJobInfo state, EncodingOptions encodingOptions) { - var videoType = state.MediaSource.VideoType ?? VideoType.VideoFile; var videoStream = state.VideoStream; - var isColorDepth10 = !string.IsNullOrEmpty(videoStream.Profile) && (videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase) - || videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase)); - - if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) + if (videoStream == null) { return null; } + var videoType = state.MediaSource.VideoType ?? VideoType.VideoFile; // Only use alternative encoders for video files. // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this. @@ -2544,10 +2611,15 @@ namespace MediaBrowser.Controller.MediaEncoding return null; } - if (videoStream != null - && !string.IsNullOrEmpty(videoStream.Codec) - && !string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType)) + if (IsCopyCodec(state.OutputVideoCodec)) { + return null; + } + + if (!string.IsNullOrEmpty(videoStream.Codec) && !string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType)) + { + var isColorDepth10 = IsColorDepth10(state); + // Only hevc and vp9 formats have 10-bit hardware decoder support now. if (isColorDepth10 && !(string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase) || string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase) @@ -2623,13 +2695,6 @@ namespace MediaBrowser.Controller.MediaEncoding case "h264": if (_mediaEncoder.SupportsDecoder("h264_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) { - // cuvid decoder does not support 10-bit input. - if ((videoStream.BitDepth ?? 8) > 8) - { - encodingOptions.HardwareDecodingCodecs = Array.Empty<string>(); - return null; - } - return "-c:v h264_cuvid"; } @@ -2896,21 +2961,24 @@ namespace MediaBrowser.Controller.MediaEncoding /// </summary> public string GetHwaccelType(EncodingJobInfo state, EncodingOptions options, string videoCodec) { - var isWindows = Environment.OSVersion.Platform == PlatformID.Win32NT; + var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); 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 (!isWindows) + if (isLinux) { return "-hwaccel vaapi"; } - else if (isWindows8orLater) + + if (isWindows && isWindows8orLater) { return "-hwaccel d3d11va"; } - else + + if (isWindows && !isWindows8orLater) { return "-hwaccel dxva2"; } @@ -3000,7 +3068,7 @@ namespace MediaBrowser.Controller.MediaEncoding args += " -mpegts_m2ts_mode 1"; } - if (EncodingHelper.IsCopyCodec(videoCodec)) + if (IsCopyCodec(videoCodec)) { if (state.VideoStream != null && string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) @@ -3102,7 +3170,7 @@ namespace MediaBrowser.Controller.MediaEncoding var args = "-codec:a:0 " + codec; - if (EncodingHelper.IsCopyCodec(codec)) + if (IsCopyCodec(codec)) { return args; } @@ -3179,5 +3247,42 @@ namespace MediaBrowser.Controller.MediaEncoding { return string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase); } + + public static bool IsColorDepth10(EncodingJobInfo state) + { + var result = false; + var videoStream = state.VideoStream; + + if (videoStream != null) + { + if (!string.IsNullOrEmpty(videoStream.PixelFormat)) + { + result = videoStream.PixelFormat.Contains("p10", StringComparison.OrdinalIgnoreCase); + if (result) + { + return true; + } + } + + if (!string.IsNullOrEmpty(videoStream.Profile)) + { + result = videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase) + || videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase) + || videoStream.Profile.Contains("Profile 2", StringComparison.OrdinalIgnoreCase); + if (result) + { + return true; + } + } + + result = (videoStream.BitDepth ?? 8) == 10; + if (result) + { + return true; + } + } + + return result; + } } } diff --git a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs index d0fac1efa..e742df517 100644 --- a/MediaBrowser.Controller/SyncPlay/GroupInfo.cs +++ b/MediaBrowser.Controller/SyncPlay/GroupInfo.cs @@ -16,7 +16,7 @@ namespace MediaBrowser.Controller.SyncPlay /// <summary> /// Gets the default ping value used for sessions. /// </summary> - public long DefaulPing { get; } = 500; + public long DefaultPing { get; } = 500; /// <summary> /// Gets or sets the group identifier. @@ -70,16 +70,16 @@ namespace MediaBrowser.Controller.SyncPlay /// <param name="session">The session.</param> public void AddSession(SessionInfo session) { - if (ContainsSession(session.Id.ToString())) + if (ContainsSession(session.Id)) { return; } var member = new GroupMember(); member.Session = session; - member.Ping = DefaulPing; + member.Ping = DefaultPing; member.IsBuffering = false; - Participants[session.Id.ToString()] = member; + Participants[session.Id] = member; } /// <summary> @@ -88,12 +88,12 @@ namespace MediaBrowser.Controller.SyncPlay /// <param name="session">The session.</param> public void RemoveSession(SessionInfo session) { - if (!ContainsSession(session.Id.ToString())) + if (!ContainsSession(session.Id)) { return; } - Participants.Remove(session.Id.ToString(), out _); + Participants.Remove(session.Id, out _); } /// <summary> @@ -103,12 +103,12 @@ namespace MediaBrowser.Controller.SyncPlay /// <param name="ping">The ping.</param> public void UpdatePing(SessionInfo session, long ping) { - if (!ContainsSession(session.Id.ToString())) + if (!ContainsSession(session.Id)) { return; } - Participants[session.Id.ToString()].Ping = ping; + Participants[session.Id].Ping = ping; } /// <summary> @@ -117,7 +117,7 @@ namespace MediaBrowser.Controller.SyncPlay /// <value name="session">The highest ping in the group.</value> public long GetHighestPing() { - long max = Int64.MinValue; + long max = long.MinValue; foreach (var session in Participants.Values) { max = Math.Max(max, session.Ping); @@ -133,12 +133,12 @@ namespace MediaBrowser.Controller.SyncPlay /// <param name="isBuffering">The state.</param> public void SetBuffering(SessionInfo session, bool isBuffering) { - if (!ContainsSession(session.Id.ToString())) + if (!ContainsSession(session.Id)) { return; } - Participants[session.Id.ToString()].IsBuffering = isBuffering; + Participants[session.Id].IsBuffering = isBuffering; } /// <summary> diff --git a/MediaBrowser.Controller/SyncPlay/GroupMember.cs b/MediaBrowser.Controller/SyncPlay/GroupMember.cs index a3975c334..cde6f8e8c 100644 --- a/MediaBrowser.Controller/SyncPlay/GroupMember.cs +++ b/MediaBrowser.Controller/SyncPlay/GroupMember.cs @@ -8,7 +8,7 @@ namespace MediaBrowser.Controller.SyncPlay public class GroupMember { /// <summary> - /// Gets or sets whether this member is buffering. + /// Gets or sets a value indicating whether this member is buffering. /// </summary> /// <value><c>true</c> if member is buffering; <c>false</c> otherwise.</value> public bool IsBuffering { get; set; } diff --git a/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs b/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs index de1fcd259..45c543806 100644 --- a/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs +++ b/MediaBrowser.Controller/SyncPlay/ISyncPlayController.cs @@ -33,7 +33,7 @@ namespace MediaBrowser.Controller.SyncPlay /// </summary> /// <param name="session">The session.</param> /// <param name="cancellationToken">The cancellation token.</param> - void InitGroup(SessionInfo session, CancellationToken cancellationToken); + void CreateGroup(SessionInfo session, CancellationToken cancellationToken); /// <summary> /// Adds the session to the group. diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs index e6359f4fb..ccfae2fa5 100644 --- a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs +++ b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs @@ -41,7 +41,7 @@ namespace MediaBrowser.MediaEncoding.BdInfo var outputStream = new BlurayDiscInfo { - MediaStreams = new MediaStream[] { } + MediaStreams = Array.Empty<MediaStream>() }; if (playlist == null) diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 4250edfb7..0fd0239b4 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -90,6 +90,7 @@ namespace MediaBrowser.MediaEncoding.Encoder // $ ffmpeg -version | perl -ne ' print "$1=$2.$3," if /^(lib\w+)\s+(\d+)\.\s*(\d+)/' private static readonly IReadOnlyDictionary<string, Version> _ffmpegVersionMap = new Dictionary<string, Version> { + { "libavutil=56.51,libavcodec=58.91,libavformat=58.45,libavdevice=58.10,libavfilter=7.85,libswscale=5.7,libswresample=3.7,libpostproc=55.7,", new Version(4, 3) }, { "libavutil=56.31,libavcodec=58.54,libavformat=58.29,libavdevice=58.8,libavfilter=7.57,libswscale=5.5,libswresample=3.5,libpostproc=55.5,", new Version(4, 2) }, { "libavutil=56.22,libavcodec=58.35,libavformat=58.20,libavdevice=58.5,libavfilter=7.40,libswscale=5.3,libswresample=3.3,libpostproc=55.3,", new Version(4, 1) }, { "libavutil=56.14,libavcodec=58.18,libavformat=58.12,libavdevice=58.3,libavfilter=7.16,libswscale=5.1,libswresample=3.1,libpostproc=55.1,", new Version(4, 0) }, diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 3f21c2bd4..c85d963ed 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -1008,7 +1008,7 @@ namespace MediaBrowser.MediaEncoding.Probing var artist = FFProbeHelpers.GetDictionaryValue(tags, "artist"); if (string.IsNullOrWhiteSpace(artist)) { - audio.Artists = new string[] { }; + audio.Artists = Array.Empty<string>(); } else { @@ -1031,7 +1031,7 @@ namespace MediaBrowser.MediaEncoding.Probing if (string.IsNullOrWhiteSpace(albumArtist)) { - audio.AlbumArtists = new string[] { }; + audio.AlbumArtists = Array.Empty<string>(); } else { diff --git a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs index 398c5db8c..bdc5f8bb7 100644 --- a/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs +++ b/MediaBrowser.Model/Dlna/MediaFormatProfileResolver.cs @@ -206,7 +206,7 @@ namespace MediaBrowser.Model.Dlna } } - return new MediaFormatProfile[] { }; + return Array.Empty<MediaFormatProfile>(); } private MediaFormatProfile ValueOf(string value) diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index b89e9ce90..204340c46 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -144,7 +144,7 @@ namespace MediaBrowser.Model.Dlna public Dictionary<string, string> StreamOptions { get; private set; } - public string MediaSourceId => MediaSource == null ? null : MediaSource.Id; + public string MediaSourceId => MediaSource?.Id; public bool IsDirectStream => PlayMethod == PlayMethod.DirectStream || @@ -813,18 +813,18 @@ namespace MediaBrowser.Model.Dlna { var stream = TargetAudioStream; - string inputCodec = stream == null ? null : stream.Codec; + string inputCodec = stream?.Codec; if (IsDirectStream) { - return string.IsNullOrEmpty(inputCodec) ? new string[] { } : new[] { inputCodec }; + return string.IsNullOrEmpty(inputCodec) ? Array.Empty<string>() : new[] { inputCodec }; } foreach (string codec in AudioCodecs) { if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase)) { - return string.IsNullOrEmpty(codec) ? new string[] { } : new[] { codec }; + return string.IsNullOrEmpty(codec) ? Array.Empty<string>() : new[] { codec }; } } @@ -838,18 +838,18 @@ namespace MediaBrowser.Model.Dlna { var stream = TargetVideoStream; - string inputCodec = stream == null ? null : stream.Codec; + string inputCodec = stream?.Codec; if (IsDirectStream) { - return string.IsNullOrEmpty(inputCodec) ? new string[] { } : new[] { inputCodec }; + return string.IsNullOrEmpty(inputCodec) ? Array.Empty<string>() : new[] { inputCodec }; } foreach (string codec in VideoCodecs) { if (string.Equals(codec, inputCodec, StringComparison.OrdinalIgnoreCase)) { - return string.IsNullOrEmpty(codec) ? new string[] { } : new[] { codec }; + return string.IsNullOrEmpty(codec) ? Array.Empty<string>() : new[] { codec }; } } diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 83bd0c07e..902e29b20 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -23,7 +23,7 @@ <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.5" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.6" /> <PackageReference Include="System.Globalization" Version="4.3.0" /> <PackageReference Include="System.Text.Json" Version="4.7.2" /> </ItemGroup> diff --git a/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs b/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs index 12d537492..2ef6f7c60 100644 --- a/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs +++ b/MediaBrowser.Model/Querying/UpcomingEpisodesQuery.cs @@ -1,6 +1,7 @@ #nullable disable #pragma warning disable CS1591 +using System; using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.Querying @@ -54,7 +55,7 @@ namespace MediaBrowser.Model.Querying public UpcomingEpisodesQuery() { - EnableImageTypes = new ImageType[] { }; + EnableImageTypes = Array.Empty<ImageType>(); } } } diff --git a/MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs b/MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs index d67b6bd55..0c77a6132 100644 --- a/MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs +++ b/MediaBrowser.Model/SyncPlay/JoinGroupRequest.cs @@ -12,11 +12,5 @@ namespace MediaBrowser.Model.SyncPlay /// </summary> /// <value>The Group id to join.</value> public Guid GroupId { get; set; } - - /// <summary> - /// Gets or sets the playing item id. - /// </summary> - /// <value>The client's currently playing item id.</value> - public Guid PlayingItemId { get; set; } } } diff --git a/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs b/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs index 671f4e01f..e89efeed8 100644 --- a/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs +++ b/MediaBrowser.Model/SyncPlay/PlaybackRequestType.cs @@ -23,16 +23,16 @@ namespace MediaBrowser.Model.SyncPlay /// <summary> /// A user is signaling that playback is buffering. /// </summary> - Buffering = 3, + Buffer = 3, /// <summary> /// A user is signaling that playback resumed. /// </summary> - BufferingDone = 4, + Ready = 4, /// <summary> /// A user is reporting its ping. /// </summary> - UpdatePing = 5 + Ping = 5 } } diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index ecfa3d46d..3b0c7b56c 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -723,7 +723,7 @@ namespace MediaBrowser.Providers.Manager userDataList.AddRange(localItem.UserDataList); } - MergeData(localItem, temp, new MetadataField[] { }, !options.ReplaceAllMetadata, true); + MergeData(localItem, temp, Array.Empty<MetadataField>(), !options.ReplaceAllMetadata, true); refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.MetadataImport; // Only one local provider allowed per item @@ -849,7 +849,7 @@ namespace MediaBrowser.Providers.Manager { result.Provider = provider.Name; - MergeData(result, temp, new MetadataField[] { }, false, false); + MergeData(result, temp, Array.Empty<MetadataField>(), false, false); MergeNewData(temp.Item, id); refreshResult.UpdateType = refreshResult.UpdateType | ItemUpdateType.MetadataDownload; diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 86a182fe5..4ee065cd1 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -25,7 +23,6 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Events; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; using Priority_Queue; using Book = MediaBrowser.Controller.Entities.Book; @@ -42,33 +39,38 @@ namespace MediaBrowser.Providers.Manager /// </summary> public class ProviderManager : IProviderManager, IDisposable { + private readonly object _refreshQueueLock = new object(); private readonly ILogger<ProviderManager> _logger; private readonly IHttpClient _httpClient; private readonly ILibraryMonitor _libraryMonitor; private readonly IFileSystem _fileSystem; private readonly IServerApplicationPaths _appPaths; - private readonly IJsonSerializer _json; private readonly ILibraryManager _libraryManager; private readonly ISubtitleManager _subtitleManager; private readonly IServerConfigurationManager _configurationManager; + private readonly ConcurrentDictionary<Guid, double> _activeRefreshes = new ConcurrentDictionary<Guid, double>(); + private readonly CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource(); + private readonly SimplePriorityQueue<Tuple<Guid, MetadataRefreshOptions>> _refreshQueue = + new SimplePriorityQueue<Tuple<Guid, MetadataRefreshOptions>>(); - private IImageProvider[] ImageProviders { get; set; } - - private IMetadataService[] _metadataServices = { }; - private IMetadataProvider[] _metadataProviders = { }; + private IMetadataService[] _metadataServices = Array.Empty<IMetadataService>(); + private IMetadataProvider[] _metadataProviders = Array.Empty<IMetadataProvider>(); private IEnumerable<IMetadataSaver> _savers; - private IExternalId[] _externalIds; - - private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource(); - - public event EventHandler<GenericEventArgs<BaseItem>> RefreshStarted; - public event EventHandler<GenericEventArgs<BaseItem>> RefreshCompleted; - public event EventHandler<GenericEventArgs<Tuple<BaseItem, double>>> RefreshProgress; + private bool _isProcessingRefreshQueue; + private bool _disposed; /// <summary> - /// Initializes a new instance of the <see cref="ProviderManager" /> class. + /// Initializes a new instance of the <see cref="ProviderManager"/> class. /// </summary> + /// <param name="httpClient">The Http client.</param> + /// <param name="subtitleManager">The subtitle manager.</param> + /// <param name="configurationManager">The configuration manager.</param> + /// <param name="libraryMonitor">The library monitor.</param> + /// <param name="logger">The logger.</param> + /// <param name="fileSystem">The filesystem.</param> + /// <param name="appPaths">The server application paths.</param> + /// <param name="libraryManager">The library manager.</param> public ProviderManager( IHttpClient httpClient, ISubtitleManager subtitleManager, @@ -77,8 +79,7 @@ namespace MediaBrowser.Providers.Manager ILogger<ProviderManager> logger, IFileSystem fileSystem, IServerApplicationPaths appPaths, - ILibraryManager libraryManager, - IJsonSerializer json) + ILibraryManager libraryManager) { _logger = logger; _httpClient = httpClient; @@ -87,16 +88,27 @@ namespace MediaBrowser.Providers.Manager _fileSystem = fileSystem; _appPaths = appPaths; _libraryManager = libraryManager; - _json = json; _subtitleManager = subtitleManager; } - /// <summary> - /// Adds the metadata providers. - /// </summary> - public void AddParts(IEnumerable<IImageProvider> imageProviders, IEnumerable<IMetadataService> metadataServices, - IEnumerable<IMetadataProvider> metadataProviders, IEnumerable<IMetadataSaver> metadataSavers, - IEnumerable<IExternalId> externalIds) + /// <inheritdoc/> + public event EventHandler<GenericEventArgs<BaseItem>> RefreshStarted; + + /// <inheritdoc/> + public event EventHandler<GenericEventArgs<BaseItem>> RefreshCompleted; + + /// <inheritdoc/> + public event EventHandler<GenericEventArgs<Tuple<BaseItem, double>>> RefreshProgress; + + private IImageProvider[] ImageProviders { get; set; } + + /// <inheritdoc/> + public void AddParts( + IEnumerable<IImageProvider> imageProviders, + IEnumerable<IMetadataService> metadataServices, + IEnumerable<IMetadataProvider> metadataProviders, + IEnumerable<IMetadataSaver> metadataSavers, + IEnumerable<IExternalId> externalIds) { ImageProviders = imageProviders.ToArray(); @@ -104,27 +116,17 @@ namespace MediaBrowser.Providers.Manager _metadataProviders = metadataProviders.ToArray(); _externalIds = externalIds.OrderBy(i => i.ProviderName).ToArray(); - _savers = metadataSavers.Where(i => - { - var configurable = i as IConfigurableProvider; - - return configurable == null || configurable.IsEnabled; - }).ToArray(); + _savers = metadataSavers + .Where(i => !(i is IConfigurableProvider configurable) || configurable.IsEnabled) + .ToArray(); } + /// <inheritdoc/> public Task<ItemUpdateType> RefreshSingleItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken) { - IMetadataService service = null; var type = item.GetType(); - foreach (var current in _metadataServices) - { - if (current.CanRefreshPrimary(type)) - { - service = current; - break; - } - } + var service = _metadataServices.FirstOrDefault(current => current.CanRefreshPrimary(type)); if (service == null) { @@ -147,35 +149,36 @@ namespace MediaBrowser.Providers.Manager return Task.FromResult(ItemUpdateType.None); } + /// <inheritdoc/> public async Task SaveImage(BaseItem item, string url, ImageType type, int? imageIndex, CancellationToken cancellationToken) { - using (var response = await _httpClient.GetResponse(new HttpRequestOptions + using var response = await _httpClient.GetResponse(new HttpRequestOptions { CancellationToken = cancellationToken, Url = url, BufferContent = false + }).ConfigureAwait(false); - }).ConfigureAwait(false)) + // Workaround for tvheadend channel icons + // TODO: Isolate this hack into the tvh plugin + if (string.IsNullOrEmpty(response.ContentType)) { - // Workaround for tvheadend channel icons - // TODO: Isolate this hack into the tvh plugin - if (string.IsNullOrEmpty(response.ContentType)) + if (url.IndexOf("/imagecache/", StringComparison.OrdinalIgnoreCase) != -1) { - if (url.IndexOf("/imagecache/", StringComparison.OrdinalIgnoreCase) != -1) - { - response.ContentType = "image/png"; - } + response.ContentType = "image/png"; } - - await SaveImage(item, response.Content, response.ContentType, type, imageIndex, cancellationToken).ConfigureAwait(false); } + + await SaveImage(item, response.Content, response.ContentType, type, imageIndex, cancellationToken).ConfigureAwait(false); } + /// <inheritdoc/> public Task SaveImage(BaseItem item, Stream source, string mimeType, ImageType type, int? imageIndex, CancellationToken cancellationToken) { return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, source, mimeType, type, imageIndex, cancellationToken); } + /// <inheritdoc/> public Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(source)) @@ -188,12 +191,14 @@ namespace MediaBrowser.Providers.Manager return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken); } + /// <inheritdoc/> public Task SaveImage(User user, Stream source, string mimeType, string path) { return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger) .SaveImage(user, source, path); } + /// <inheritdoc/> public async Task<IEnumerable<RemoteImageInfo>> GetAvailableRemoteImages(BaseItem item, RemoteImageQuery query, CancellationToken cancellationToken) { var providers = GetRemoteImageProviders(item, query.IncludeDisabledProviders); @@ -213,7 +218,7 @@ namespace MediaBrowser.Providers.Manager languages.Add(preferredLanguage); } - var tasks = providers.Select(i => GetImages(item, cancellationToken, i, languages, query.ImageType)); + var tasks = providers.Select(i => GetImages(item, i, languages, cancellationToken, query.ImageType)); var results = await Task.WhenAll(tasks).ConfigureAwait(false); @@ -224,12 +229,17 @@ namespace MediaBrowser.Providers.Manager /// Gets the images. /// </summary> /// <param name="item">The item.</param> - /// <param name="cancellationToken">The cancellation token.</param> /// <param name="provider">The provider.</param> /// <param name="preferredLanguages">The preferred languages.</param> + /// <param name="cancellationToken">The cancellation token.</param> /// <param name="type">The type.</param> /// <returns>Task{IEnumerable{RemoteImageInfo}}.</returns> - private async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken, IRemoteImageProvider provider, List<string> preferredLanguages, ImageType? type = null) + private async Task<IEnumerable<RemoteImageInfo>> GetImages( + BaseItem item, + IRemoteImageProvider provider, + IReadOnlyCollection<string> preferredLanguages, + CancellationToken cancellationToken, + ImageType? type = null) { try { @@ -260,16 +270,18 @@ namespace MediaBrowser.Providers.Manager } } - /// <summary> - /// Gets the supported image providers. - /// </summary> - /// <param name="item">The item.</param> - /// <returns>IEnumerable{IImageProvider}.</returns> + /// <inheritdoc/> public IEnumerable<ImageProviderInfo> GetRemoteImageProviderInfo(BaseItem item) { return GetRemoteImageProviders(item, true).Select(i => new ImageProviderInfo(i.Name, i.GetSupportedImages(item).ToArray())); } + /// <summary> + /// Gets the image providers for the provided item. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="refreshOptions">The image refresh options.</param> + /// <returns>The image providers for the item.</returns> public IEnumerable<IImageProvider> GetImageProviders(BaseItem item, ImageRefreshOptions refreshOptions) { return GetImageProviders(item, _libraryManager.GetLibraryOptions(item), GetMetadataOptions(item), refreshOptions, false); @@ -283,7 +295,7 @@ namespace MediaBrowser.Providers.Manager var typeOptions = libraryOptions.GetTypeOptions(item.GetType().Name); var typeFetcherOrder = typeOptions?.ImageFetcherOrder; - return ImageProviders.Where(i => CanRefresh(i, item, libraryOptions, options, refreshOptions, includeDisabled)) + return ImageProviders.Where(i => CanRefresh(i, item, libraryOptions, refreshOptions, includeDisabled)) .OrderBy(i => { // See if there's a user-defined order @@ -304,6 +316,13 @@ namespace MediaBrowser.Providers.Manager .ThenBy(GetOrder); } + /// <summary> + /// Gets the metadata providers for the provided item. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="libraryOptions">The library options.</param> + /// <typeparam name="T">The type of metadata provider.</typeparam> + /// <returns>The metadata providers.</returns> public IEnumerable<IMetadataProvider<T>> GetMetadataProviders<T>(BaseItem item, LibraryOptions libraryOptions) where T : BaseItem { @@ -319,7 +338,7 @@ namespace MediaBrowser.Providers.Manager var currentOptions = globalMetadataOptions; return _metadataProviders.OfType<IMetadataProvider<T>>() - .Where(i => CanRefresh(i, item, libraryOptions, currentOptions, includeDisabled, forceEnableInternetMetadata)) + .Where(i => CanRefresh(i, item, libraryOptions, includeDisabled, forceEnableInternetMetadata)) .OrderBy(i => GetConfiguredOrder(item, i, libraryOptions, globalMetadataOptions)) .ThenBy(GetDefaultOrder); } @@ -329,14 +348,20 @@ namespace MediaBrowser.Providers.Manager var options = GetMetadataOptions(item); var libraryOptions = _libraryManager.GetLibraryOptions(item); - return GetImageProviders(item, libraryOptions, options, - new ImageRefreshOptions( - new DirectoryService(_fileSystem)), - includeDisabled) - .OfType<IRemoteImageProvider>(); + return GetImageProviders( + item, + libraryOptions, + options, + new ImageRefreshOptions(new DirectoryService(_fileSystem)), + includeDisabled).OfType<IRemoteImageProvider>(); } - private bool CanRefresh(IMetadataProvider provider, BaseItem item, LibraryOptions libraryOptions, MetadataOptions options, bool includeDisabled, bool forceEnableInternetMetadata) + private bool CanRefresh( + IMetadataProvider provider, + BaseItem item, + LibraryOptions libraryOptions, + bool includeDisabled, + bool forceEnableInternetMetadata) { if (!includeDisabled) { @@ -372,7 +397,12 @@ namespace MediaBrowser.Providers.Manager return true; } - private bool CanRefresh(IImageProvider provider, BaseItem item, LibraryOptions libraryOptions, MetadataOptions options, ImageRefreshOptions refreshOptions, bool includeDisabled) + private bool CanRefresh( + IImageProvider provider, + BaseItem item, + LibraryOptions libraryOptions, + ImageRefreshOptions refreshOptions, + bool includeDisabled) { if (!includeDisabled) { @@ -412,9 +442,7 @@ namespace MediaBrowser.Providers.Manager /// <returns>System.Int32.</returns> private int GetOrder(IImageProvider provider) { - var hasOrder = provider as IHasOrder; - - if (hasOrder == null) + if (!(provider is IHasOrder hasOrder)) { return 0; } @@ -441,7 +469,7 @@ namespace MediaBrowser.Providers.Manager if (provider is IRemoteMetadataProvider) { var typeOptions = libraryOptions.GetTypeOptions(item.GetType().Name); - var typeFetcherOrder = typeOptions == null ? null : typeOptions.MetadataFetcherOrder; + var typeFetcherOrder = typeOptions?.MetadataFetcherOrder; var fetcherOrder = typeFetcherOrder ?? globalMetadataOptions.MetadataFetcherOrder; @@ -459,9 +487,7 @@ namespace MediaBrowser.Providers.Manager private int GetDefaultOrder(IMetadataProvider provider) { - var hasOrder = provider as IHasOrder; - - if (hasOrder != null) + if (provider is IHasOrder hasOrder) { return hasOrder.Order; } @@ -469,9 +495,10 @@ namespace MediaBrowser.Providers.Manager return 0; } + /// <inheritdoc/> public MetadataPluginSummary[] GetAllMetadataPlugins() { - return new MetadataPluginSummary[] + return new[] { GetPluginSummary<Movie>(), GetPluginSummary<BoxSet>(), @@ -493,7 +520,7 @@ namespace MediaBrowser.Providers.Manager where T : BaseItem, new() { // Give it a dummy path just so that it looks like a file system item - var dummy = new T() + var dummy = new T { Path = Path.Combine(_appPaths.InternalMetadataPath, "dummy"), ParentId = Guid.NewGuid() @@ -508,11 +535,12 @@ namespace MediaBrowser.Providers.Manager var libraryOptions = new LibraryOptions(); - var imageProviders = GetImageProviders(dummy, libraryOptions, options, - new ImageRefreshOptions( - new DirectoryService(_fileSystem)), - true) - .ToList(); + var imageProviders = GetImageProviders( + dummy, + libraryOptions, + options, + new ImageRefreshOptions(new DirectoryService(_fileSystem)), + true).ToList(); var pluginList = summary.Plugins.ToList(); @@ -572,7 +600,6 @@ namespace MediaBrowser.Providers.Manager private void AddImagePlugins<T>(List<MetadataPlugin> list, T item, List<IImageProvider> imageProviders) where T : BaseItem { - // Locals list.AddRange(imageProviders.Where(i => (i is ILocalImageProvider)).Select(i => new MetadataPlugin { @@ -588,6 +615,7 @@ namespace MediaBrowser.Providers.Manager })); } + /// <inheritdoc/> public MetadataOptions GetMetadataOptions(BaseItem item) { var type = item.GetType().Name; @@ -597,17 +625,13 @@ namespace MediaBrowser.Providers.Manager new MetadataOptions(); } - /// <summary> - /// Saves the metadata. - /// </summary> + /// <inheritdoc/> public void SaveMetadata(BaseItem item, ItemUpdateType updateType) { SaveMetadata(item, updateType, _savers); } - /// <summary> - /// Saves the metadata. - /// </summary> + /// <inheritdoc/> public void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<string> savers) { SaveMetadata(item, updateType, _savers.Where(i => savers.Contains(i.Name, StringComparer.OrdinalIgnoreCase))); @@ -619,7 +643,6 @@ namespace MediaBrowser.Providers.Manager /// <param name="item">The item.</param> /// <param name="updateType">Type of the update.</param> /// <param name="savers">The savers.</param> - /// <returns>Task.</returns> private void SaveMetadata(BaseItem item, ItemUpdateType updateType, IEnumerable<IMetadataSaver> savers) { var libraryOptions = _libraryManager.GetLibraryOptions(item); @@ -628,11 +651,9 @@ namespace MediaBrowser.Providers.Manager { _logger.LogDebug("Saving {0} to {1}.", item.Path ?? item.Name, saver.Name); - var fileSaver = saver as IMetadataFileSaver; - - if (fileSaver != null) + if (saver is IMetadataFileSaver fileSaver) { - string path = null; + string path; try { @@ -699,11 +720,9 @@ namespace MediaBrowser.Providers.Manager { if (updateType >= ItemUpdateType.MetadataEdit) { - var fileSaver = saver as IMetadataFileSaver; - // Manual edit occurred // Even if save local is off, save locally anyway if the metadata file already exists - if (fileSaver == null || !File.Exists(fileSaver.GetSavePath(item))) + if (!(saver is IMetadataFileSaver fileSaver) || !File.Exists(fileSaver.GetSavePath(item))) { return false; } @@ -734,6 +753,7 @@ namespace MediaBrowser.Providers.Manager } } + /// <inheritdoc/> public Task<IEnumerable<RemoteSearchResult>> GetRemoteSearchResults<TItemType, TLookupType>(RemoteSearchQuery<TLookupType> searchInfo, CancellationToken cancellationToken) where TItemType : BaseItem, new() where TLookupType : ItemLookupInfo @@ -748,7 +768,7 @@ namespace MediaBrowser.Providers.Manager return GetRemoteSearchResults<TItemType, TLookupType>(searchInfo, referenceItem, cancellationToken); } - public async Task<IEnumerable<RemoteSearchResult>> GetRemoteSearchResults<TItemType, TLookupType>(RemoteSearchQuery<TLookupType> searchInfo, BaseItem referenceItem, CancellationToken cancellationToken) + private async Task<IEnumerable<RemoteSearchResult>> GetRemoteSearchResults<TItemType, TLookupType>(RemoteSearchQuery<TLookupType> searchInfo, BaseItem referenceItem, CancellationToken cancellationToken) where TItemType : BaseItem, new() where TLookupType : ItemLookupInfo { @@ -837,7 +857,9 @@ namespace MediaBrowser.Providers.Manager return resultList; } - private async Task<IEnumerable<RemoteSearchResult>> GetSearchResults<TLookupType>(IRemoteSearchProvider<TLookupType> provider, TLookupType searchInfo, + private async Task<IEnumerable<RemoteSearchResult>> GetSearchResults<TLookupType>( + IRemoteSearchProvider<TLookupType> provider, + TLookupType searchInfo, CancellationToken cancellationToken) where TLookupType : ItemLookupInfo { @@ -853,6 +875,7 @@ namespace MediaBrowser.Providers.Manager return list; } + /// <inheritdoc/> public Task<HttpResponseInfo> GetSearchImage(string providerName, string url, CancellationToken cancellationToken) { var provider = _metadataProviders.OfType<IRemoteSearchProvider>().FirstOrDefault(i => string.Equals(i.Name, providerName, StringComparison.OrdinalIgnoreCase)); @@ -865,6 +888,7 @@ namespace MediaBrowser.Providers.Manager return provider.GetImageResponse(url, cancellationToken); } + /// <inheritdoc/> public IEnumerable<IExternalId> GetExternalIds(IHasProviderIds item) { return _externalIds.Where(i => @@ -881,6 +905,7 @@ namespace MediaBrowser.Providers.Manager }); } + /// <inheritdoc/> public IEnumerable<ExternalUrl> GetExternalUrls(BaseItem item) { return GetExternalIds(item) @@ -909,6 +934,7 @@ namespace MediaBrowser.Providers.Manager }).Where(i => i != null).Concat(item.GetRelatedUrls()); } + /// <inheritdoc/> public IEnumerable<ExternalIdInfo> GetExternalIdInfos(IHasProviderIds item) { return GetExternalIds(item) @@ -921,8 +947,7 @@ namespace MediaBrowser.Providers.Manager }); } - private ConcurrentDictionary<Guid, double> _activeRefreshes = new ConcurrentDictionary<Guid, double>(); - + /// <inheritdoc/> public Dictionary<Guid, Guid> GetRefreshQueue() { lock (_refreshQueueLock) @@ -938,6 +963,7 @@ namespace MediaBrowser.Providers.Manager } } + /// <inheritdoc/> public void OnRefreshStart(BaseItem item) { _logger.LogInformation("OnRefreshStart {0}", item.Id.ToString("N", CultureInfo.InvariantCulture)); @@ -945,6 +971,7 @@ namespace MediaBrowser.Providers.Manager RefreshStarted?.Invoke(this, new GenericEventArgs<BaseItem>(item)); } + /// <inheritdoc/> public void OnRefreshComplete(BaseItem item) { _logger.LogInformation("OnRefreshComplete {0}", item.Id.ToString("N", CultureInfo.InvariantCulture)); @@ -954,6 +981,7 @@ namespace MediaBrowser.Providers.Manager RefreshCompleted?.Invoke(this, new GenericEventArgs<BaseItem>(item)); } + /// <inheritdoc/> public double? GetRefreshProgress(Guid id) { if (_activeRefreshes.TryGetValue(id, out double value)) @@ -964,6 +992,7 @@ namespace MediaBrowser.Providers.Manager return null; } + /// <inheritdoc/> public void OnRefreshProgress(BaseItem item, double progress) { var id = item.Id; @@ -983,12 +1012,7 @@ namespace MediaBrowser.Providers.Manager RefreshProgress?.Invoke(this, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(item, progress))); } - private readonly SimplePriorityQueue<Tuple<Guid, MetadataRefreshOptions>> _refreshQueue = - new SimplePriorityQueue<Tuple<Guid, MetadataRefreshOptions>>(); - - private readonly object _refreshQueueLock = new object(); - private bool _isProcessingRefreshQueue; - + /// <inheritdoc/> public void QueueRefresh(Guid id, MetadataRefreshOptions options, RefreshPriority priority) { if (_disposed) @@ -1032,7 +1056,7 @@ namespace MediaBrowser.Providers.Manager if (item != null) { // Try to throttle this a little bit. - await Task.Delay(100).ConfigureAwait(false); + await Task.Delay(100, cancellationToken).ConfigureAwait(false); var task = item is MusicArtist artist ? RefreshArtist(artist, refreshItem.Item2, cancellationToken) @@ -1062,17 +1086,14 @@ namespace MediaBrowser.Providers.Manager await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false); // Collection folders don't validate their children so we'll have to simulate that here - - if (item is CollectionFolder collectionFolder) + switch (item) { - await RefreshCollectionFolderChildren(options, collectionFolder, cancellationToken).ConfigureAwait(false); - } - else - { - if (item is Folder folder) - { + case CollectionFolder collectionFolder: + await RefreshCollectionFolderChildren(options, collectionFolder, cancellationToken).ConfigureAwait(false); + break; + case Folder folder: await folder.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options).ConfigureAwait(false); - } + break; } } @@ -1082,7 +1103,7 @@ namespace MediaBrowser.Providers.Manager { await child.RefreshMetadata(options, cancellationToken).ConfigureAwait(false); - await child.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options, true).ConfigureAwait(false); + await child.ValidateChildren(new SimpleProgress<double>(), cancellationToken, options).ConfigureAwait(false); } } @@ -1118,12 +1139,13 @@ namespace MediaBrowser.Providers.Manager } } + /// <inheritdoc/> public Task RefreshFullItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken) { return RefreshItem(item, options, cancellationToken); } - private bool _disposed; + /// <inheritdoc/> public void Dispose() { _disposed = true; diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 446e27df6..42c7cec53 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -16,8 +16,8 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.5" /> - <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.5" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" /> + <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.6" /> <PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" /> <PackageReference Include="PlaylistsNET" Version="1.0.6" /> <PackageReference Include="TvDbSharper" Version="3.2.0" /> diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html index fbf413f2b..82f26a8f2 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/AudioDb/Configuration/config.html @@ -28,29 +28,31 @@ pluginId: "a629c0da-fac5-4c7e-931a-7174223f14c8" }; - $('.configPage').on('pageshow', function () { - Dashboard.showLoadingMsg(); - ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { - $('#enable').checked = config.Enable; - $('#replaceAlbumName').checked = config.ReplaceAlbumName; - - Dashboard.hideLoadingMsg(); + document.querySelector('.configPage') + .addEventListener('pageshow', function () { + Dashboard.showLoadingMsg(); + ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { + document.querySelector('#enable').checked = config.Enable; + document.querySelector('#replaceAlbumName').checked = config.ReplaceAlbumName; + + Dashboard.hideLoadingMsg(); + }); }); - }); - - $('.configForm').on('submit', function (e) { - Dashboard.showLoadingMsg(); - var form = this; - ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { - config.Enable = $('#enable', form).checked; - config.ReplaceAlbumName = $('#replaceAlbumName', form).checked; - - ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); + document.querySelector('.configForm') + .addEventListener('submit', function (e) { + Dashboard.showLoadingMsg(); + + ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { + config.Enable = document.querySelector('#enable').checked; + config.ReplaceAlbumName = document.querySelector('#replaceAlbumName').checked; + + ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); + }); + + e.preventDefault(); + return false; }); - - return false; - }); </script> </div> </body> diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html index 90196b046..1945e6cb4 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/config.html @@ -36,33 +36,47 @@ uniquePluginId: "8c95c4d2-e50c-4fb0-a4f3-6c06ff0f9a1a" }; - $('.musicBrainzConfigPage').on('pageshow', function () { - Dashboard.showLoadingMsg(); - ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) { - $('#server').val(config.Server).change(); - $('#rateLimit').val(config.RateLimit).change(); - $('#enable').checked = config.Enable; - $('#replaceArtistName').checked = config.ReplaceArtistName; + document.querySelector('.musicBrainzConfigPage') + .addEventListener('pageshow', function () { + Dashboard.showLoadingMsg(); + ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) { + var server = document.querySelector('#server'); + server.value = config.Server; + server.dispatchEvent(new Event('change', { + bubbles: true, + cancelable: false + })); + + var rateLimit = document.querySelector('#rateLimit'); + rateLimit.value = config.RateLimit; + rateLimit.dispatchEvent(new Event('change', { + bubbles: true, + cancelable: false + })); + + document.querySelector('#enable').checked = config.Enable; + document.querySelector('#replaceArtistName').checked = config.ReplaceArtistName; - Dashboard.hideLoadingMsg(); + Dashboard.hideLoadingMsg(); + }); }); - }); - - $('.musicBrainzConfigForm').on('submit', function (e) { - Dashboard.showLoadingMsg(); - - var form = this; - ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) { - config.Server = $('#server', form).val(); - config.RateLimit = $('#rateLimit', form).val(); - config.Enable = $('#enable', form).checked; - config.ReplaceArtistName = $('#replaceArtistName', form).checked; - - ApiClient.updatePluginConfiguration(MusicBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); + + document.querySelector('.musicBrainzConfigForm') + .addEventListener('submit', function (e) { + Dashboard.showLoadingMsg(); + + ApiClient.getPluginConfiguration(MusicBrainzPluginConfig.uniquePluginId).then(function (config) { + config.Server = document.querySelector('#server').value; + config.RateLimit = document.querySelector('#rateLimit').value; + config.Enable = document.querySelector('#enable').checked; + config.ReplaceArtistName = document.querySelector('#replaceArtistName').checked; + + ApiClient.updatePluginConfiguration(MusicBrainzPluginConfig.uniquePluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); + }); + + e.preventDefault(); + return false; }); - - return false; - }); </script> </div> </body> diff --git a/MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html b/MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html index 8b117ec8d..f4375b3cb 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/Omdb/Configuration/config.html @@ -24,25 +24,28 @@ pluginId: "a628c0da-fac5-4c7e-9d1a-7134223f14c8" }; - $('.configPage').on('pageshow', function () { - Dashboard.showLoadingMsg(); - ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { - $('#castAndCrew').checked = config.CastAndCrew; - Dashboard.hideLoadingMsg(); + document.querySelector('.configPage') + .addEventListener('pageshow', function () { + Dashboard.showLoadingMsg(); + ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { + document.querySelector('#castAndCrew').checked = config.CastAndCrew; + Dashboard.hideLoadingMsg(); + }); }); - }); - $('.configForm').on('submit', function (e) { - Dashboard.showLoadingMsg(); - - var form = this; - ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { - config.CastAndCrew = $('#castAndCrew', form).checked; - ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); + + document.querySelector('.configForm') + .addEventListener('submit', function (e) { + Dashboard.showLoadingMsg(); + + ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { + config.CastAndCrew = document.querySelector('#castAndCrew').checked; + ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult); + }); + + e.preventDefault(); + return false; }); - - return false; - }); </script> </div> </body> diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs index 2c6682f82..cd2f96f14 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Providers; @@ -229,6 +230,45 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb return GetEpisodesPageAsync(tvdbId, 1, episodeQuery, language, cancellationToken); } + public async IAsyncEnumerable<KeyType> GetImageKeyTypesForSeriesAsync(int tvdbId, string language, [EnumeratorCancellation] CancellationToken cancellationToken) + { + var cacheKey = GenerateKey(nameof(TvDbClient.Series.GetImagesSummaryAsync), tvdbId); + var imagesSummary = await TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesSummaryAsync(tvdbId, cancellationToken)).ConfigureAwait(false); + + if (imagesSummary.Data.Fanart > 0) + { + yield return KeyType.Fanart; + } + + if (imagesSummary.Data.Series > 0) + { + yield return KeyType.Series; + } + + if (imagesSummary.Data.Poster > 0) + { + yield return KeyType.Poster; + } + } + + public async IAsyncEnumerable<KeyType> GetImageKeyTypesForSeasonAsync(int tvdbId, string language, [EnumeratorCancellation] CancellationToken cancellationToken) + { + var cacheKey = GenerateKey(nameof(TvDbClient.Series.GetImagesSummaryAsync), tvdbId); + var imagesSummary = await TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesSummaryAsync(tvdbId, cancellationToken)).ConfigureAwait(false); + + if (imagesSummary.Data.Season > 0) + { + yield return KeyType.Season; + } + + if (imagesSummary.Data.Fanart > 0) + { + yield return KeyType.Fanart; + } + + // TODO seasonwide is not supported in TvDbSharper + } + private async Task<T> TryGetValue<T>(string key, string language, Func<Task<T>> resultFactory) { if (_cache.TryGetValue(key, out T cachedValue)) diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs index 5af99a573..e9ba20475 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs @@ -65,8 +65,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb var language = item.GetPreferredMetadataLanguage(); var remoteImages = new List<RemoteImageInfo>(); - var keyTypes = new[] { KeyType.Season, KeyType.Seasonwide, KeyType.Fanart }; - foreach (var keyType in keyTypes) + var keyTypes = _tvdbClientManager.GetImageKeyTypesForSeasonAsync(tvdbId, language, cancellationToken).ConfigureAwait(false); + await foreach (var keyType in keyTypes) { var imageQuery = new ImagesQuery { diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs index 7dd012825..c33aefbc1 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs @@ -59,9 +59,10 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb var language = item.GetPreferredMetadataLanguage(); var remoteImages = new List<RemoteImageInfo>(); - var keyTypes = new[] { KeyType.Poster, KeyType.Series, KeyType.Fanart }; var tvdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tvdb)); - foreach (KeyType keyType in keyTypes) + var allowedKeyTypes = _tvdbClientManager.GetImageKeyTypesForSeriesAsync(tvdbId, language, cancellationToken) + .ConfigureAwait(false); + await foreach (KeyType keyType in allowedKeyTypes) { var imageQuery = new ImagesQuery { diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs index b3641dc9f..df48629d0 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs @@ -247,10 +247,15 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb { Name = tvdbTitles.FirstOrDefault(), ProductionYear = firstAired.Year, - SearchProviderName = Name, - ImageUrl = TvdbUtils.BannerUrl + seriesSearchResult.Banner + SearchProviderName = Name }; + if (!string.IsNullOrEmpty(seriesSearchResult.Banner)) + { + // Results from their Search endpoints already include the /banners/ part in the url, because reasons... + remoteSearchResult.ImageUrl = TvdbUtils.TvdbImageBaseUrl + seriesSearchResult.Banner; + } + try { var seriesSesult = @@ -365,10 +370,14 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb Type = PersonType.Actor, Name = (actor.Name ?? string.Empty).Trim(), Role = actor.Role, - ImageUrl = TvdbUtils.BannerUrl + actor.Image, SortOrder = actor.SortOrder }; + if (!string.IsNullOrEmpty(actor.Image)) + { + personInfo.ImageUrl = TvdbUtils.BannerUrl + actor.Image; + } + if (!string.IsNullOrWhiteSpace(personInfo.Name)) { result.AddPerson(personInfo); diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs index 3f71041b2..37a8d04a6 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs @@ -9,7 +9,8 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb { public const string TvdbApiKey = "OG4V3YJ3FAP7FP2K"; public const string TvdbBaseUrl = "https://www.thetvdb.com/"; - public const string BannerUrl = TvdbBaseUrl + "banners/"; + public const string TvdbImageBaseUrl = "https://www.thetvdb.com"; + public const string BannerUrl = TvdbImageBaseUrl + "/banners/"; public static ImageType GetImageTypeFromKeyType(string keyType) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index 5e2b83294..74870c999 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -148,7 +148,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies using (HttpResponseInfo response = await GetMovieDbResponse(new HttpRequestOptions { - Url = string.Format(TmdbConfigUrl, TmdbUtils.ApiKey), + Url = string.Format(CultureInfo.InvariantCulture, TmdbConfigUrl, TmdbUtils.ApiKey), CancellationToken = cancellationToken, AcceptHeader = TmdbUtils.AcceptHeader }).ConfigureAwait(false)) @@ -245,7 +245,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies preferredLanguage = "alllang"; } - var filename = string.Format("all-{0}.json", preferredLanguage); + var filename = string.Format(CultureInfo.InvariantCulture, "all-{0}.json", preferredLanguage); return Path.Combine(path, filename); } @@ -276,7 +276,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies languages.Add("en"); } - return string.Join(",", languages.ToArray()); + return string.Join(",", languages); } public static string NormalizeLanguage(string language) @@ -321,11 +321,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies /// <returns>Task{CompleteMovieData}.</returns> internal async Task<MovieResult> FetchMainResult(string id, bool isTmdbId, string language, CancellationToken cancellationToken) { - var url = string.Format(GetMovieInfo3, id, TmdbUtils.ApiKey); + var url = string.Format(CultureInfo.InvariantCulture, GetMovieInfo3, id, TmdbUtils.ApiKey); if (!string.IsNullOrEmpty(language)) { - url += string.Format("&language={0}", NormalizeLanguage(language)); + url += string.Format(CultureInfo.InvariantCulture, "&language={0}", NormalizeLanguage(language)); // Get images in english and with no language url += "&include_image_language=" + GetImageLanguagesParam(language); @@ -377,7 +377,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies { _logger.LogInformation("MovieDbProvider couldn't find meta for language " + language + ". Trying English..."); - url = string.Format(GetMovieInfo3, id, TmdbUtils.ApiKey) + "&language=en"; + url = string.Format(CultureInfo.InvariantCulture, GetMovieInfo3, id, TmdbUtils.ApiKey) + "&language=en"; if (!string.IsNullOrEmpty(language)) { diff --git a/debian/changelog b/debian/changelog index 35fb65957..d38d03805 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +jellyfin-server (10.6.0-2) unstable; urgency=medium + + * Fix upgrade bug + + -- Joshua Boniface <joshua@boniface.me> Sun, 19 Jul 22:47:27 -0400 + jellyfin-server (10.6.0-1) unstable; urgency=medium * Forthcoming stable release diff --git a/debian/control b/debian/control index 896d8286b..39c2aa055 100644 --- a/debian/control +++ b/debian/control @@ -15,9 +15,8 @@ Vcs-Git: https://github.org/jellyfin/jellyfin.git Vcs-Browser: https://github.org/jellyfin/jellyfin Package: jellyfin-server -Replaces: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server -Breaks: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server -Conflicts: mediabrowser, emby, emby-server-beta, jellyfin-dev, emby-server +Replaces: jellyfin (<<10.6.0) +Breaks: jellyfin (<<10.6.0) Architecture: any Depends: at, libsqlite3-0, diff --git a/deployment/build.windows.amd64 b/deployment/build.windows.amd64 index 3fabc2cac..bd5dc6438 100755 --- a/deployment/build.windows.amd64 +++ b/deployment/build.windows.amd64 @@ -8,8 +8,7 @@ set -o xtrace # Version variables NSSM_VERSION="nssm-2.24-101-g897c7ad" NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip" -FFMPEG_VERSION="ffmpeg-4.2.1-win64-static" -FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win64/static/${FFMPEG_VERSION}.zip" +FFMPEG_URL="https://repo.jellyfin.org/releases/server/windows/ffmpeg/jellyfin-ffmpeg.zip"; # Move to source directory pushd ${SOURCE_DIR} @@ -29,12 +28,11 @@ dotnet publish Jellyfin.Server --configuration Release --self-contained --runtim # Prepare addins addin_build_dir="$( mktemp -d )" wget ${NSSM_URL} -O ${addin_build_dir}/nssm.zip -wget ${FFMPEG_URL} -O ${addin_build_dir}/ffmpeg.zip +wget ${FFMPEG_URL} -O ${addin_build_dir}/jellyfin-ffmpeg.zip unzip ${addin_build_dir}/nssm.zip -d ${addin_build_dir} cp ${addin_build_dir}/${NSSM_VERSION}/win64/nssm.exe ${output_dir}/nssm.exe -unzip ${addin_build_dir}/ffmpeg.zip -d ${addin_build_dir} -cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffmpeg.exe ${output_dir}/ffmpeg.exe -cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffprobe.exe ${output_dir}/ffprobe.exe +unzip ${addin_build_dir}/jellyfin-ffmpeg.zip -d ${addin_build_dir}/jellyfin-ffmpeg +cp ${addin_build_dir}/jellyfin-ffmpeg/* ${output_dir} rm -rf ${addin_build_dir} # Prepare scripts diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index 100f95424..132050e7b 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -13,15 +13,15 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="AutoFixture" Version="4.12.0" /> + <PackageReference Include="AutoFixture" Version="4.13.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.12.0" /> <PackageReference Include="AutoFixture.Xunit2" Version="4.12.0" /> - <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.5" /> + <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.6" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> - <PackageReference Include="Moq" Version="4.14.4" /> + <PackageReference Include="Moq" Version="4.14.5" /> </ItemGroup> <!-- Code Analyzers --> diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs index af29fec87..9eb601edf 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs @@ -17,6 +17,7 @@ namespace Jellyfin.MediaEncoding.Tests } [Theory] + [InlineData(EncoderValidatorTestsData.FFmpegV43Output, true)] [InlineData(EncoderValidatorTestsData.FFmpegV421Output, true)] [InlineData(EncoderValidatorTestsData.FFmpegV42Output, true)] [InlineData(EncoderValidatorTestsData.FFmpegV414Output, true)] @@ -32,6 +33,7 @@ namespace Jellyfin.MediaEncoding.Tests { public IEnumerator<object?[]> GetEnumerator() { + yield return new object?[] { EncoderValidatorTestsData.FFmpegV43Output, new Version(4, 3) }; yield return new object?[] { EncoderValidatorTestsData.FFmpegV421Output, new Version(4, 2, 1) }; yield return new object?[] { EncoderValidatorTestsData.FFmpegV42Output, new Version(4, 2) }; yield return new object?[] { EncoderValidatorTestsData.FFmpegV414Output, new Version(4, 1, 4) }; diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs index c46c9578b..f5ff3d723 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs @@ -2,6 +2,18 @@ namespace Jellyfin.MediaEncoding.Tests { internal static class EncoderValidatorTestsData { + public const string FFmpegV43Output = @"ffmpeg version 4.3 Copyright (c) 2000-2020 the FFmpeg developers +built with gcc 7 (Ubuntu 7.5.0-3ubuntu1~18.04) +configuration: --prefix=/usr/lib/jellyfin-ffmpeg --target-os=linux --disable-doc --disable-ffplay --disable-shared --disable-libxcb --disable-vdpau --disable-sdl2 --disable-xlib --enable-gpl --enable-version3 --enable-static --enable-libfontconfig --enable-fontconfig --enable-gmp --enable-gnutls --enable-libass --enable-libbluray --enable-libdrm --enable-libfreetype --enable-libfribidi --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libx264 --enable-libx265 --enable-libzvbi --arch=amd64 --enable-amf --enable-nvenc --enable-nvdec --enable-vaapi --enable-opencl +libavutil 56. 51.100 / 56. 51.100 +libavcodec 58. 91.100 / 58. 91.100 +libavformat 58. 45.100 / 58. 45.100 +libavdevice 58. 10.100 / 58. 10.100 +libavfilter 7. 85.100 / 7. 85.100 +libswscale 5. 7.100 / 5. 7.100 +libswresample 3. 7.100 / 3. 7.100 +libpostproc 55. 7.100 / 55. 7.100"; + public const string FFmpegV421Output = @"ffmpeg version 4.2.1 Copyright (c) 2000-2019 the FFmpeg developers built with gcc 9.1.1 (GCC) 20190807 configuration: --enable-gpl --enable-version3 --enable-sdl2 --enable-fontconfig --enable-gnutls --enable-iconv --enable-libass --enable-libdav1d --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libopus --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libtheora --enable-libtwolame --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libzimg --enable-lzma --enable-zlib --enable-gmp --enable-libvidstab --enable-libvorbis --enable-libvo-amrwbenc --enable-libmysofa --enable-libspeex --enable-libxvid --enable-libaom --enable-libmfx --enable-amf --enable-ffnvcodec --enable-cuvid --enable-d3d11va --enable-nvenc --enable-nvdec --enable-dxva2 --enable-avisynth --enable-libopenmpt diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index dd53c9902..03187f4b9 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -14,9 +14,9 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="AutoFixture" Version="4.12.0" /> + <PackageReference Include="AutoFixture" Version="4.13.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.12.0" /> - <PackageReference Include="Moq" Version="4.14.4" /> + <PackageReference Include="Moq" Version="4.14.5" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> diff --git a/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj b/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj index a767c0039..8309faebd 100644 --- a/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj +++ b/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj @@ -8,7 +8,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.5" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.6" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> diff --git a/windows/build-jellyfin.ps1 b/windows/build-jellyfin.ps1 index c762137a7..b76a8e0bb 100644 --- a/windows/build-jellyfin.ps1 +++ b/windows/build-jellyfin.ps1 @@ -47,7 +47,7 @@ function Install-FFMPEG { param( [string]$ResolvedInstallLocation, [string]$Architecture, - [string]$FFMPEGVersionX86 = "ffmpeg-4.2.1-win32-shared" + [string]$FFMPEGVersionX86 = "ffmpeg-4.3-win32-shared" ) Write-Verbose "Checking Architecture" if($Architecture -notin @('x86','x64')){ |
