diff options
163 files changed, 1949 insertions, 3002 deletions
diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml index 46c478b08..c829da98a 100644 --- a/.ci/azure-pipelines.yml +++ b/.ci/azure-pipelines.yml @@ -245,7 +245,7 @@ jobs: inputs: targetType: 'filePath' # Optional. Options: filePath, inline filePath: ./deployment/windows/build-jellyfin.ps1 # Required when targetType == FilePath - arguments: -InstallFFMPEG -InstallNSSM -MakeNSIS -UXLocation $(Agent.TempDirectory)\jellyfin-ux -InstallLocation $(build.artifactstagingdirectory) + arguments: -InstallFFMPEG -InstallNSSM -MakeNSIS -InstallTrayApp -UXLocation $(Agent.TempDirectory)\jellyfin-ux -InstallLocation $(build.artifactstagingdirectory) #script: '# Write your PowerShell commands here.Write-Host Hello World' # Required when targetType == Inline errorActionPreference: 'stop' # Optional. Options: stop, continue, silentlyContinue #failOnStderr: false # Optional diff --git a/.copr/Makefile b/.copr/Makefile index 84b98a011..ba330ada9 100644 --- a/.copr/Makefile +++ b/.copr/Makefile @@ -1,8 +1,59 @@ -srpm: - dnf -y install git - git submodule update --init --recursive - cd deployment/fedora-package-x64; \ - ./create_tarball.sh; \ - rpmbuild -bs pkg-src/jellyfin.spec \ - --define "_sourcedir $$PWD/pkg-src/" \ - --define "_srcrpmdir $(outdir)" +VERSION := $(shell sed -ne '/^Version:/s/.* *//p' \ + deployment/fedora-package-x64/pkg-src/jellyfin.spec) + +deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz: + curl -f -L -o deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz \ + https://github.com/jellyfin/jellyfin-web/archive/v$(VERSION).tar.gz \ + || curl -f -L -o deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz \ + https://github.com/jellyfin/jellyfin-web/archive/master.tar.gz \ + +srpm: deployment/fedora-package-x64/pkg-src/jellyfin-web-$(VERSION).tar.gz + cd deployment/fedora-package-x64; \ + SOURCE_DIR=../.. \ + WORKDIR="$${PWD}"; \ + package_temporary_dir="$${WORKDIR}/pkg-dist-tmp"; \ + pkg_src_dir="$${WORKDIR}/pkg-src"; \ + GNU_TAR=1; \ + tar \ + --transform "s,^\.,jellyfin-$(VERSION)," \ + --exclude='.git*' \ + --exclude='**/.git' \ + --exclude='**/.hg' \ + --exclude='**/.vs' \ + --exclude='**/.vscode' \ + --exclude='deployment' \ + --exclude='**/bin' \ + --exclude='**/obj' \ + --exclude='**/.nuget' \ + --exclude='*.deb' \ + --exclude='*.rpm' \ + -czf "pkg-src/jellyfin-$(VERSION).tar.gz" \ + -C $${SOURCE_DIR} ./ || GNU_TAR=0; \ + if [ $${GNU_TAR} -eq 0 ]; then \ + package_temporary_dir="$$(mktemp -d)"; \ + mkdir -p "$${package_temporary_dir}/jellyfin"; \ + tar \ + --exclude='.git*' \ + --exclude='**/.git' \ + --exclude='**/.hg' \ + --exclude='**/.vs' \ + --exclude='**/.vscode' \ + --exclude='deployment' \ + --exclude='**/bin' \ + --exclude='**/obj' \ + --exclude='**/.nuget' \ + --exclude='*.deb' \ + --exclude='*.rpm' \ + -czf "$${package_temporary_dir}/jellyfin/jellyfin-$(VERSION).tar.gz" \ + -C $${SOURCE_DIR} ./; \ + mkdir -p "$${package_temporary_dir}/jellyfin-$(VERSION)"; \ + tar -xzf "$${package_temporary_dir}/jellyfin/jellyfin-$(VERSION).tar.gz" \ + -C "$${package_temporary_dir}/jellyfin-$(VERSION); \ + rm -f "$${package_temporary_dir}/jellyfin/jellyfin-$(VERSION).tar.gz"; \ + tar -czf "$${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-$(VERSION).tar.gz" \ + -C "$${package_temporary_dir}" "jellyfin-$(VERSION); \ + rm -rf $${package_temporary_dir}; \ + fi; \ + rpmbuild -bs pkg-src/jellyfin.spec \ + --define "_sourcedir $$PWD/pkg-src/" \ + --define "_srcrpmdir $(outdir)" diff --git a/.github/stale.yml b/.github/stale.yml index 9ea0e3796..6b90978cc 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -11,6 +11,7 @@ exemptLabels: - future - feature - enhancement + - confirmed # Label to use when marking an issue as stale staleLabel: stale # Comment to post when marking an issue as stale. Set to `false` to disable diff --git a/.gitignore b/.gitignore index 34cf1a84c..42243f01a 100644 --- a/.gitignore +++ b/.gitignore @@ -268,3 +268,6 @@ doc/ # Deployment artifacts dist *.exe + +# BenchmarkDotNet artifacts +BenchmarkDotNet.Artifacts diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj index 34b49120b..8d6fabdb4 100644 --- a/Emby.Dlna/Emby.Dlna.csproj +++ b/Emby.Dlna/Emby.Dlna.csproj @@ -12,7 +12,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/Emby.Dlna/Ssdp/DeviceDiscovery.cs b/Emby.Dlna/Ssdp/DeviceDiscovery.cs index 298f68a28..c5f3593da 100644 --- a/Emby.Dlna/Ssdp/DeviceDiscovery.cs +++ b/Emby.Dlna/Ssdp/DeviceDiscovery.cs @@ -4,8 +4,6 @@ using System.Linq; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Events; -using MediaBrowser.Model.Net; -using Microsoft.Extensions.Logging; using Rssdp; using Rssdp.Infrastructure; @@ -15,13 +13,14 @@ namespace Emby.Dlna.Ssdp { private bool _disposed; - private readonly ILogger _logger; private readonly IServerConfigurationManager _config; private event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscoveredInternal; private int _listenerCount; private object _syncLock = new object(); + + /// <inheritdoc /> public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscovered { add @@ -31,6 +30,7 @@ namespace Emby.Dlna.Ssdp _listenerCount++; DeviceDiscoveredInternal += value; } + StartInternal(); } remove @@ -43,21 +43,16 @@ namespace Emby.Dlna.Ssdp } } + /// <inheritdoc /> public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceLeft; private SsdpDeviceLocator _deviceLocator; - private readonly ISocketFactory _socketFactory; private ISsdpCommunicationsServer _commsServer; - public DeviceDiscovery( - ILoggerFactory loggerFactory, - IServerConfigurationManager config, - ISocketFactory socketFactory) + public DeviceDiscovery(IServerConfigurationManager config) { - _logger = loggerFactory.CreateLogger(nameof(DeviceDiscovery)); _config = config; - _socketFactory = socketFactory; } // Call this method from somewhere in your code to start the search. @@ -82,8 +77,8 @@ namespace Emby.Dlna.Ssdp //_DeviceLocator.NotificationFilter = "upnp:rootdevice"; // Connect our event handler so we process devices as they are found - _deviceLocator.DeviceAvailable += deviceLocator_DeviceAvailable; - _deviceLocator.DeviceUnavailable += _DeviceLocator_DeviceUnavailable; + _deviceLocator.DeviceAvailable += OnDeviceLocatorDeviceAvailable; + _deviceLocator.DeviceUnavailable += OnDeviceLocatorDeviceUnavailable; var dueTime = TimeSpan.FromSeconds(5); var interval = TimeSpan.FromSeconds(_config.GetDlnaConfiguration().ClientDiscoveryIntervalSeconds); @@ -94,7 +89,7 @@ namespace Emby.Dlna.Ssdp } // Process each found device in the event handler - void deviceLocator_DeviceAvailable(object sender, DeviceAvailableEventArgs e) + private void OnDeviceLocatorDeviceAvailable(object sender, DeviceAvailableEventArgs e) { var originalHeaders = e.DiscoveredDevice.ResponseHeaders; @@ -115,7 +110,7 @@ namespace Emby.Dlna.Ssdp DeviceDiscoveredInternal?.Invoke(this, args); } - private void _DeviceLocator_DeviceUnavailable(object sender, DeviceUnavailableEventArgs e) + private void OnDeviceLocatorDeviceUnavailable(object sender, DeviceUnavailableEventArgs e) { var originalHeaders = e.DiscoveredDevice.ResponseHeaders; diff --git a/Emby.Drawing/Emby.Drawing.csproj b/Emby.Drawing/Emby.Drawing.csproj index 2e539f2c7..85cecdc44 100644 --- a/Emby.Drawing/Emby.Drawing.csproj +++ b/Emby.Drawing/Emby.Drawing.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> @@ -17,9 +17,4 @@ <Compile Include="..\SharedVersion.cs" /> </ItemGroup> - <PropertyGroup> - <!-- We need at least C# 7.1 for the "default literal" feature--> - <LangVersion>latest</LangVersion> - </PropertyGroup> - </Project> diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 88a9b46e6..9cf430daf 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -328,28 +328,32 @@ namespace Emby.Naming.Common // *** End Kodi Standard Naming - new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3})[^\\\/]*$") + // [bar] Foo - 1 [baz] + new EpisodeExpression(@".*?(\[.*?\])+.*?(?<seriesname>(\w+\s)+?)[-\s_]+(?<epnumber>\d+).*$"){ + IsNamed=false, + }, + new EpisodeExpression(@".*(\\|\/)[sS]?(?<seasonnumber>\d+)[xX](?<epnumber>\d+)[^\\\/]*$") { IsNamed = true }, - new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>\d{1,4})[x,X]?[eE](?<epnumber>\d{1,3})[^\\\/]*$") + new EpisodeExpression(@".*(\\|\/)[sS](?<seasonnumber>\d+)[x,X]?[eE](?<epnumber>\d+)[^\\\/]*$") { IsNamed = true }, - new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d{1,3}))[^\\\/]*$") + new EpisodeExpression(@".*(\\|\/)(?<seriesname>((?![sS]?\d{1,4}[xX]\d{1,3})[^\\\/])*)?([sS]?(?<seasonnumber>\d{1,4})[xX](?<epnumber>\d+))[^\\\/]*$") { IsNamed = true }, - new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})[^\\\/]*$") + new EpisodeExpression(@".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d+)[^\\\/]*$") { IsNamed = true }, // "01.avi" - new EpisodeExpression(@".*[\\\/](?<epnumber>\d{1,3})(-(?<endingepnumber>\d{2,3}))*\.\w+$") + new EpisodeExpression(@".*[\\\/](?<epnumber>\d+)(-(?<endingepnumber>\d+))*\.\w+$") { IsOptimistic = true, IsNamed = true @@ -654,9 +658,9 @@ namespace Emby.Naming.Common @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})((-| - )?[xXeE](?<endingepnumber>\d{1,3}))+[^\\\/]*$", @".*(\\|\/)(?<seriesname>[^\\\/]*)[sS](?<seasonnumber>\d{1,4})[xX\.]?[eE](?<epnumber>\d{1,3})(-[xX]?[eE]?(?<endingepnumber>\d{1,3}))+[^\\\/]*$" }.Select(i => new EpisodeExpression(i) - { - IsNamed = true - }).ToArray(); + { + IsNamed = true + }).ToArray(); VideoFileExtensions = extensions .Distinct(StringComparer.OrdinalIgnoreCase) diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index fca0aa1b3..a23fa3df7 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -24,8 +24,9 @@ <!-- Code analysers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.4" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> </ItemGroup> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> diff --git a/Emby.Notifications/Emby.Notifications.csproj b/Emby.Notifications/Emby.Notifications.csproj index cbd3bde4f..004ded77b 100644 --- a/Emby.Notifications/Emby.Notifications.csproj +++ b/Emby.Notifications/Emby.Notifications.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj index b57b93a8c..a71c75127 100644 --- a/Emby.Photos/Emby.Photos.csproj +++ b/Emby.Photos/Emby.Photos.csproj @@ -14,7 +14,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index 4832c19c4..7ec5252d0 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.AppBase /// <summary> /// The _configuration sync lock. /// </summary> - private object _configurationSyncLock = new object(); + private readonly object _configurationSyncLock = new object(); /// <summary> /// The _configuration. @@ -98,16 +98,31 @@ namespace Emby.Server.Implementations.AppBase public IApplicationPaths CommonApplicationPaths { get; private set; } /// <summary> - /// Gets the system configuration + /// Gets the system configuration. /// </summary> /// <value>The configuration.</value> public BaseApplicationConfiguration CommonConfiguration { get { - // Lazy load - LazyInitializer.EnsureInitialized(ref _configuration, ref _configurationLoaded, ref _configurationSyncLock, () => (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer)); - return _configuration; + if (_configurationLoaded) + { + return _configuration; + } + + lock (_configurationSyncLock) + { + if (_configurationLoaded) + { + return _configuration; + } + + _configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer); + + _configurationLoaded = true; + + return _configuration; + } } protected set { diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 04904fc4a..7cb7aa748 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -362,7 +362,7 @@ namespace Emby.Server.Implementations { _configuration = configuration; - XmlSerializer = new MyXmlSerializer(fileSystem, loggerFactory); + XmlSerializer = new MyXmlSerializer(); NetworkManager = networkManager; networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets; @@ -866,8 +866,7 @@ namespace Emby.Server.Implementations NotificationManager = new NotificationManager(LoggerFactory, UserManager, ServerConfigurationManager); serviceCollection.AddSingleton(NotificationManager); - serviceCollection.AddSingleton<IDeviceDiscovery>( - new DeviceDiscovery(LoggerFactory, ServerConfigurationManager, SocketFactory)); + serviceCollection.AddSingleton<IDeviceDiscovery>(new DeviceDiscovery(ServerConfigurationManager)); ChapterManager = new ChapterManager(LibraryManager, LoggerFactory, ServerConfigurationManager, ItemRepository); serviceCollection.AddSingleton(ChapterManager); @@ -906,7 +905,7 @@ namespace Emby.Server.Implementations _displayPreferencesRepository.Initialize(); - var userDataRepo = new SqliteUserDataRepository(LoggerFactory, ApplicationPaths); + var userDataRepo = new SqliteUserDataRepository(LoggerFactory.CreateLogger<SqliteUserDataRepository>(), ApplicationPaths); SetStaticProperties(); @@ -1730,7 +1729,7 @@ namespace Emby.Server.Implementations /// dns is prefixed with a valid Uri prefix. /// </summary> /// <param name="externalDns">The external dns prefix to get the hostname of.</param> - /// <returns>The hostname in <paramref name="externalDns"/></returns> + /// <returns>The hostname in <paramref name="externalDns"/>.</returns> private static string GetHostnameFromExternalDns(string externalDns) { if (string.IsNullOrEmpty(externalDns)) diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 8e5f5b561..22681fb49 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -470,7 +470,7 @@ namespace Emby.Server.Implementations.Channels _libraryManager.CreateItem(item, null); } - await item.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) + await item.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = !isNew && forceUpdate }, cancellationToken); @@ -1156,7 +1156,7 @@ namespace Emby.Server.Implementations.Channels if (isNew || forceUpdate || item.DateLastRefreshed == default(DateTime)) { - _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), RefreshPriority.Normal); + _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal); } return item; diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index bb5057b1c..6d414be73 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -149,7 +149,7 @@ namespace Emby.Server.Implementations.Collections if (options.ItemIdList.Length > 0) { - AddToCollection(collection.Id, options.ItemIdList, false, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) + AddToCollection(collection.Id, options.ItemIdList, false, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { // The initial adding of items is going to create a local metadata file // This will cause internet metadata to be skipped as a result @@ -158,7 +158,7 @@ namespace Emby.Server.Implementations.Collections } else { - _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), RefreshPriority.High); + _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High); } CollectionCreated?.Invoke(this, new CollectionCreatedEventArgs @@ -178,12 +178,12 @@ namespace Emby.Server.Implementations.Collections public void AddToCollection(Guid collectionId, IEnumerable<string> ids) { - AddToCollection(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem))); + AddToCollection(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem))); } public void AddToCollection(Guid collectionId, IEnumerable<Guid> ids) { - AddToCollection(collectionId, ids.Select(i => i.ToString("N", CultureInfo.InvariantCulture)), true, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem))); + AddToCollection(collectionId, ids.Select(i => i.ToString("N", CultureInfo.InvariantCulture)), true, new MetadataRefreshOptions(new DirectoryService(_fileSystem))); } private void AddToCollection(Guid collectionId, IEnumerable<string> ids, bool fireEvent, MetadataRefreshOptions refreshOptions) @@ -287,7 +287,7 @@ namespace Emby.Server.Implementations.Collections } collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); - _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) + _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, RefreshPriority.High); diff --git a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs index 2cd4d65b3..2f6c1288d 100644 --- a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs @@ -110,8 +110,8 @@ namespace Emby.Server.Implementations.Data using (var statement = connection.PrepareStatement("replace into userdisplaypreferences (id, userid, client, data) values (@id, @userId, @client, @data)")) { - statement.TryBind("@id", displayPreferences.Id.ToGuidBlob()); - statement.TryBind("@userId", userId.ToGuidBlob()); + statement.TryBind("@id", new Guid(displayPreferences.Id).ToByteArray()); + statement.TryBind("@userId", userId.ToByteArray()); statement.TryBind("@client", client); statement.TryBind("@data", serialized); @@ -170,8 +170,8 @@ namespace Emby.Server.Implementations.Data { using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where id = @id and userId=@userId and client=@client")) { - statement.TryBind("@id", guidId.ToGuidBlob()); - statement.TryBind("@userId", userId.ToGuidBlob()); + statement.TryBind("@id", guidId.ToByteArray()); + statement.TryBind("@userId", userId.ToByteArray()); statement.TryBind("@client", client); foreach (var row in statement.ExecuteQuery()) @@ -200,7 +200,7 @@ namespace Emby.Server.Implementations.Data using (var connection = GetConnection(true)) using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId")) { - statement.TryBind("@userId", userId.ToGuidBlob()); + statement.TryBind("@userId", userId.ToByteArray()); foreach (var row in statement.ExecuteQuery()) { diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs index e7c394b54..c76ae0cac 100644 --- a/Emby.Server.Implementations/Data/SqliteExtensions.cs +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -9,6 +9,47 @@ namespace Emby.Server.Implementations.Data { public static class SqliteExtensions { + private const string DatetimeFormatUtc = "yyyy-MM-dd HH:mm:ss.FFFFFFFK"; + private const string DatetimeFormatLocal = "yyyy-MM-dd HH:mm:ss.FFFFFFF"; + + /// <summary> + /// An array of ISO-8601 DateTime formats that we support parsing. + /// </summary> + private static readonly string[] _datetimeFormats = new string[] + { + "THHmmssK", + "THHmmK", + "HH:mm:ss.FFFFFFFK", + "HH:mm:ssK", + "HH:mmK", + DatetimeFormatUtc, + "yyyy-MM-dd HH:mm:ssK", + "yyyy-MM-dd HH:mmK", + "yyyy-MM-ddTHH:mm:ss.FFFFFFFK", + "yyyy-MM-ddTHH:mmK", + "yyyy-MM-ddTHH:mm:ssK", + "yyyyMMddHHmmssK", + "yyyyMMddHHmmK", + "yyyyMMddTHHmmssFFFFFFFK", + "THHmmss", + "THHmm", + "HH:mm:ss.FFFFFFF", + "HH:mm:ss", + "HH:mm", + DatetimeFormatLocal, + "yyyy-MM-dd HH:mm:ss", + "yyyy-MM-dd HH:mm", + "yyyy-MM-ddTHH:mm:ss.FFFFFFF", + "yyyy-MM-ddTHH:mm", + "yyyy-MM-ddTHH:mm:ss", + "yyyyMMddHHmmss", + "yyyyMMddHHmm", + "yyyyMMddTHHmmssFFFFFFF", + "yyyy-MM-dd", + "yyyyMMdd", + "yy-MM-dd" + }; + public static void RunQueries(this SQLiteDatabaseConnection connection, string[] queries) { if (queries == null) @@ -22,16 +63,6 @@ namespace Emby.Server.Implementations.Data }); } - public static byte[] ToGuidBlob(this string str) - { - return ToGuidBlob(new Guid(str)); - } - - public static byte[] ToGuidBlob(this Guid guid) - { - return guid.ToByteArray(); - } - public static Guid ReadGuidFromBlob(this IResultSetValue result) { return new Guid(result.ToBlob()); @@ -50,58 +81,16 @@ namespace Emby.Server.Implementations.Data CultureInfo.InvariantCulture); } - private static string GetDateTimeKindFormat( - DateTimeKind kind) - { - return (kind == DateTimeKind.Utc) ? _datetimeFormatUtc : _datetimeFormatLocal; - } - - /// <summary> - /// An array of ISO-8601 DateTime formats that we support parsing. - /// </summary> - private static string[] _datetimeFormats = new string[] { - "THHmmssK", - "THHmmK", - "HH:mm:ss.FFFFFFFK", - "HH:mm:ssK", - "HH:mmK", - "yyyy-MM-dd HH:mm:ss.FFFFFFFK", /* NOTE: UTC default (5). */ - "yyyy-MM-dd HH:mm:ssK", - "yyyy-MM-dd HH:mmK", - "yyyy-MM-ddTHH:mm:ss.FFFFFFFK", - "yyyy-MM-ddTHH:mmK", - "yyyy-MM-ddTHH:mm:ssK", - "yyyyMMddHHmmssK", - "yyyyMMddHHmmK", - "yyyyMMddTHHmmssFFFFFFFK", - "THHmmss", - "THHmm", - "HH:mm:ss.FFFFFFF", - "HH:mm:ss", - "HH:mm", - "yyyy-MM-dd HH:mm:ss.FFFFFFF", /* NOTE: Non-UTC default (19). */ - "yyyy-MM-dd HH:mm:ss", - "yyyy-MM-dd HH:mm", - "yyyy-MM-ddTHH:mm:ss.FFFFFFF", - "yyyy-MM-ddTHH:mm", - "yyyy-MM-ddTHH:mm:ss", - "yyyyMMddHHmmss", - "yyyyMMddHHmm", - "yyyyMMddTHHmmssFFFFFFF", - "yyyy-MM-dd", - "yyyyMMdd", - "yy-MM-dd" - }; - - private static string _datetimeFormatUtc = _datetimeFormats[5]; - private static string _datetimeFormatLocal = _datetimeFormats[19]; + private static string GetDateTimeKindFormat(DateTimeKind kind) + => (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal; public static DateTime ReadDateTime(this IResultSetValue result) { var dateText = result.ToString(); return DateTime.ParseExact( - dateText, _datetimeFormats, + dateText, + _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None).ToUniversalTime(); } @@ -139,7 +128,10 @@ namespace Emby.Server.Implementations.Data public static void Attach(SQLiteDatabaseConnection db, string path, string alias) { - var commandText = string.Format("attach @path as {0};", alias); + var commandText = string.Format( + CultureInfo.InvariantCulture, + "attach @path as {0};", + alias); using (var statement = db.PrepareStatement(commandText)) { @@ -186,10 +178,7 @@ namespace Emby.Server.Implementations.Data private static void CheckName(string name) { #if DEBUG - //if (!name.IndexOf("@", StringComparison.OrdinalIgnoreCase) != 0) - { - throw new Exception("Invalid param name: " + name); - } + throw new ArgumentException("Invalid param name: " + name, nameof(name)); #endif } @@ -264,7 +253,7 @@ namespace Emby.Server.Implementations.Data { if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) { - bindParam.Bind(value.ToGuidBlob()); + bindParam.Bind(value.ToByteArray()); } else { @@ -392,8 +381,7 @@ namespace Emby.Server.Implementations.Data } } - public static IEnumerable<IReadOnlyList<IResultSetValue>> ExecuteQuery( - this IStatement This) + public static IEnumerable<IReadOnlyList<IResultSetValue>> ExecuteQuery(this IStatement This) { while (This.MoveNext()) { diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 30027ff0d..65f8a915f 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -546,7 +546,7 @@ namespace Emby.Server.Implementations.Data { using (var saveImagesStatement = base.PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id")) { - saveImagesStatement.TryBind("@Id", item.Id.ToGuidBlob()); + saveImagesStatement.TryBind("@Id", item.Id.ToByteArray()); saveImagesStatement.TryBind("@Images", SerializeImages(item)); saveImagesStatement.MoveNext(); @@ -1988,7 +1988,7 @@ namespace Emby.Server.Implementations.Data throw new ArgumentNullException(nameof(chapters)); } - var idBlob = id.ToGuidBlob(); + var idBlob = id.ToByteArray(); using (var connection = GetConnection()) { @@ -3767,7 +3767,7 @@ namespace Emby.Server.Implementations.Data if (statement != null) { - statement.TryBind(paramName, personId.ToGuidBlob()); + statement.TryBind(paramName, personId.ToByteArray()); } index++; } @@ -3978,7 +3978,7 @@ namespace Emby.Server.Implementations.Data clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))"); if (statement != null) { - statement.TryBind(paramName, artistId.ToGuidBlob()); + statement.TryBind(paramName, artistId.ToByteArray()); } index++; } @@ -3997,7 +3997,7 @@ namespace Emby.Server.Implementations.Data clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=1))"); if (statement != null) { - statement.TryBind(paramName, artistId.ToGuidBlob()); + statement.TryBind(paramName, artistId.ToByteArray()); } index++; } @@ -4016,7 +4016,7 @@ namespace Emby.Server.Implementations.Data clauses.Add("((select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from itemvalues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from itemvalues where ItemId=Guid and Type=1))"); if (statement != null) { - statement.TryBind(paramName, artistId.ToGuidBlob()); + statement.TryBind(paramName, artistId.ToByteArray()); } index++; } @@ -4035,7 +4035,7 @@ namespace Emby.Server.Implementations.Data clauses.Add("Album in (select Name from typedbaseitems where guid=" + paramName + ")"); if (statement != null) { - statement.TryBind(paramName, albumId.ToGuidBlob()); + statement.TryBind(paramName, albumId.ToByteArray()); } index++; } @@ -4054,7 +4054,7 @@ namespace Emby.Server.Implementations.Data clauses.Add("(guid not in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))"); if (statement != null) { - statement.TryBind(paramName, artistId.ToGuidBlob()); + statement.TryBind(paramName, artistId.ToByteArray()); } index++; } @@ -4073,7 +4073,7 @@ namespace Emby.Server.Implementations.Data clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=2))"); if (statement != null) { - statement.TryBind(paramName, genreId.ToGuidBlob()); + statement.TryBind(paramName, genreId.ToByteArray()); } index++; } @@ -4144,7 +4144,7 @@ namespace Emby.Server.Implementations.Data if (statement != null) { - statement.TryBind(paramName, studioId.ToGuidBlob()); + statement.TryBind(paramName, studioId.ToByteArray()); } index++; } @@ -4920,7 +4920,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type { connection.RunInTransaction(db => { - var idBlob = id.ToGuidBlob(); + var idBlob = id.ToByteArray(); // Delete people ExecuteWithSingleParam(db, "delete from People where ItemId=@Id", idBlob); @@ -5039,7 +5039,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type whereClauses.Add("ItemId=@ItemId"); if (statement != null) { - statement.TryBind("@ItemId", query.ItemId.ToGuidBlob()); + statement.TryBind("@ItemId", query.ItemId.ToByteArray()); } } if (!query.AppearsInItemId.Equals(Guid.Empty)) @@ -5047,7 +5047,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type whereClauses.Add("Name in (Select Name from People where ItemId=@AppearsInItemId)"); if (statement != null) { - statement.TryBind("@AppearsInItemId", query.AppearsInItemId.ToGuidBlob()); + statement.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray()); } } var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList(); @@ -5116,7 +5116,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type CheckDisposed(); - var itemIdBlob = itemId.ToGuidBlob(); + var itemIdBlob = itemId.ToByteArray(); // First delete deleteAncestorsStatement.Reset(); @@ -5150,7 +5150,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type var ancestorId = ancestorIds[i]; - statement.TryBind("@AncestorId" + index, ancestorId.ToGuidBlob()); + statement.TryBind("@AncestorId" + index, ancestorId.ToByteArray()); statement.TryBind("@AncestorIdText" + index, ancestorId.ToString("N", CultureInfo.InvariantCulture)); } @@ -5615,7 +5615,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type CheckDisposed(); - var guidBlob = itemId.ToGuidBlob(); + var guidBlob = itemId.ToByteArray(); // First delete db.Execute("delete from ItemValues where ItemId=@Id", guidBlob); @@ -5639,10 +5639,13 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type { if (isSubsequentRow) { - insertText.Append(","); + insertText.Append(','); } - insertText.AppendFormat("(@ItemId, @Type{0}, @Value{0}, @CleanValue{0})", i.ToString(CultureInfo.InvariantCulture)); + insertText.AppendFormat( + CultureInfo.InvariantCulture, + "(@ItemId, @Type{0}, @Value{0}, @CleanValue{0})", + i); isSubsequentRow = true; } @@ -5695,7 +5698,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type { connection.RunInTransaction(db => { - var itemIdBlob = itemId.ToGuidBlob(); + var itemIdBlob = itemId.ToByteArray(); // First delete chapters db.Execute("delete from People where ItemId=@ItemId", itemIdBlob); @@ -5814,7 +5817,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type using (var statement = PrepareStatement(connection, cmdText)) { - statement.TryBind("@ItemId", query.ItemId.ToGuidBlob()); + statement.TryBind("@ItemId", query.ItemId.ToByteArray()); if (query.Type.HasValue) { @@ -5856,7 +5859,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type { connection.RunInTransaction(db => { - var itemIdBlob = id.ToGuidBlob(); + var itemIdBlob = id.ToByteArray(); // First delete chapters db.Execute("delete from mediastreams where ItemId=@ItemId", itemIdBlob); diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index 9d4855bcf..26ac17bdc 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Threading; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities; @@ -15,23 +14,19 @@ namespace Emby.Server.Implementations.Data public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository { public SqliteUserDataRepository( - ILoggerFactory loggerFactory, + ILogger<SqliteUserDataRepository> logger, IApplicationPaths appPaths) - : base(loggerFactory.CreateLogger(nameof(SqliteUserDataRepository))) + : base(logger) { DbFilePath = Path.Combine(appPaths.DataPath, "library.db"); } - /// <summary> - /// Gets the name of the repository - /// </summary> - /// <value>The name.</value> + /// <inheritdoc /> public string Name => "SQLite"; /// <summary> - /// Opens the connection to the database + /// Opens the connection to the database. /// </summary> - /// <returns>Task.</returns> public void Initialize(IUserManager userManager, SemaphoreSlim dbLock, SQLiteDatabaseConnection dbConnection) { WriteLock.Dispose(); @@ -97,7 +92,7 @@ namespace Emby.Server.Implementations.Data continue; } - statement.TryBind("@UserId", user.Id.ToGuidBlob()); + statement.TryBind("@UserId", user.Id.ToByteArray()); statement.TryBind("@InternalUserId", user.InternalId); statement.MoveNext(); diff --git a/Emby.Server.Implementations/Data/SqliteUserRepository.cs b/Emby.Server.Implementations/Data/SqliteUserRepository.cs index 80fe278f8..26798993b 100644 --- a/Emby.Server.Implementations/Data/SqliteUserRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserRepository.cs @@ -116,7 +116,7 @@ namespace Emby.Server.Implementations.Data { using (var statement = db.PrepareStatement("insert into LocalUsersv2 (guid, data) values (@guid, @data)")) { - statement.TryBind("@guid", user.Id.ToGuidBlob()); + statement.TryBind("@guid", user.Id.ToByteArray()); statement.TryBind("@data", serialized); statement.MoveNext(); diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index ea4444268..45607dc09 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -1,4 +1,4 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" /> @@ -10,7 +10,6 @@ <ProjectReference Include="..\MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj" /> <ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj" /> <ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" /> - <ProjectReference Include="..\Mono.Nat\Mono.Nat.csproj" /> <ProjectReference Include="..\MediaBrowser.Api\MediaBrowser.Api.csproj" /> <ProjectReference Include="..\MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj" /> <ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" /> @@ -29,9 +28,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.Logging" Version="2.2.0" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.2.0" /> + <PackageReference Include="Microsoft.Extensions.Logging" Version="3.0.0" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.0.0" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.0.0" /> + <PackageReference Include="Mono.Nat" Version="2.0.0" /> <PackageReference Include="ServiceStack.Text.Core" Version="5.6.0" /> <PackageReference Include="sharpcompress" Version="0.24.0" /> <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.0.1" /> @@ -47,16 +47,12 @@ <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> - <PropertyGroup> - <!-- We need at least C# 7.3 to compare tuples--> - <LangVersion>latest</LangVersion> - </PropertyGroup> - <!-- Code analysers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.4" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> </ItemGroup> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs index f26a70586..08041eb59 100644 --- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Net; +using System.Text; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Plugins; @@ -15,209 +14,135 @@ using Mono.Nat; namespace Emby.Server.Implementations.EntryPoints { + /// <summary> + /// Server entrypoint handling external port forwarding. + /// </summary> public class ExternalPortForwarding : IServerEntryPoint { private readonly IServerApplicationHost _appHost; private readonly ILogger _logger; - private readonly IHttpClient _httpClient; private readonly IServerConfigurationManager _config; private readonly IDeviceDiscovery _deviceDiscovery; + private readonly object _createdRulesLock = new object(); + private List<IPEndPoint> _createdRules = new List<IPEndPoint>(); private Timer _timer; + private string _lastConfigIdentifier; - private NatManager _natManager; + private bool _disposed = false; - public ExternalPortForwarding(ILoggerFactory loggerFactory, IServerApplicationHost appHost, IServerConfigurationManager config, IDeviceDiscovery deviceDiscovery, IHttpClient httpClient) + /// <summary> + /// Initializes a new instance of the <see cref="ExternalPortForwarding"/> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="appHost">The application host.</param> + /// <param name="config">The configuration manager.</param> + /// <param name="deviceDiscovery">The device discovery.</param> + public ExternalPortForwarding( + ILogger<ExternalPortForwarding> logger, + IServerApplicationHost appHost, + IServerConfigurationManager config, + IDeviceDiscovery deviceDiscovery) { - _logger = loggerFactory.CreateLogger("PortMapper"); + _logger = logger; _appHost = appHost; _config = config; _deviceDiscovery = deviceDiscovery; - _httpClient = httpClient; - _config.ConfigurationUpdated += _config_ConfigurationUpdated1; - } - - private void _config_ConfigurationUpdated1(object sender, EventArgs e) - { - _config_ConfigurationUpdated(sender, e); } - private string _lastConfigIdentifier; private string GetConfigIdentifier() { - var values = new List<string>(); + const char Separator = '|'; var config = _config.Configuration; - values.Add(config.EnableUPnP.ToString()); - values.Add(config.PublicPort.ToString(CultureInfo.InvariantCulture)); - values.Add(_appHost.HttpPort.ToString(CultureInfo.InvariantCulture)); - values.Add(_appHost.HttpsPort.ToString(CultureInfo.InvariantCulture)); - values.Add(_appHost.EnableHttps.ToString()); - values.Add((config.EnableRemoteAccess).ToString()); - - return string.Join("|", values.ToArray()); + return new StringBuilder(32) + .Append(config.EnableUPnP).Append(Separator) + .Append(config.PublicPort).Append(Separator) + .Append(_appHost.HttpPort).Append(Separator) + .Append(_appHost.HttpsPort).Append(Separator) + .Append(_appHost.EnableHttps).Append(Separator) + .Append(config.EnableRemoteAccess).Append(Separator) + .ToString(); } - private async void _config_ConfigurationUpdated(object sender, EventArgs e) + private void OnConfigurationUpdated(object sender, EventArgs e) { if (!string.Equals(_lastConfigIdentifier, GetConfigIdentifier(), StringComparison.OrdinalIgnoreCase)) { - DisposeNat(); + Stop(); - await RunAsync(); + Start(); } } + /// <inheritdoc /> public Task RunAsync() { - if (_config.Configuration.EnableUPnP && _config.Configuration.EnableRemoteAccess) - { - Start(); - } + Start(); - _config.ConfigurationUpdated -= _config_ConfigurationUpdated; - _config.ConfigurationUpdated += _config_ConfigurationUpdated; + _config.ConfigurationUpdated += OnConfigurationUpdated; return Task.CompletedTask; } private void Start() { - _logger.LogDebug("Starting NAT discovery"); - if (_natManager == null) + if (!_config.Configuration.EnableUPnP || !_config.Configuration.EnableRemoteAccess) { - _natManager = new NatManager(_logger, _httpClient); - _natManager.DeviceFound += NatUtility_DeviceFound; - _natManager.StartDiscovery(); + return; } + _logger.LogDebug("Starting NAT discovery"); + + NatUtility.DeviceFound += OnNatUtilityDeviceFound; + NatUtility.StartDiscovery(); + _timer = new Timer(ClearCreatedRules, null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10)); - _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; + _deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered; _lastConfigIdentifier = GetConfigIdentifier(); } - private async void _deviceDiscovery_DeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e) + private void Stop() { - if (_disposed) - { - return; - } - - var info = e.Argument; - - if (!info.Headers.TryGetValue("USN", out string usn)) usn = string.Empty; - - if (!info.Headers.TryGetValue("NT", out string nt)) nt = string.Empty; - - // Filter device type - if (usn.IndexOf("WANIPConnection:", StringComparison.OrdinalIgnoreCase) == -1 && - nt.IndexOf("WANIPConnection:", StringComparison.OrdinalIgnoreCase) == -1 && - usn.IndexOf("WANPPPConnection:", StringComparison.OrdinalIgnoreCase) == -1 && - nt.IndexOf("WANPPPConnection:", StringComparison.OrdinalIgnoreCase) == -1) - { - return; - } - - var identifier = string.IsNullOrWhiteSpace(usn) ? nt : usn; - - if (info.Location == null) - { - return; - } - - lock (_usnsHandled) - { - if (_usnsHandled.Contains(identifier)) - { - return; - } - _usnsHandled.Add(identifier); - } - - _logger.LogDebug("Found NAT device: " + identifier); - - if (IPAddress.TryParse(info.Location.Host, out var address)) - { - // The Handle method doesn't need the port - var endpoint = new IPEndPoint(address, info.Location.Port); - - IPAddress localAddress = null; - - try - { - var localAddressString = await _appHost.GetLocalApiUrl(CancellationToken.None).ConfigureAwait(false); + _logger.LogDebug("Stopping NAT discovery"); - if (Uri.TryCreate(localAddressString, UriKind.Absolute, out var uri)) - { - localAddressString = uri.Host; + NatUtility.StopDiscovery(); + NatUtility.DeviceFound -= OnNatUtilityDeviceFound; - if (!IPAddress.TryParse(localAddressString, out localAddress)) - { - return; - } - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error"); - return; - } - - if (_disposed) - { - return; - } + _timer?.Dispose(); - // This should never happen, but the Handle method will throw ArgumentNullException if it does - if (localAddress == null) - { - return; - } - - var natManager = _natManager; - if (natManager != null) - { - await natManager.Handle(localAddress, info, endpoint, NatProtocol.Upnp).ConfigureAwait(false); - } - } + _deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered; } private void ClearCreatedRules(object state) { - lock (_createdRules) + lock (_createdRulesLock) { _createdRules.Clear(); } - lock (_usnsHandled) - { - _usnsHandled.Clear(); - } } - void NatUtility_DeviceFound(object sender, DeviceEventArgs e) + private void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e) { - if (_disposed) - { - return; - } + NatUtility.Search(e.Argument.LocalIpAddress, NatProtocol.Upnp); + } + private void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e) + { try { var device = e.Device; CreateRules(device); } - catch + catch (Exception ex) { - // Commenting out because users are reporting problems out of our control - //_logger.LogError(ex, "Error creating port forwarding rules"); + _logger.LogError(ex, "Error creating port forwarding rules"); } } - private List<string> _createdRules = new List<string>(); - private List<string> _usnsHandled = new List<string>(); private async void CreateRules(INatDevice device) { if (_disposed) @@ -227,15 +152,13 @@ namespace Emby.Server.Implementations.EntryPoints // On some systems the device discovered event seems to fire repeatedly // This check will help ensure we're not trying to port map the same device over and over - var address = device.LocalAddress; - - var addressString = address.ToString(); + var address = device.DeviceEndpoint; - lock (_createdRules) + lock (_createdRulesLock) { - if (!_createdRules.Contains(addressString)) + if (!_createdRules.Contains(address)) { - _createdRules.Add(addressString); + _createdRules.Add(address); } else { @@ -263,54 +186,43 @@ namespace Emby.Server.Implementations.EntryPoints } } - private Task CreatePortMap(INatDevice device, int privatePort, int publicPort) + private Task<Mapping> CreatePortMap(INatDevice device, int privatePort, int publicPort) { - _logger.LogDebug("Creating port map on local port {0} to public port {1} with device {2}", privatePort, publicPort, device.LocalAddress.ToString()); - - return device.CreatePortMap(new Mapping(Protocol.Tcp, privatePort, publicPort) - { - Description = _appHost.Name - }); + _logger.LogDebug( + "Creating port map on local port {0} to public port {1} with device {2}", + privatePort, + publicPort, + device.DeviceEndpoint); + + return device.CreatePortMapAsync( + new Mapping(Protocol.Tcp, privatePort, publicPort, 0, _appHost.Name)); } - private bool _disposed = false; + /// <inheritdoc /> public void Dispose() { - _disposed = true; - DisposeNat(); + Dispose(true); + GC.SuppressFinalize(this); } - private void DisposeNat() + /// <summary> + /// Releases unmanaged and - optionally - managed resources. + /// </summary> + /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool dispose) { - _logger.LogDebug("Stopping NAT discovery"); - - if (_timer != null) + if (_disposed) { - _timer.Dispose(); - _timer = null; + return; } - _deviceDiscovery.DeviceDiscovered -= _deviceDiscovery_DeviceDiscovered; + _config.ConfigurationUpdated -= OnConfigurationUpdated; - var natManager = _natManager; + Stop(); - if (natManager != null) - { - _natManager = null; + _timer = null; - using (natManager) - { - try - { - natManager.StopDiscovery(); - natManager.DeviceFound -= NatUtility_DeviceFound; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error stopping NAT Discovery"); - } - } - } + _disposed = true; } } } diff --git a/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs b/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs index b2328121e..3a7516dca 100644 --- a/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs +++ b/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs @@ -54,7 +54,7 @@ namespace Emby.Server.Implementations.EntryPoints { cancellationToken.ThrowIfCancellationRequested(); - await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), cancellationToken).ConfigureAwait(false); + await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false); } } diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index e4f98acb9..cd2a7dcf0 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -539,6 +539,11 @@ namespace Emby.Server.Implementations.HttpServer } finally { + if (httpRes.StatusCode >= 500) + { + _logger.LogDebug("Sending HTTP Response 500 in response to {Url}", urlToLog); + } + stopWatch.Stop(); var elapsed = stopWatch.Elapsed; if (elapsed.TotalMilliseconds > 500) diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index c95b00ede..85110c21c 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -2,11 +2,11 @@ using System; using System.Linq; using System.Text; using System.Threading.Tasks; +using MediaBrowser.Common; using MediaBrowser.Common.Cryptography; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Cryptography; -using static MediaBrowser.Common.HexHelper; namespace Emby.Server.Implementations.Library { @@ -122,7 +122,7 @@ namespace Emby.Server.Implementations.Library { return string.IsNullOrEmpty(user.EasyPassword) ? null - : ToHexString(PasswordHash.Parse(user.EasyPassword).Hash); + : Hex.Encode(PasswordHash.Parse(user.EasyPassword).Hash); } /// <summary> diff --git a/Emby.Server.Implementations/Library/InvalidAuthProvider.cs b/Emby.Server.Implementations/Library/InvalidAuthProvider.cs index 6956369dc..7913df5e4 100644 --- a/Emby.Server.Implementations/Library/InvalidAuthProvider.cs +++ b/Emby.Server.Implementations/Library/InvalidAuthProvider.cs @@ -1,7 +1,6 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Net; namespace Emby.Server.Implementations.Library { diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index fc73fc649..528636ecd 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -519,7 +519,7 @@ namespace Emby.Server.Implementations.Library } public BaseItem ResolvePath(FileSystemMetadata fileInfo, Folder parent = null) - => ResolvePath(fileInfo, new DirectoryService(_logger, _fileSystem), null, parent); + => ResolvePath(fileInfo, new DirectoryService(_fileSystem), null, parent); private BaseItem ResolvePath( FileSystemMetadata fileInfo, @@ -1045,7 +1045,7 @@ namespace Emby.Server.Implementations.Library await RootFolder.ValidateChildren( new SimpleProgress<double>(), cancellationToken, - new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), + new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: false).ConfigureAwait(false); await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false); @@ -1053,7 +1053,7 @@ namespace Emby.Server.Implementations.Library await GetUserRootFolder().ValidateChildren( new SimpleProgress<double>(), cancellationToken, - new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), + new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: false).ConfigureAwait(false); // Quickly scan CollectionFolders for changes @@ -1074,7 +1074,7 @@ namespace Emby.Server.Implementations.Library innerProgress.RegisterAction(pct => progress.Report(pct * .96)); // Now validate the entire media library - await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), recursive: true).ConfigureAwait(false); + await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true).ConfigureAwait(false); progress.Report(96); @@ -1899,7 +1899,7 @@ namespace Emby.Server.Implementations.Library /// <param name="cancellationToken">The cancellation token.</param> public void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) { - UpdateItems(new [] { item }, parent, updateReason, cancellationToken); + UpdateItems(new[] { item }, parent, updateReason, cancellationToken); } /// <summary> @@ -2135,7 +2135,7 @@ namespace Emby.Server.Implementations.Library if (refresh) { item.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None); - _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), RefreshPriority.Normal); + _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal); } return item; @@ -2192,7 +2192,7 @@ namespace Emby.Server.Implementations.Library { _providerManagerFactory().QueueRefresh( item.Id, - new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { // Need to force save to increment DateLastSaved ForceSave = true @@ -2259,7 +2259,7 @@ namespace Emby.Server.Implementations.Library { _providerManagerFactory().QueueRefresh( item.Id, - new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { // Need to force save to increment DateLastSaved ForceSave = true @@ -2336,7 +2336,7 @@ namespace Emby.Server.Implementations.Library { _providerManagerFactory().QueueRefresh( item.Id, - new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { // Need to force save to increment DateLastSaved ForceSave = true @@ -2485,6 +2485,15 @@ namespace Emby.Server.Implementations.Library { episode.ParentIndexNumber = season.IndexNumber; } + else + { + /* + Anime series don't generally have a season in their file name, however, + tvdb needs a season to correctly get the metadata. + Hence, a null season needs to be filled with something. */ + //FIXME perhaps this would be better for tvdb parser to ask for season 1 if no season is specified + episode.ParentIndexNumber = 1; + } if (episode.ParentIndexNumber.HasValue) { diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index d83e1fc02..7a26e0c37 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -134,12 +134,13 @@ namespace Emby.Server.Implementations.Library if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio || i.Type == MediaStreamType.Video)) { - await item.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) - { - EnableRemoteContentProbe = true, - MetadataRefreshMode = MediaBrowser.Controller.Providers.MetadataRefreshMode.FullRefresh - - }, cancellationToken).ConfigureAwait(false); + await item.RefreshMetadata( + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + EnableRemoteContentProbe = true, + MetadataRefreshMode = MetadataRefreshMode.FullRefresh + }, + cancellationToken).ConfigureAwait(false); mediaSources = GetStaticMediaSources(item, enablePathSubstitution, user); } diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 52b2f56ff..60d16c8a0 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -8,6 +8,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common; using MediaBrowser.Common.Cryptography; using MediaBrowser.Common.Events; using MediaBrowser.Common.Net; @@ -31,7 +32,6 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Users; using Microsoft.Extensions.Logging; -using static MediaBrowser.Common.HexHelper; namespace Emby.Server.Implementations.Library { @@ -490,7 +490,7 @@ namespace Emby.Server.Implementations.Library { return string.IsNullOrEmpty(user.EasyPassword) ? null - : ToHexString(PasswordHash.Parse(user.EasyPassword).Hash); + : Hex.Encode(PasswordHash.Parse(user.EasyPassword).Hash); } private void ResetInvalidLoginAttemptCount(User user) @@ -639,7 +639,7 @@ namespace Emby.Server.Implementations.Library { foreach (var user in Users) { - await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), cancellationToken).ConfigureAwait(false); + await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false); } } diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs index d00c6cde1..137a010ec 100644 --- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs @@ -11,16 +11,17 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Library.Validators { /// <summary> - /// Class PeopleValidator + /// Class PeopleValidator. /// </summary> public class PeopleValidator { /// <summary> - /// The _library manager + /// The _library manager. /// </summary> private readonly ILibraryManager _libraryManager; + /// <summary> - /// The _logger + /// The _logger. /// </summary> private readonly ILogger _logger; @@ -62,7 +63,7 @@ namespace Emby.Server.Implementations.Library.Validators { var item = _libraryManager.GetPerson(person); - var options = new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) + var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ImageRefreshMode = MetadataRefreshMode.ValidationOnly, MetadataRefreshMode = MetadataRefreshMode.ValidationOnly @@ -96,12 +97,19 @@ namespace Emby.Server.Implementations.Library.Validators foreach (var item in deadEntities) { - _logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name); - - _libraryManager.DeleteItem(item, new DeleteOptions - { - DeleteFileLocation = false - }, false); + _logger.LogInformation( + "Deleting dead {2} {0} {1}.", + item.Id.ToString("N", CultureInfo.InvariantCulture), + item.Name, + item.GetType().Name); + + _libraryManager.DeleteItem( + item, + new DeleteOptions + { + DeleteFileLocation = false + }, + false); } progress.Report(100); diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index da0013f12..687a178a6 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -1489,16 +1489,18 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { _logger.LogInformation("Refreshing recording parent {path}", item.Path); - _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) - { - RefreshPaths = new string[] + _providerManager.QueueRefresh( + item.Id, + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { - path, - Path.GetDirectoryName(path), - Path.GetDirectoryName(Path.GetDirectoryName(path)) - } - - }, RefreshPriority.High); + RefreshPaths = new string[] + { + path, + Path.GetDirectoryName(path), + Path.GetDirectoryName(Path.GetDirectoryName(path)) + } + }, + RefreshPriority.High); } } diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index 89b92c999..d4bd598e3 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -1226,12 +1226,13 @@ namespace Emby.Server.Implementations.LiveTv currentChannel.AddTag("Kids"); } - //currentChannel.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken); - await currentChannel.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) - { - ForceSave = true - - }, cancellationToken).ConfigureAwait(false); + currentChannel.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken); + await currentChannel.RefreshMetadata( + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + ForceSave = true + }, + cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -1245,7 +1246,7 @@ namespace Emby.Server.Implementations.LiveTv numComplete++; double percent = numComplete / (double)allChannelsList.Count; - progress.Report(85 * percent + 15); + progress.Report((85 * percent) + 15); } progress.Report(100); @@ -1278,12 +1279,14 @@ namespace Emby.Server.Implementations.LiveTv if (item != null) { - _libraryManager.DeleteItem(item, new DeleteOptions - { - DeleteFileLocation = false, - DeleteFromExternalProvider = false - - }, false); + _libraryManager.DeleteItem( + item, + new DeleteOptions + { + DeleteFileLocation = false, + DeleteFromExternalProvider = false + }, + false); } } @@ -2301,8 +2304,10 @@ namespace Emby.Server.Implementations.LiveTv if (provider == null) { throw new ResourceNotFoundException( - string.Format("Couldn't find provider of type: '{0}'", info.Type) - ); + string.Format( + CultureInfo.InvariantCulture, + "Couldn't find provider of type: '{0}'", + info.Type)); } await provider.Validate(info, validateLogin, validateListings).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index c19148921..86fbac380 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -23,7 +23,7 @@ "HeaderFavoriteEpisodes": "Oblíbené epizody", "HeaderFavoriteShows": "Oblíbené seriály", "HeaderFavoriteSongs": "Oblíbená hudba", - "HeaderLiveTV": "Live TV", + "HeaderLiveTV": "Živá TV", "HeaderNextUp": "Nadcházející", "HeaderRecordingGroups": "Skupiny nahrávek", "HomeVideos": "Domáci videa", diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index a2fc126a2..3a6852321 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -5,7 +5,7 @@ "Artists": "Előadók", "AuthenticationSucceededWithUserName": "{0} sikeresen azonosítva", "Books": "Könyvek", - "CameraImageUploadedFrom": "Új kamerakép került feltöltésre {0}", + "CameraImageUploadedFrom": "Új kamerakép került feltöltésre innen: {0}", "Channels": "Csatornák", "ChapterNameValue": "Jelenet {0}", "Collections": "Gyűjtemények", @@ -15,14 +15,14 @@ "Favorites": "Kedvencek", "Folders": "Könyvtárak", "Genres": "Műfajok", - "HeaderAlbumArtists": "Album Előadók", + "HeaderAlbumArtists": "Album előadók", "HeaderCameraUploads": "Kamera feltöltések", "HeaderContinueWatching": "Folyamatban lévő filmek", - "HeaderFavoriteAlbums": "Kedvenc Albumok", - "HeaderFavoriteArtists": "Kedvenc Előadók", - "HeaderFavoriteEpisodes": "Kedvenc Epizódok", - "HeaderFavoriteShows": "Kedvenc Sorozatok", - "HeaderFavoriteSongs": "Kedvenc Dalok", + "HeaderFavoriteAlbums": "Kedvenc albumok", + "HeaderFavoriteArtists": "Kedvenc előadók", + "HeaderFavoriteEpisodes": "Kedvenc epizódok", + "HeaderFavoriteShows": "Kedvenc sorozatok", + "HeaderFavoriteSongs": "Kedvenc dalok", "HeaderLiveTV": "Élő TV", "HeaderNextUp": "Következik", "HeaderRecordingGroups": "Felvételi csoportok", @@ -34,21 +34,21 @@ "LabelRunningTimeValue": "Futási idő: {0}", "Latest": "Legújabb", "MessageApplicationUpdated": "Jellyfin Szerver frissítve", - "MessageApplicationUpdatedTo": "Jellyfin Szerver frissítve lett a következőre {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Szerver konfigurációs rész {0} frissítve", + "MessageApplicationUpdatedTo": "Jellyfin Szerver frissítve lett a következőre: {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "Szerver konfigurációs rész frissítve: {0}", "MessageServerConfigurationUpdated": "Szerver konfiguráció frissítve", "MixedContent": "Vegyes tartalom", "Movies": "Filmek", "Music": "Zene", - "MusicVideos": "Zenei Videók", + "MusicVideos": "Zenei videók", "NameInstallFailed": "{0} sikertelen telepítés", "NameSeasonNumber": "Évad {0}", "NameSeasonUnknown": "Ismeretlen évad", "NewVersionIsAvailable": "Letölthető a Jellyfin Szerver új verziója.", - "NotificationOptionApplicationUpdateAvailable": "Új programfrissítés érhető el", - "NotificationOptionApplicationUpdateInstalled": "Programfrissítés telepítve", + "NotificationOptionApplicationUpdateAvailable": "Frissítés érhető el az alkalmazáshoz", + "NotificationOptionApplicationUpdateInstalled": "Alkalmazásfrissítés telepítve", "NotificationOptionAudioPlayback": "Audió lejátszás elkezdve", - "NotificationOptionAudioPlaybackStopped": "Audió lejátszás befejezve", + "NotificationOptionAudioPlaybackStopped": "Audió lejátszás leállítva", "NotificationOptionCameraImageUploaded": "Kamera kép feltöltve", "NotificationOptionInstallationFailed": "Telepítési hiba", "NotificationOptionNewLibraryContent": "Új tartalom hozzáadva", @@ -60,15 +60,15 @@ "NotificationOptionTaskFailed": "Ütemezett feladat hiba", "NotificationOptionUserLockedOut": "Felhasználó tiltva", "NotificationOptionVideoPlayback": "Videó lejátszás elkezdve", - "NotificationOptionVideoPlaybackStopped": "Videó lejátszás befejezve", + "NotificationOptionVideoPlaybackStopped": "Videó lejátszás leállítva", "Photos": "Fényképek", "Playlists": "Lejátszási listák", "Plugin": "Bővítmény", "PluginInstalledWithName": "{0} telepítve", "PluginUninstalledWithName": "{0} eltávolítva", "PluginUpdatedWithName": "{0} frissítve", - "ProviderValue": "Provider: {0}", - "ScheduledTaskFailedWithName": "{0} hiba", + "ProviderValue": "Szolgáltató: {0}", + "ScheduledTaskFailedWithName": "{0} sikertelen", "ScheduledTaskStartedWithName": "{0} elkezdve", "ServerNameNeedsToBeRestarted": "{0}-t újra kell indítani", "Shows": "Műsorok", @@ -76,10 +76,10 @@ "StartupEmbyServerIsLoading": "A Jellyfin Szerver betöltődik. Kérlek próbáld újra később.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0} ehhez: {1}", - "SubtitlesDownloadedForItem": "Letöltött feliratok a következőhöz {0}", + "SubtitlesDownloadedForItem": "Letöltött feliratok a következőhöz: {0}", "Sync": "Szinkronizál", "System": "Rendszer", - "TvShows": "TV Műsorok", + "TvShows": "TV műsorok", "User": "Felhasználó", "UserCreatedWithName": "{0} felhasználó létrehozva", "UserDeletedWithName": "{0} felhasználó törölve", @@ -88,7 +88,7 @@ "UserOfflineFromDevice": "{0} kijelentkezett innen: {1}", "UserOnlineFromDevice": "{0} online itt: {1}", "UserPasswordChangedWithName": "Jelszó megváltozott a következő felhasználó számára: {0}", - "UserPolicyUpdatedWithName": "A felhasználói házirend frissítve lett {0}", + "UserPolicyUpdatedWithName": "A felhasználói házirend frissítve lett neki: {0}", "UserStartedPlayingItemWithValues": "{0} elkezdte játszani a következőt: {1} itt: {2}", "UserStoppedPlayingItemWithValues": "{0} befejezte a következőt: {1} itt: {2}", "ValueHasBeenAddedToLibrary": "{0} hozzáadva a médiatárhoz", diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 24af1839f..637e514ed 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -1,7 +1,7 @@ { "Albums": "Albums", "AppDeviceValues": "App: {0}, Apparaat: {1}", - "Application": "Toepassing", + "Application": "Applicatie", "Artists": "Artiesten", "AuthenticationSucceededWithUserName": "{0} is succesvol geverifieerd", "Books": "Boeken", @@ -30,7 +30,7 @@ "Inherit": "Overerven", "ItemAddedWithName": "{0} is toegevoegd aan de bibliotheek", "ItemRemovedWithName": "{0} is verwijderd uit de bibliotheek", - "LabelIpAddressValue": "IP adres: {0}", + "LabelIpAddressValue": "IP-adres: {0}", "LabelRunningTimeValue": "Looptijd: {0}", "Latest": "Nieuwste", "MessageApplicationUpdated": "Jellyfin Server is bijgewerkt", @@ -50,7 +50,7 @@ "NotificationOptionAudioPlayback": "Muziek gestart", "NotificationOptionAudioPlaybackStopped": "Muziek gestopt", "NotificationOptionCameraImageUploaded": "Camera-afbeelding geüpload", - "NotificationOptionInstallationFailed": "Installatie mislukt", + "NotificationOptionInstallationFailed": "Installatie mislukking", "NotificationOptionNewLibraryContent": "Nieuwe content toegevoegd", "NotificationOptionPluginError": "Plug-in fout", "NotificationOptionPluginInstalled": "Plug-in geïnstalleerd", diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index ba5e93982..87f8553ae 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -5,7 +5,7 @@ "Artists": "艺术家", "AuthenticationSucceededWithUserName": "{0} 认证成功", "Books": "书籍", - "CameraImageUploadedFrom": "已从 {0} 上传了一张新的相机图像", + "CameraImageUploadedFrom": "已从 {0} 上传了一张新的相片", "Channels": "频道", "ChapterNameValue": "章节 {0}", "Collections": "合集", diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs index 7d85a0666..d948dad68 100644 --- a/Emby.Server.Implementations/Networking/NetworkManager.cs +++ b/Emby.Server.Implementations/Networking/NetworkManager.cs @@ -20,6 +20,9 @@ namespace Emby.Server.Implementations.Networking private IPAddress[] _localIpAddresses; private readonly object _localIpAddressSyncLock = new object(); + private readonly object _subnetLookupLock = new object(); + private Dictionary<string, List<string>> _subnetLookup = new Dictionary<string, List<string>>(StringComparer.Ordinal); + public NetworkManager(ILogger<NetworkManager> logger) { _logger = logger; @@ -28,10 +31,10 @@ namespace Emby.Server.Implementations.Networking NetworkChange.NetworkAvailabilityChanged += OnNetworkAvailabilityChanged; } - public Func<string[]> LocalSubnetsFn { get; set; } - public event EventHandler NetworkChanged; + public Func<string[]> LocalSubnetsFn { get; set; } + private void OnNetworkAvailabilityChanged(object sender, NetworkAvailabilityEventArgs e) { _logger.LogDebug("NetworkAvailabilityChanged"); @@ -179,10 +182,9 @@ namespace Emby.Server.Implementations.Networking return false; } - private Dictionary<string, List<string>> _subnetLookup = new Dictionary<string, List<string>>(StringComparer.Ordinal); private List<string> GetSubnets(string endpointFirstPart) { - lock (_subnetLookup) + lock (_subnetLookupLock) { if (_subnetLookup.TryGetValue(endpointFirstPart, out var subnets)) { @@ -200,7 +202,11 @@ namespace Emby.Server.Implementations.Networking int subnet_Test = 0; foreach (string part in unicastIPAddressInformation.IPv4Mask.ToString().Split('.')) { - if (part.Equals("0")) break; + if (part.Equals("0", StringComparison.Ordinal)) + { + break; + } + subnet_Test++; } diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 40b568b40..0f58e43ed 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -90,8 +90,7 @@ namespace Emby.Server.Implementations.Playlists } else { - var folder = item as Folder; - if (folder != null) + if (item is Folder folder) { options.MediaType = folder.GetRecursiveChildren(i => !i.IsFolder && i.SupportsAddingToPlaylist) .Select(i => i.MediaType) @@ -140,7 +139,7 @@ namespace Emby.Server.Implementations.Playlists parentFolder.AddChild(playlist, CancellationToken.None); - await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) { ForceSave = true }, CancellationToken.None) + await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, CancellationToken.None) .ConfigureAwait(false); if (options.ItemIdList.Length > 0) @@ -201,7 +200,7 @@ namespace Emby.Server.Implementations.Playlists var list = new List<LinkedChild>(); - var items = (GetPlaylistItems(itemIds, playlist.MediaType, user, options)) + var items = GetPlaylistItems(itemIds, playlist.MediaType, user, options) .Where(i => i.SupportsAddingToPlaylist) .ToList(); @@ -221,18 +220,18 @@ namespace Emby.Server.Implementations.Playlists SavePlaylistFile(playlist); } - _providerManager.QueueRefresh(playlist.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) - { - ForceSave = true - - }, RefreshPriority.High); + _providerManager.QueueRefresh( + playlist.Id, + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + ForceSave = true + }, + RefreshPriority.High); } public void RemoveFromPlaylist(string playlistId, IEnumerable<string> entryIds) { - var playlist = _libraryManager.GetItemById(playlistId) as Playlist; - - if (playlist == null) + if (!(_libraryManager.GetItemById(playlistId) is Playlist playlist)) { throw new ArgumentException("No Playlist exists with the supplied Id"); } @@ -254,7 +253,7 @@ namespace Emby.Server.Implementations.Playlists SavePlaylistFile(playlist); } - _providerManager.QueueRefresh(playlist.Id, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) + _providerManager.QueueRefresh(playlist.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true @@ -263,9 +262,7 @@ namespace Emby.Server.Implementations.Playlists public void MoveItem(string playlistId, string entryId, int newIndex) { - var playlist = _libraryManager.GetItemById(playlistId) as Playlist; - - if (playlist == null) + if (!(_libraryManager.GetItemById(playlistId) is Playlist playlist)) { throw new ArgumentException("No Playlist exists with the supplied Id"); } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index 2f07ff15a..ecd526251 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -19,16 +19,17 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.ScheduledTasks { /// <summary> - /// Class ChapterImagesTask + /// Class ChapterImagesTask. /// </summary> public class ChapterImagesTask : IScheduledTask { /// <summary> - /// The _logger + /// The _logger. /// </summary> private readonly ILogger _logger; + /// <summary> - /// The _library manager + /// The _library manager. /// </summary> private readonly ILibraryManager _libraryManager; @@ -53,12 +54,12 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// <summary> - /// Creates the triggers that define when the task will run + /// Creates the triggers that define when the task will run. /// </summary> public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() { - return new[] { - + return new[] + { new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerDaily, @@ -117,7 +118,7 @@ namespace Emby.Server.Implementations.ScheduledTasks previouslyFailedImages = new List<string>(); } - var directoryService = new DirectoryService(_logger, _fileSystem); + var directoryService = new DirectoryService(_fileSystem); foreach (var video in videos) { diff --git a/Emby.Server.Implementations/Serialization/XmlSerializer.cs b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs index 6400ec16e..296822981 100644 --- a/Emby.Server.Implementations/Serialization/XmlSerializer.cs +++ b/Emby.Server.Implementations/Serialization/MyXmlSerializer.cs @@ -1,11 +1,9 @@ using System; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.IO; using System.Xml; using System.Xml.Serialization; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; -using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Serialization { @@ -14,35 +12,13 @@ namespace Emby.Server.Implementations.Serialization /// </summary> public class MyXmlSerializer : IXmlSerializer { - private readonly IFileSystem _fileSystem; - private readonly ILogger _logger; - - public MyXmlSerializer( - IFileSystem fileSystem, - ILoggerFactory loggerFactory) - { - _fileSystem = fileSystem; - _logger = loggerFactory.CreateLogger("XmlSerializer"); - } - // Need to cache these // http://dotnetcodebox.blogspot.com/2013/01/xmlserializer-class-may-result-in.html - private readonly Dictionary<string, XmlSerializer> _serializers = - new Dictionary<string, XmlSerializer>(); + private static readonly ConcurrentDictionary<string, XmlSerializer> _serializers = + new ConcurrentDictionary<string, XmlSerializer>(); - private XmlSerializer GetSerializer(Type type) - { - var key = type.FullName; - lock (_serializers) - { - if (!_serializers.TryGetValue(key, out var serializer)) - { - serializer = new XmlSerializer(type); - _serializers[key] = serializer; - } - return serializer; - } - } + private static XmlSerializer GetSerializer(Type type) + => _serializers.GetOrAdd(type.FullName, _ => new XmlSerializer(type)); /// <summary> /// Serializes to writer. @@ -91,7 +67,6 @@ namespace Emby.Server.Implementations.Serialization /// <param name="file">The file.</param> public void SerializeToFile(object obj, string file) { - _logger.LogDebug("Serializing to file {0}", file); using (var stream = new FileStream(file, FileMode.Create)) { SerializeToStream(obj, stream); @@ -106,7 +81,6 @@ namespace Emby.Server.Implementations.Serialization /// <returns>System.Object.</returns> public object DeserializeFromFile(Type type, string file) { - _logger.LogDebug("Deserializing file {0}", file); using (var stream = File.OpenRead(file)) { return DeserializeFromStream(type, stream); diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs index 2f5a8af80..46195cc74 100644 --- a/Emby.Server.Implementations/ServerApplicationPaths.cs +++ b/Emby.Server.Implementations/ServerApplicationPaths.cs @@ -6,7 +6,7 @@ using MediaBrowser.Controller; namespace Emby.Server.Implementations { /// <summary> - /// Extends BaseApplicationPaths to add paths that are only applicable on the server + /// Extends BaseApplicationPaths to add paths that are only applicable on the server. /// </summary> public class ServerApplicationPaths : BaseApplicationPaths, IServerApplicationPaths { diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 0c0c77cda..2de20829c 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Net.Http; @@ -19,7 +18,6 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Updates; using Microsoft.Extensions.Logging; -using static MediaBrowser.Common.HexHelper; namespace Emby.Server.Implementations.Updates { @@ -28,43 +26,10 @@ namespace Emby.Server.Implementations.Updates /// </summary> public class InstallationManager : IInstallationManager { - public event EventHandler<InstallationEventArgs> PackageInstalling; - public event EventHandler<InstallationEventArgs> PackageInstallationCompleted; - public event EventHandler<InstallationFailedEventArgs> PackageInstallationFailed; - public event EventHandler<InstallationEventArgs> PackageInstallationCancelled; - - /// <summary> - /// The current installations - /// </summary> - private List<(InstallationInfo info, CancellationTokenSource token)> _currentInstallations { get; set; } - /// <summary> - /// The completed installations - /// </summary> - private ConcurrentBag<InstallationInfo> _completedInstallationsInternal; - - public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal; - - /// <summary> - /// Occurs when [plugin uninstalled]. - /// </summary> - public event EventHandler<GenericEventArgs<IPlugin>> PluginUninstalled; - - /// <summary> - /// Occurs when [plugin updated]. - /// </summary> - public event EventHandler<GenericEventArgs<(IPlugin, PackageVersionInfo)>> PluginUpdated; - - /// <summary> - /// Occurs when [plugin updated]. - /// </summary> - public event EventHandler<GenericEventArgs<PackageVersionInfo>> PluginInstalled; - - /// <summary> - /// The _logger + /// The _logger. /// </summary> private readonly ILogger _logger; - private readonly IApplicationPaths _appPaths; private readonly IHttpClient _httpClient; private readonly IJsonSerializer _jsonSerializer; @@ -79,6 +44,18 @@ namespace Emby.Server.Implementations.Updates private readonly IZipClient _zipClient; + private readonly object _currentInstallationsLock = new object(); + + /// <summary> + /// The current installations. + /// </summary> + private List<(InstallationInfo info, CancellationTokenSource token)> _currentInstallations; + + /// <summary> + /// The completed installations. + /// </summary> + private ConcurrentBag<InstallationInfo> _completedInstallationsInternal; + public InstallationManager( ILogger<InstallationManager> logger, IApplicationHost appHost, @@ -107,6 +84,31 @@ namespace Emby.Server.Implementations.Updates _zipClient = zipClient; } + public event EventHandler<InstallationEventArgs> PackageInstalling; + + public event EventHandler<InstallationEventArgs> PackageInstallationCompleted; + + public event EventHandler<InstallationFailedEventArgs> PackageInstallationFailed; + + public event EventHandler<InstallationEventArgs> PackageInstallationCancelled; + + /// <summary> + /// Occurs when a plugin is uninstalled. + /// </summary> + public event EventHandler<GenericEventArgs<IPlugin>> PluginUninstalled; + + /// <summary> + /// Occurs when a plugin plugin is updated. + /// </summary> + public event EventHandler<GenericEventArgs<(IPlugin, PackageVersionInfo)>> PluginUpdated; + + /// <summary> + /// Occurs when a plugin plugin is installed. + /// </summary> + public event EventHandler<GenericEventArgs<PackageVersionInfo>> PluginInstalled; + + public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal; + /// <summary> /// Gets all available packages. /// </summary> @@ -330,7 +332,7 @@ namespace Emby.Server.Implementations.Updates var tuple = (installationInfo, innerCancellationTokenSource); // Add it to the in-progress list - lock (_currentInstallations) + lock (_currentInstallationsLock) { _currentInstallations.Add(tuple); } @@ -349,7 +351,7 @@ namespace Emby.Server.Implementations.Updates { await InstallPackageInternal(package, linkedToken).ConfigureAwait(false); - lock (_currentInstallations) + lock (_currentInstallationsLock) { _currentInstallations.Remove(tuple); } @@ -360,7 +362,7 @@ namespace Emby.Server.Implementations.Updates } catch (OperationCanceledException) { - lock (_currentInstallations) + lock (_currentInstallationsLock) { _currentInstallations.Remove(tuple); } @@ -375,7 +377,7 @@ namespace Emby.Server.Implementations.Updates { _logger.LogError(ex, "Package installation failed"); - lock (_currentInstallations) + lock (_currentInstallationsLock) { _currentInstallations.Remove(tuple); } @@ -455,7 +457,7 @@ namespace Emby.Server.Implementations.Updates { cancellationToken.ThrowIfCancellationRequested(); - var hash = ToHexString(md5.ComputeHash(stream)); + var hash = Hex.Encode(md5.ComputeHash(stream)); if (!string.Equals(package.checksum, hash, StringComparison.OrdinalIgnoreCase)) { _logger.LogError( @@ -535,7 +537,7 @@ namespace Emby.Server.Implementations.Updates /// <inheritdoc/> public bool CancelInstallation(Guid id) { - lock (_currentInstallations) + lock (_currentInstallationsLock) { var install = _currentInstallations.Find(x => x.Item1.Id == id); if (install == default((InstallationInfo, CancellationTokenSource))) @@ -563,7 +565,7 @@ namespace Emby.Server.Implementations.Updates { if (dispose) { - lock (_currentInstallations) + lock (_currentInstallationsLock) { foreach (var tuple in _currentInstallations) { diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index 396bdd4b7..988ac364a 100644 --- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index efdb75f98..4238d7fe3 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -9,8 +9,6 @@ </PropertyGroup> <PropertyGroup> - <!-- We need at least C# 7.1 for async main--> - <LangVersion>latest</LangVersion> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> @@ -25,8 +23,9 @@ <!-- Code analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.4" /> - <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> </ItemGroup> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> @@ -35,8 +34,8 @@ <ItemGroup> <PackageReference Include="CommandLineParser" Version="2.6.0" /> - <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.2.4" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0" /> + <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.0.0" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.0.0" /> <PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" /> <PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" /> diff --git a/MediaBrowser.Api/IHasItemFields.cs b/MediaBrowser.Api/IHasItemFields.cs index 8598ea262..85b4a7e2d 100644 --- a/MediaBrowser.Api/IHasItemFields.cs +++ b/MediaBrowser.Api/IHasItemFields.cs @@ -32,7 +32,7 @@ namespace MediaBrowser.Api if (string.IsNullOrEmpty(val)) { - return new ItemFields[] { }; + return Array.Empty<ItemFields>(); } return val.Split(',').Select(v => @@ -41,6 +41,7 @@ namespace MediaBrowser.Api { return (ItemFields?)value; } + return null; }).Where(i => i.HasValue).Select(i => i.Value).ToArray(); diff --git a/MediaBrowser.Api/ItemLookupService.cs b/MediaBrowser.Api/ItemLookupService.cs index f3ea7907f..084b20bc1 100644 --- a/MediaBrowser.Api/ItemLookupService.cs +++ b/MediaBrowser.Api/ItemLookupService.cs @@ -227,15 +227,17 @@ namespace MediaBrowser.Api //item.ProductionYear = request.ProductionYear; //item.Name = request.Name; - return _providerManager.RefreshFullItem(item, new MetadataRefreshOptions(new DirectoryService(Logger, _fileSystem)) - { - MetadataRefreshMode = MetadataRefreshMode.FullRefresh, - ImageRefreshMode = MetadataRefreshMode.FullRefresh, - ReplaceAllMetadata = true, - ReplaceAllImages = request.ReplaceAllImages, - SearchResult = request - - }, CancellationToken.None); + return _providerManager.RefreshFullItem( + item, + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + MetadataRefreshMode = MetadataRefreshMode.FullRefresh, + ImageRefreshMode = MetadataRefreshMode.FullRefresh, + ReplaceAllMetadata = true, + ReplaceAllImages = request.ReplaceAllImages, + SearchResult = request + }, + CancellationToken.None); } /// <summary> @@ -294,11 +296,9 @@ namespace MediaBrowser.Api Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); using (var stream = result.Content) + using (var filestream = _fileSystem.GetFileStream(fullCachePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) { - using (var filestream = _fileSystem.GetFileStream(fullCachePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) - { - await stream.CopyToAsync(filestream).ConfigureAwait(false); - } + await stream.CopyToAsync(filestream).ConfigureAwait(false); } Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath)); @@ -311,9 +311,6 @@ namespace MediaBrowser.Api /// <param name="filename">The filename.</param> /// <returns>System.String.</returns> private string GetFullCachePath(string filename) - { - return Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename); - } - + => Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename); } } diff --git a/MediaBrowser.Api/ItemRefreshService.cs b/MediaBrowser.Api/ItemRefreshService.cs index 8238ad19c..a1d69cd2b 100644 --- a/MediaBrowser.Api/ItemRefreshService.cs +++ b/MediaBrowser.Api/ItemRefreshService.cs @@ -63,7 +63,7 @@ namespace MediaBrowser.Api private MetadataRefreshOptions GetRefreshOptions(RefreshItem request) { - return new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)) + return new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { MetadataRefreshMode = request.MetadataRefreshMode, ImageRefreshMode = request.ImageRefreshMode, diff --git a/MediaBrowser.Api/ItemUpdateService.cs b/MediaBrowser.Api/ItemUpdateService.cs index d6514d62e..5d524b185 100644 --- a/MediaBrowser.Api/ItemUpdateService.cs +++ b/MediaBrowser.Api/ItemUpdateService.cs @@ -225,13 +225,15 @@ namespace MediaBrowser.Api if (displayOrderChanged) { - _providerManager.QueueRefresh(series.Id, new MetadataRefreshOptions(new DirectoryService(Logger, _fileSystem)) - { - MetadataRefreshMode = MetadataRefreshMode.FullRefresh, - ImageRefreshMode = MetadataRefreshMode.FullRefresh, - ReplaceAllMetadata = true - - }, RefreshPriority.High); + _providerManager.QueueRefresh( + series.Id, + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + MetadataRefreshMode = MetadataRefreshMode.FullRefresh, + ImageRefreshMode = MetadataRefreshMode.FullRefresh, + ReplaceAllMetadata = true + }, + RefreshPriority.High); } } diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index b05e8c949..2b9a64e97 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -8,6 +8,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Api.UserLibrary; +using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; @@ -25,7 +26,6 @@ using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Services; using Microsoft.Net.Http.Headers; -using static MediaBrowser.Common.HexHelper; namespace MediaBrowser.Api.LiveTv { @@ -887,8 +887,9 @@ namespace MediaBrowser.Api.LiveTv { // SchedulesDirect requires a SHA1 hash of the user's password // https://github.com/SchedulesDirect/JSON-Service/wiki/API-20141201#obtain-a-token - using (SHA1 sha = SHA1.Create()) { - return ToHexString( + using (SHA1 sha = SHA1.Create()) + { + return Hex.Encode( sha.ComputeHash(Encoding.UTF8.GetBytes(str))); } } diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index f653270a6..0d62cf8c5 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -10,7 +10,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/MediaBrowser.Api/PackageService.cs b/MediaBrowser.Api/PackageService.cs index baa6f7bb9..0f04ab45c 100644 --- a/MediaBrowser.Api/PackageService.cs +++ b/MediaBrowser.Api/PackageService.cs @@ -131,12 +131,6 @@ namespace MediaBrowser.Api /// </summary> /// <param name="request">The request.</param> /// <returns>System.Object.</returns> - /// - /// <summary> - /// Gets the specified request. - /// </summary> - /// <param name="request">The request.</param> - /// <returns>System.Object.</returns> public object Get(GetPackage request) { var packages = _installationManager.GetAvailablePackages(CancellationToken.None, applicationVersion: typeof(PackageService).Assembly.GetName().Version).Result; diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 8c4ccfa22..7bfe0e0ce 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -289,17 +289,22 @@ namespace MediaBrowser.Api.Playback throw; } + Logger.LogDebug("Launched ffmpeg process"); state.TranscodingJob = transcodingJob; // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback _ = new JobLogger(Logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream); // Wait for the file to exist before proceeeding - while (!File.Exists(state.WaitForPath ?? outputPath) && !transcodingJob.HasExited) + var ffmpegTargetFile = state.WaitForPath ?? outputPath; + Logger.LogDebug("Waiting for the creation of {0}", ffmpegTargetFile); + while (!File.Exists(ffmpegTargetFile) && !transcodingJob.HasExited) { await Task.Delay(100, cancellationTokenSource.Token).ConfigureAwait(false); } + Logger.LogDebug("File {0} created or transcoding has finished", ffmpegTargetFile); + if (state.IsInputVideo && transcodingJob.Type == TranscodingJobType.Progressive && !transcodingJob.HasExited) { await Task.Delay(1000, cancellationTokenSource.Token).ConfigureAwait(false); @@ -314,6 +319,7 @@ namespace MediaBrowser.Api.Playback { StartThrottler(state, transcodingJob); } + Logger.LogDebug("StartFfMpeg() finished successfully"); return transcodingJob; } diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index f5f753684..9ecb5fe8c 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -192,6 +192,7 @@ namespace MediaBrowser.Api.Playback.Hls if (File.Exists(segmentPath)) { job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); + Logger.LogDebug("returning {0} [it exists, try 1]", segmentPath); return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false); } @@ -207,6 +208,7 @@ namespace MediaBrowser.Api.Playback.Hls job = ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); transcodingLock.Release(); released = true; + Logger.LogDebug("returning {0} [it exists, try 2]", segmentPath); return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false); } else @@ -243,6 +245,7 @@ namespace MediaBrowser.Api.Playback.Hls request.StartTimeTicks = GetStartPositionTicks(state, requestedIndex); + state.WaitForPath = segmentPath; job = await StartFfMpeg(state, playlistPath, cancellationTokenSource).ConfigureAwait(false); } catch @@ -277,7 +280,7 @@ namespace MediaBrowser.Api.Playback.Hls // await Task.Delay(50, cancellationToken).ConfigureAwait(false); //} - Logger.LogInformation("returning {0}", segmentPath); + Logger.LogDebug("returning {0} [general case]", segmentPath); job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false); } @@ -458,56 +461,68 @@ namespace MediaBrowser.Api.Playback.Hls TranscodingJob transcodingJob, CancellationToken cancellationToken) { - var segmentFileExists = File.Exists(segmentPath); - - // If all transcoding has completed, just return immediately - if (transcodingJob != null && transcodingJob.HasExited && segmentFileExists) + var segmentExists = File.Exists(segmentPath); + if (segmentExists) { - return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false); - } + if (transcodingJob != null && transcodingJob.HasExited) + { + // Transcoding job is over, so assume all existing files are ready + Logger.LogDebug("serving up {0} as transcode is over", segmentPath); + return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false); + } - if (segmentFileExists) - { var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension); // If requested segment is less than transcoding position, we can't transcode backwards, so assume it's ready if (segmentIndex < currentTranscodingIndex) { + Logger.LogDebug("serving up {0} as transcode index {1} is past requested point {2}", segmentPath, currentTranscodingIndex, segmentIndex); return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false); } } - var segmentFilename = Path.GetFileName(segmentPath); - - while (!cancellationToken.IsCancellationRequested) + var nextSegmentPath = GetSegmentPath(state, playlistPath, segmentIndex + 1); + if (transcodingJob != null) { - try + while (!cancellationToken.IsCancellationRequested && !transcodingJob.HasExited) { - var text = File.ReadAllText(playlistPath, Encoding.UTF8); - - // If it appears in the playlist, it's done - if (text.IndexOf(segmentFilename, StringComparison.OrdinalIgnoreCase) != -1) + // To be considered ready, the segment file has to exist AND + // either the transcoding job should be done or next segment should also exist + if (segmentExists) { - if (!segmentFileExists) + if (transcodingJob.HasExited || File.Exists(nextSegmentPath)) { - segmentFileExists = File.Exists(segmentPath); + Logger.LogDebug("serving up {0} as it deemed ready", segmentPath); + return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false); } - if (segmentFileExists) + } + else + { + segmentExists = File.Exists(segmentPath); + if (segmentExists) { - return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false); + continue; // avoid unnecessary waiting if segment just became available } - //break; } + + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + } + + if (!File.Exists(segmentPath)) + { + Logger.LogWarning("cannot serve {0} as transcoding quit before we got there", segmentPath); } - catch (IOException) + else { - // May get an error if the file is locked + Logger.LogDebug("serving {0} as it's on disk and transcoding stopped", segmentPath); } - - await Task.Delay(100, cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + } + else + { + Logger.LogWarning("cannot serve {0} as it doesn't exist and no transcode is running", segmentPath); } - cancellationToken.ThrowIfCancellationRequested(); return await GetSegmentResult(state, segmentPath, segmentIndex, transcodingJob).ConfigureAwait(false); } @@ -521,6 +536,7 @@ namespace MediaBrowser.Api.Playback.Hls FileShare = FileShareMode.ReadWrite, OnComplete = () => { + Logger.LogDebug("finished serving {0}", segmentPath); if (transcodingJob != null) { transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks); @@ -909,9 +925,23 @@ namespace MediaBrowser.Api.Playback.Hls else { var keyFrameArg = string.Format( + CultureInfo.InvariantCulture, " -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"", GetStartNumber(state) * state.SegmentLength, - state.SegmentLength.ToString(CultureInfo.InvariantCulture)); + state.SegmentLength); + if (state.TargetFramerate.HasValue) + { + // This is to make sure keyframe interval is limited to our segment, + // as forcing keyframes is not enough. + // Example: we encoded half of desired length, then codec detected + // scene cut and inserted a keyframe; next forced keyframe would + // be created outside of segment, which breaks seeking. + keyFrameArg += string.Format( + CultureInfo.InvariantCulture, + " -g {0} -keyint_min {0}", + (int)(state.SegmentLength * state.TargetFramerate) + ); + } var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; @@ -955,6 +985,15 @@ namespace MediaBrowser.Api.Playback.Hls var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, videoCodec); + if (state.BaseRequest.BreakOnNonKeyFrames) + { + // FIXME: this is actually a workaround, as ideally it really should be the client which decides whether non-keyframe + // breakpoints are supported; but current implementation always uses "ffmpeg input seeking" which is liable + // to produce a missing part of video stream before first keyframe is encountered, which may lead to + // awkward cases like a few starting HLS segments having no video whatsoever, which breaks hls.js + Logger.LogInformation("Current HLS implementation doesn't support non-keyframe breaks but one is requested, ignoring that request"); + state.BaseRequest.BreakOnNonKeyFrames = false; + } var inputModifier = EncodingHelper.GetInputModifier(state, encodingOptions); // If isEncoding is true we're actually starting ffmpeg @@ -965,14 +1004,6 @@ namespace MediaBrowser.Api.Playback.Hls var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request); - var timeDeltaParam = string.Empty; - - if (isEncoding && state.TargetFramerate > 0) - { - float startTime = 1 / (state.TargetFramerate.Value * 2); - timeDeltaParam = string.Format(CultureInfo.InvariantCulture, "-segment_time_delta {0:F3}", startTime); - } - var segmentFormat = GetSegmentFileExtension(state.Request).TrimStart('.'); if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase)) { @@ -980,7 +1011,7 @@ namespace MediaBrowser.Api.Playback.Hls } return string.Format( - "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format {11} -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"", + "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f hls -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -hls_time {6} -individual_header_trailer 0 -hls_segment_type {7} -start_number {8} -hls_segment_filename \"{9}\" -hls_playlist_type vod -hls_list_size 0 -y \"{10}\"", inputModifier, EncodingHelper.GetInputArgument(state, encodingOptions), threads, @@ -988,11 +1019,10 @@ namespace MediaBrowser.Api.Playback.Hls GetVideoArguments(state, encodingOptions), GetAudioArguments(state, encodingOptions), state.SegmentLength.ToString(CultureInfo.InvariantCulture), + segmentFormat, startNumberParam, - outputPath, outputTsArg, - timeDeltaParam, - segmentFormat + outputPath ).Trim(); } } diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs index 52043d3df..1878f51d0 100644 --- a/MediaBrowser.Api/Subtitles/SubtitleService.cs +++ b/MediaBrowser.Api/Subtitles/SubtitleService.cs @@ -279,13 +279,12 @@ namespace MediaBrowser.Api.Subtitles await _subtitleManager.DownloadSubtitles(video, request.SubtitleId, CancellationToken.None) .ConfigureAwait(false); - _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(Logger, _fileSystem)), RefreshPriority.High); + _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High); } catch (Exception ex) { Logger.LogError(ex, "Error downloading subtitles"); } - }); } } diff --git a/MediaBrowser.Api/UserLibrary/PersonsService.cs b/MediaBrowser.Api/UserLibrary/PersonsService.cs index c26457778..99f830013 100644 --- a/MediaBrowser.Api/UserLibrary/PersonsService.cs +++ b/MediaBrowser.Api/UserLibrary/PersonsService.cs @@ -109,6 +109,11 @@ namespace MediaBrowser.Api.UserLibrary NameContains = query.NameContains ?? query.SearchTerm }); + if (query.IsFavorite ?? false && query.User != null) + { + items = items.Where(i => UserDataRepository.GetUserData(query.User, i).IsFavorite).ToList(); + } + return new QueryResult<(BaseItem, ItemCounts)> { TotalRecordCount = items.Count, diff --git a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs index 45694a678..da0bf6dcb 100644 --- a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs +++ b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs @@ -413,7 +413,7 @@ namespace MediaBrowser.Api.UserLibrary if (!hasMetdata) { - var options = new MetadataRefreshOptions(new DirectoryService(Logger, _fileSystem)) + var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { MetadataRefreshMode = MetadataRefreshMode.FullRefresh, ImageRefreshMode = MetadataRefreshMode.FullRefresh, diff --git a/MediaBrowser.Common/Cryptography/PasswordHash.cs b/MediaBrowser.Common/Cryptography/PasswordHash.cs index 7741571db..4c6804097 100644 --- a/MediaBrowser.Common/Cryptography/PasswordHash.cs +++ b/MediaBrowser.Common/Cryptography/PasswordHash.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Text; -using static MediaBrowser.Common.HexHelper; namespace MediaBrowser.Common.Cryptography { @@ -102,13 +101,13 @@ namespace MediaBrowser.Common.Cryptography // Check if the string also contains a salt if (splitted.Length - index == 2) { - salt = FromHexString(splitted[index++]); - hash = FromHexString(splitted[index++]); + salt = Hex.Decode(splitted[index++]); + hash = Hex.Decode(splitted[index++]); } else { salt = Array.Empty<byte>(); - hash = FromHexString(splitted[index++]); + hash = Hex.Decode(splitted[index++]); } return new PasswordHash(id, hash, salt, parameters); @@ -124,10 +123,10 @@ namespace MediaBrowser.Common.Cryptography stringBuilder.Append('$'); foreach (var pair in _parameters) { - stringBuilder.Append(pair.Key); - stringBuilder.Append('='); - stringBuilder.Append(pair.Value); - stringBuilder.Append(','); + stringBuilder.Append(pair.Key) + .Append('=') + .Append(pair.Value) + .Append(','); } // Remove last ',' @@ -137,21 +136,19 @@ namespace MediaBrowser.Common.Cryptography /// <inheritdoc /> public override string ToString() { - var str = new StringBuilder(); - str.Append('$'); - str.Append(Id); + var str = new StringBuilder() + .Append('$') + .Append(Id); SerializeParameters(str); if (Salt.Length != 0) { - str.Append('$'); - str.Append(ToHexString(Salt)); + str.Append('$') + .Append(Hex.Encode(Salt, false)); } - str.Append('$'); - str.Append(ToHexString(Hash)); - - return str.ToString(); + return str.Append('$') + .Append(Hex.Encode(Hash, false)).ToString(); } } } diff --git a/MediaBrowser.Common/Extensions/CollectionExtensions.cs b/MediaBrowser.Common/Extensions/CollectionExtensions.cs deleted file mode 100644 index 215224398..000000000 --- a/MediaBrowser.Common/Extensions/CollectionExtensions.cs +++ /dev/null @@ -1,48 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; - -namespace MediaBrowser.Common.Extensions -{ - // The MS CollectionExtensions are only available in netcoreapp - public static class CollectionExtensions - { - public static TValue GetValueOrDefault<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, TKey key) - { - dictionary.TryGetValue(key, out var ret); - return ret; - } - - /// <summary> - /// Copies all the elements of the current collection to the specified list - /// starting at the specified destination array index. The index is specified as a 32-bit integer. - /// </summary> - /// <param name="source">The current collection that is the source of the elements.</param> - /// <param name="destination">The list that is the destination of the elements copied from the current collection.</param> - /// <param name="index">A 32-bit integer that represents the index in <c>destination</c> at which copying begins.</param> - /// <typeparam name="T"></typeparam> - public static void CopyTo<T>(this IReadOnlyList<T> source, IList<T> destination, int index = 0) - { - for (int i = 0; i < source.Count; i++) - { - destination[index + i] = source[i]; - } - } - - /// <summary> - /// Copies all the elements of the current collection to the specified list - /// starting at the specified destination array index. The index is specified as a 32-bit integer. - /// </summary> - /// <param name="source">The current collection that is the source of the elements.</param> - /// <param name="destination">The list that is the destination of the elements copied from the current collection.</param> - /// <param name="index">A 32-bit integer that represents the index in <c>destination</c> at which copying begins.</param> - /// <typeparam name="T"></typeparam> - public static void CopyTo<T>(this IReadOnlyCollection<T> source, IList<T> destination, int index = 0) - { - foreach (T item in source) - { - destination[index++] = item; - } - } - } -} diff --git a/MediaBrowser.Common/Extensions/CopyToExtensions.cs b/MediaBrowser.Common/Extensions/CopyToExtensions.cs new file mode 100644 index 000000000..78a73f07e --- /dev/null +++ b/MediaBrowser.Common/Extensions/CopyToExtensions.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Common.Extensions +{ + /// <summary> + /// Provides <c>CopyTo</c> extensions methods for <see cref="IReadOnlyList{T}" />. + /// </summary> + public static class CollectionExtensions + { + /// <summary> + /// Copies all the elements of the current collection to the specified list + /// starting at the specified destination array index. The index is specified as a 32-bit integer. + /// </summary> + /// <param name="source">The current collection that is the source of the elements.</param> + /// <param name="destination">The list that is the destination of the elements copied from the current collection.</param> + /// <param name="index">A 32-bit integer that represents the index in <c>destination</c> at which copying begins.</param> + /// <typeparam name="T"></typeparam> + public static void CopyTo<T>(this IReadOnlyList<T> source, IList<T> destination, int index = 0) + { + for (int i = 0; i < source.Count; i++) + { + destination[index + i] = source[i]; + } + } + } +} diff --git a/MediaBrowser.Common/Hex.cs b/MediaBrowser.Common/Hex.cs new file mode 100644 index 000000000..b2d9aea3a --- /dev/null +++ b/MediaBrowser.Common/Hex.cs @@ -0,0 +1,94 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace MediaBrowser.Common +{ + /// <summary> + /// Encoding and decoding hex strings. + /// </summary> + public static class Hex + { + internal const string HexCharsLower = "0123456789abcdef"; + internal const string HexCharsUpper = "0123456789ABCDEF"; + + internal const int LastHexSymbol = 0x66; // 102: f + + /// <summary> + /// Map from an ASCII char to its hex value shifted, + /// e.g. <c>b</c> -> 11. 0xFF means it's not a hex symbol. + /// </summary> + /// <value></value> + internal static ReadOnlySpan<byte> HexLookup => new byte[] { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f + }; + + /// <summary> + /// Encodes <c>bytes</c> as a hex string. + /// </summary> + /// <param name="bytes"></param> + /// <param name="lowercase"></param> + /// <returns><c>bytes</c> as a hex string.</returns> + public static string Encode(ReadOnlySpan<byte> bytes, bool lowercase = true) + { + var hexChars = lowercase ? HexCharsLower : HexCharsUpper; + + // TODO: use string.Create when it's supports spans + // Ref: https://github.com/dotnet/corefx/issues/29120 + char[] s = new char[bytes.Length * 2]; + int j = 0; + for (int i = 0; i < bytes.Length; i++) + { + s[j++] = hexChars[bytes[i] >> 4]; + s[j++] = hexChars[bytes[i] & 0x0f]; + } + + return new string(s); + } + + /// <summary> + /// Decodes a hex string into bytes. + /// </summary> + /// <param name="str">The <see cref="string" />.</param> + /// <returns>The decoded bytes.</returns> + public static byte[] Decode(ReadOnlySpan<char> str) + { + if (str.Length == 0) + { + return Array.Empty<byte>(); + } + + var unHex = HexLookup; + + int byteLen = str.Length / 2; + byte[] bytes = new byte[byteLen]; + int i = 0; + for (int j = 0; j < byteLen; j++) + { + byte a; + byte b; + if (str[i] > LastHexSymbol + || (a = unHex[str[i++]]) == 0xFF + || str[i] > LastHexSymbol + || (b = unHex[str[i++]]) == 0xFF) + { + ThrowArgumentException(nameof(str)); + break; // Unreachable + } + + bytes[j] = (byte)((a * 16) | b); + } + + return bytes; + } + + [DoesNotReturn] + private static void ThrowArgumentException(string paramName) + => throw new ArgumentException("Character is not a hex symbol.", paramName); + } +} diff --git a/MediaBrowser.Common/HexHelper.cs b/MediaBrowser.Common/HexHelper.cs deleted file mode 100644 index 61007b5b2..000000000 --- a/MediaBrowser.Common/HexHelper.cs +++ /dev/null @@ -1,24 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Globalization; - -namespace MediaBrowser.Common -{ - public static class HexHelper - { - public static byte[] FromHexString(string str) - { - byte[] bytes = new byte[str.Length / 2]; - for (int i = 0; i < str.Length; i += 2) - { - bytes[i / 2] = byte.Parse(str.Substring(i, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture); - } - - return bytes; - } - - public static string ToHexString(byte[] bytes) - => BitConverter.ToString(bytes).Replace("-", ""); - } -} diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index cf3f6c2a4..889fbfa5a 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -12,7 +12,7 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.2.0" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.0.0" /> <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.0" /> </ItemGroup> @@ -21,7 +21,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs index cacda8140..54540e892 100644 --- a/MediaBrowser.Controller/Entities/AggregateFolder.cs +++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs @@ -89,7 +89,7 @@ namespace MediaBrowser.Controller.Entities { var locations = PhysicalLocations; - var newLocations = CreateResolveArgs(new DirectoryService(Logger, FileSystem), false).PhysicalLocations; + var newLocations = CreateResolveArgs(new DirectoryService(FileSystem), false).PhysicalLocations; if (!locations.SequenceEqual(newLocations)) { diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 599d41bb2..1fd706857 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1344,7 +1344,7 @@ namespace MediaBrowser.Controller.Entities public Task RefreshMetadata(CancellationToken cancellationToken) { - return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)), cancellationToken); + return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(FileSystem)), cancellationToken); } protected virtual void TriggerOnRefreshStart() @@ -2197,7 +2197,7 @@ namespace MediaBrowser.Controller.Entities /// <returns>Task.</returns> public virtual void ChangedExternally() { - ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)) + ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(FileSystem)) { }, RefreshPriority.High); diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index bc5e7467e..e5adf88d1 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -171,7 +171,7 @@ namespace MediaBrowser.Controller.Entities { var locations = PhysicalLocations; - var newLocations = CreateResolveArgs(new DirectoryService(Logger, FileSystem), false).PhysicalLocations; + var newLocations = CreateResolveArgs(new DirectoryService(FileSystem), false).PhysicalLocations; if (!locations.SequenceEqual(newLocations)) { diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 61cc208d7..1cf7bb9b2 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -216,7 +216,7 @@ namespace MediaBrowser.Controller.Entities public Task ValidateChildren(IProgress<double> progress, CancellationToken cancellationToken) { - return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem))); + return ValidateChildren(progress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(FileSystem))); } /// <summary> diff --git a/MediaBrowser.Controller/Entities/User.cs b/MediaBrowser.Controller/Entities/User.cs index c70ecccf1..53601a610 100644 --- a/MediaBrowser.Controller/Entities/User.cs +++ b/MediaBrowser.Controller/Entities/User.cs @@ -148,7 +148,7 @@ namespace MediaBrowser.Controller.Entities Name = newName; return RefreshMetadata( - new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)) + new MetadataRefreshOptions(new DirectoryService(FileSystem)) { ReplaceAllMetadata = true, ImageRefreshMode = MetadataRefreshMode.FullRefresh, diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index c6bca2518..276eb71bc 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -17,7 +17,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index d896a7aef..4dfb27130 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2168,7 +2168,8 @@ namespace MediaBrowser.Controller.MediaEncoding // Important: If this is ever re-enabled, make sure not to use it with wtv because it breaks seeking if (!string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase) && state.TranscodingType != TranscodingJobType.Progressive - && state.EnableBreakOnNonKeyFrames(outputVideoCodec)) + && !state.EnableBreakOnNonKeyFrames(outputVideoCodec) + && (state.BaseRequest.StartTimeTicks ?? 0) > 0) { inputModifier += " -noaccurate_seek"; } diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs index 133e7c115..303b03a21 100644 --- a/MediaBrowser.Controller/Providers/DirectoryService.cs +++ b/MediaBrowser.Controller/Providers/DirectoryService.cs @@ -2,13 +2,11 @@ using System; using System.Collections.Generic; using System.Linq; using MediaBrowser.Model.IO; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Providers { public class DirectoryService : IDirectoryService { - private readonly ILogger _logger; private readonly IFileSystem _fileSystem; private readonly Dictionary<string, FileSystemMetadata[]> _cache = new Dictionary<string, FileSystemMetadata[]>(StringComparer.OrdinalIgnoreCase); @@ -17,9 +15,8 @@ namespace MediaBrowser.Controller.Providers private readonly Dictionary<string, List<string>> _filePathCache = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase); - public DirectoryService(ILogger logger, IFileSystem fileSystem) + public DirectoryService(IFileSystem fileSystem) { - _logger = logger; _fileSystem = fileSystem; } @@ -27,8 +24,6 @@ namespace MediaBrowser.Controller.Providers { if (!_cache.TryGetValue(path, out FileSystemMetadata[] entries)) { - //_logger.LogDebug("Getting files for " + path); - entries = _fileSystem.GetFileSystemEntries(path).ToArray(); //_cache.TryAdd(path, entries); @@ -49,6 +44,7 @@ namespace MediaBrowser.Controller.Providers list.Add(item); } } + return list; } @@ -89,6 +85,5 @@ namespace MediaBrowser.Controller.Providers return result; } - } } diff --git a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs index 0872335c5..39538aacd 100644 --- a/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs +++ b/MediaBrowser.Controller/Subtitles/ISubtitleManager.cs @@ -24,7 +24,8 @@ namespace MediaBrowser.Controller.Subtitles /// <summary> /// Searches the subtitles. /// </summary> - Task<RemoteSubtitleInfo[]> SearchSubtitles(Video video, + Task<RemoteSubtitleInfo[]> SearchSubtitles( + Video video, string language, bool? isPerfectMatch, CancellationToken cancellationToken); @@ -34,8 +35,9 @@ namespace MediaBrowser.Controller.Subtitles /// </summary> /// <param name="request">The request.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{IEnumerable{RemoteSubtitleInfo}}.</returns> - Task<RemoteSubtitleInfo[]> SearchSubtitles(SubtitleSearchRequest request, + /// <returns>Task{RemoteSubtitleInfo[]}.</returns> + Task<RemoteSubtitleInfo[]> SearchSubtitles( + SubtitleSearchRequest request, CancellationToken cancellationToken); /// <summary> @@ -53,7 +55,7 @@ namespace MediaBrowser.Controller.Subtitles /// </summary> /// <param name="id">The identifier.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{SubtitleResponse}.</returns> + /// <returns><see cref="Task{SubtitleResponse}" />.</returns> Task<SubtitleResponse> GetRemoteSubtitles(string id, CancellationToken cancellationToken); /// <summary> diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj index a8f8da9b8..71eb62693 100644 --- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj +++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj @@ -10,7 +10,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index 264f31f3c..558ea7d67 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> @@ -18,7 +18,7 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="System.Text.Encoding.CodePages" Version="4.5.1" /> + <PackageReference Include="System.Text.Encoding.CodePages" Version="4.6.0" /> <PackageReference Include="UTF.Unknown" Version="2.1.0" /> </ItemGroup> diff --git a/MediaBrowser.Model/Globalization/ILocalizationManager.cs b/MediaBrowser.Model/Globalization/ILocalizationManager.cs index 91d946db8..0b6cfe1b7 100644 --- a/MediaBrowser.Model/Globalization/ILocalizationManager.cs +++ b/MediaBrowser.Model/Globalization/ILocalizationManager.cs @@ -64,10 +64,10 @@ namespace MediaBrowser.Model.Globalization bool HasUnicodeCategory(string value, UnicodeCategory category); /// <summary> - /// Returns the correct <see cref="Cultureinfo" /> for the given language. + /// Returns the correct <see cref="CultureInfo" /> for the given language. /// </summary> /// <param name="language">The language.</param> - /// <returns>The correct <see cref="Cultureinfo" /> for the given language.</returns> + /// <returns>The correct <see cref="CultureInfo" /> for the given language.</returns> CultureDto FindLanguageInfo(string language); } } diff --git a/MediaBrowser.Model/IO/StreamDefaults.cs b/MediaBrowser.Model/IO/StreamDefaults.cs index 1dc29e06e..4b55ce1f3 100644 --- a/MediaBrowser.Model/IO/StreamDefaults.cs +++ b/MediaBrowser.Model/IO/StreamDefaults.cs @@ -1,17 +1,17 @@ namespace MediaBrowser.Model.IO { /// <summary> - /// Class StreamDefaults + /// Class StreamDefaults. /// </summary> public static class StreamDefaults { /// <summary> - /// The default copy to buffer size + /// The default copy to buffer size. /// </summary> public const int DefaultCopyToBufferSize = 81920; /// <summary> - /// The default file stream buffer size + /// The default file stream buffer size. /// </summary> public const int DefaultFileStreamBufferSize = 4096; } diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 3ed319a0d..53cd08fbd 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -1,4 +1,4 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <Authors>Jellyfin Contributors</Authors> @@ -15,7 +15,8 @@ <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.2.0" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.0.0" /> + <PackageReference Include="System.Globalization" Version="4.3.0" /> <PackageReference Include="System.Text.Json" Version="4.6.0" /> </ItemGroup> diff --git a/MediaBrowser.Model/Net/SocketReceiveResult.cs b/MediaBrowser.Model/Net/SocketReceiveResult.cs index cd7a2e55f..3a4ad3738 100644 --- a/MediaBrowser.Model/Net/SocketReceiveResult.cs +++ b/MediaBrowser.Model/Net/SocketReceiveResult.cs @@ -18,7 +18,7 @@ namespace MediaBrowser.Model.Net public int ReceivedBytes { get; set; } /// <summary> - /// The <see cref="IpEndPointInfo"/> the data was received from. + /// The <see cref="IPEndPoint"/> the data was received from. /// </summary> public IPEndPoint RemoteEndPoint { get; set; } public IPAddress LocalIPAddress { get; set; } diff --git a/MediaBrowser.Providers/Books/AudioBookMetadataService.cs b/MediaBrowser.Providers/Books/AudioBookMetadataService.cs index 0062d5ab3..309241bfa 100644 --- a/MediaBrowser.Providers/Books/AudioBookMetadataService.cs +++ b/MediaBrowser.Providers/Books/AudioBookMetadataService.cs @@ -16,9 +16,8 @@ namespace MediaBrowser.Providers.Books ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, - IUserDataManager userDataManager, ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { } diff --git a/MediaBrowser.Providers/Books/BookMetadataService.cs b/MediaBrowser.Providers/Books/BookMetadataService.cs index 358f87a0f..9d6a1ef59 100644 --- a/MediaBrowser.Providers/Books/BookMetadataService.cs +++ b/MediaBrowser.Providers/Books/BookMetadataService.cs @@ -11,6 +11,17 @@ namespace MediaBrowser.Providers.Books { public class BookMetadataService : MetadataService<Book, BookInfo> { + public BookMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) + { + } + + /// <inheritdoc /> protected override void MergeData(MetadataResult<Book> source, MetadataResult<Book> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); @@ -20,9 +31,5 @@ namespace MediaBrowser.Providers.Books target.Item.SeriesName = source.Item.SeriesName; } } - - public BookMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) - { - } } } diff --git a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs index 693edb143..5bf01232c 100644 --- a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs +++ b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs @@ -14,11 +14,35 @@ namespace MediaBrowser.Providers.BoxSets { public class BoxSetMetadataService : MetadataService<BoxSet, BoxSetInfo> { + public BoxSetMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) + { + } + + /// <inheritdoc /> + protected override bool EnableUpdatingGenresFromChildren => true; + + /// <inheritdoc /> + protected override bool EnableUpdatingOfficialRatingFromChildren => true; + + /// <inheritdoc /> + protected override bool EnableUpdatingStudiosFromChildren => true; + + /// <inheritdoc /> + protected override bool EnableUpdatingPremiereDateFromChildren => true; + + /// <inheritdoc /> protected override IList<BaseItem> GetChildrenForMetadataUpdates(BoxSet item) { return item.GetLinkedChildren(); } + /// <inheritdoc /> protected override void MergeData(MetadataResult<BoxSet> source, MetadataResult<BoxSet> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); @@ -32,6 +56,7 @@ namespace MediaBrowser.Providers.BoxSets } } + /// <inheritdoc /> protected override ItemUpdateType BeforeSaveInternal(BoxSet item, bool isFullRefresh, ItemUpdateType currentUpdateType) { var updateType = base.BeforeSaveInternal(item, isFullRefresh, currentUpdateType); @@ -47,17 +72,5 @@ namespace MediaBrowser.Providers.BoxSets return updateType; } - - public BoxSetMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) - { - } - - protected override bool EnableUpdatingGenresFromChildren => true; - - protected override bool EnableUpdatingOfficialRatingFromChildren => true; - - protected override bool EnableUpdatingStudiosFromChildren => true; - - protected override bool EnableUpdatingPremiereDateFromChildren => true; } } diff --git a/MediaBrowser.Providers/Channels/ChannelMetadataService.cs b/MediaBrowser.Providers/Channels/ChannelMetadataService.cs index 0763a0163..da41f208c 100644 --- a/MediaBrowser.Providers/Channels/ChannelMetadataService.cs +++ b/MediaBrowser.Providers/Channels/ChannelMetadataService.cs @@ -11,13 +11,20 @@ namespace MediaBrowser.Providers.Channels { public class ChannelMetadataService : MetadataService<Channel, ItemLookupInfo> { - protected override void MergeData(MetadataResult<Channel> source, MetadataResult<Channel> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) + public ChannelMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { - ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } - public ChannelMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + /// <inheritdoc /> + protected override void MergeData(MetadataResult<Channel> source, MetadataResult<Channel> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } } } diff --git a/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs b/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs index 2c28b3e35..dd1b4709d 100644 --- a/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs +++ b/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs @@ -12,13 +12,20 @@ namespace MediaBrowser.Providers.Folders { public class CollectionFolderMetadataService : MetadataService<CollectionFolder, ItemLookupInfo> { - protected override void MergeData(MetadataResult<CollectionFolder> source, MetadataResult<CollectionFolder> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) + public CollectionFolderMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { - ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } - public CollectionFolderMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + /// <inheritdoc /> + protected override void MergeData(MetadataResult<CollectionFolder> source, MetadataResult<CollectionFolder> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } } } diff --git a/MediaBrowser.Providers/Folders/FolderMetadataService.cs b/MediaBrowser.Providers/Folders/FolderMetadataService.cs index bb1634422..8409e03fd 100644 --- a/MediaBrowser.Providers/Folders/FolderMetadataService.cs +++ b/MediaBrowser.Providers/Folders/FolderMetadataService.cs @@ -11,16 +11,24 @@ namespace MediaBrowser.Providers.Folders { public class FolderMetadataService : MetadataService<Folder, ItemLookupInfo> { + public FolderMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) + { + } + + /// <inheritdoc /> // Make sure the type-specific services get picked first public override int Order => 10; + /// <inheritdoc /> protected override void MergeData(MetadataResult<Folder> source, MetadataResult<Folder> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } - - public FolderMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) - { - } } } diff --git a/MediaBrowser.Providers/Folders/UserViewMetadataService.cs b/MediaBrowser.Providers/Folders/UserViewMetadataService.cs index 8d6e77aeb..2ceb71afc 100644 --- a/MediaBrowser.Providers/Folders/UserViewMetadataService.cs +++ b/MediaBrowser.Providers/Folders/UserViewMetadataService.cs @@ -11,13 +11,20 @@ namespace MediaBrowser.Providers.Folders { public class UserViewMetadataService : MetadataService<UserView, ItemLookupInfo> { - protected override void MergeData(MetadataResult<UserView> source, MetadataResult<UserView> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) + public UserViewMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { - ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } - public UserViewMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + /// <inheritdoc /> + protected override void MergeData(MetadataResult<UserView> source, MetadataResult<UserView> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } } } diff --git a/MediaBrowser.Providers/Genres/GenreMetadataService.cs b/MediaBrowser.Providers/Genres/GenreMetadataService.cs index 28a46b008..932eb368c 100644 --- a/MediaBrowser.Providers/Genres/GenreMetadataService.cs +++ b/MediaBrowser.Providers/Genres/GenreMetadataService.cs @@ -11,13 +11,20 @@ namespace MediaBrowser.Providers.Genres { public class GenreMetadataService : MetadataService<Genre, ItemLookupInfo> { - protected override void MergeData(MetadataResult<Genre> source, MetadataResult<Genre> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) + public GenreMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { - ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } - public GenreMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + /// <inheritdoc /> + protected override void MergeData(MetadataResult<Genre> source, MetadataResult<Genre> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } } } diff --git a/MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs b/MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs index 6009d3a58..13dd97215 100644 --- a/MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs +++ b/MediaBrowser.Providers/LiveTv/ProgramMetadataService.cs @@ -11,13 +11,20 @@ namespace MediaBrowser.Providers.LiveTv { public class LiveTvMetadataService : MetadataService<LiveTvChannel, ItemLookupInfo> { - protected override void MergeData(MetadataResult<LiveTvChannel> source, MetadataResult<LiveTvChannel> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) + public LiveTvMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { - ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } - public LiveTvMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + /// <inheritdoc /> + protected override void MergeData(MetadataResult<LiveTvChannel> source, MetadataResult<LiveTvChannel> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } } } diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index 033aea146..e9179815e 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -340,7 +340,7 @@ namespace MediaBrowser.Providers.Manager if (deleted) { - item.ValidateImages(new DirectoryService(_logger, _fileSystem)); + item.ValidateImages(new DirectoryService(_fileSystem)); } } diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index f0716f201..c3401f12b 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -23,16 +23,14 @@ namespace MediaBrowser.Providers.Manager protected readonly ILogger Logger; protected readonly IProviderManager ProviderManager; protected readonly IFileSystem FileSystem; - protected readonly IUserDataManager UserDataManager; protected readonly ILibraryManager LibraryManager; - protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) + protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager) { ServerConfigurationManager = serverConfigurationManager; Logger = logger; ProviderManager = providerManager; FileSystem = fileSystem; - UserDataManager = userDataManager; LibraryManager = libraryManager; } @@ -44,7 +42,7 @@ namespace MediaBrowser.Providers.Manager } catch (Exception ex) { - Logger.LogError(ex, "Error getting file {path}", path); + Logger.LogError(ex, "Error getting file {Path}", path); return null; } } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 6a8d03f6d..631d063a5 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -328,7 +328,7 @@ namespace MediaBrowser.Providers.Manager return GetImageProviders(item, libraryOptions, options, new ImageRefreshOptions( - new DirectoryService(_logger, _fileSystem)), + new DirectoryService(_fileSystem)), includeDisabled) .OfType<IRemoteImageProvider>(); } @@ -507,7 +507,7 @@ namespace MediaBrowser.Providers.Manager var imageProviders = GetImageProviders(dummy, libraryOptions, options, new ImageRefreshOptions( - new DirectoryService(_logger, _fileSystem)), + new DirectoryService(_fileSystem)), true) .ToList(); diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index c7ecc59c9..ae2102806 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -11,15 +11,15 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration" Version="2.2.0" /> - <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.2.0" /> + <PackageReference Include="Microsoft.Extensions.Configuration" Version="3.0.0" /> + <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.0.0" /> <PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" /> <PackageReference Include="PlaylistsNET" Version="1.0.4" /> <PackageReference Include="TvDbSharper" Version="2.0.0" /> </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> @@ -28,5 +28,5 @@ <!-- We need at least C# 7.1 --> <LangVersion>latest</LangVersion> </PropertyGroup> - + </Project> diff --git a/MediaBrowser.Providers/Movies/MovieExternalIds.cs b/MediaBrowser.Providers/Movies/MovieExternalIds.cs index 09ed6034c..55810b1ed 100644 --- a/MediaBrowser.Providers/Movies/MovieExternalIds.cs +++ b/MediaBrowser.Providers/Movies/MovieExternalIds.cs @@ -9,17 +9,20 @@ namespace MediaBrowser.Providers.Movies { public class ImdbExternalId : IExternalId { + /// <inheritdoc /> public string Name => "IMDb"; + /// <inheritdoc /> public string Key => MetadataProviders.Imdb.ToString(); + /// <inheritdoc /> public string UrlFormatString => "https://www.imdb.com/title/{0}"; + /// <inheritdoc /> public bool Supports(IHasProviderIds item) { // Supports images for tv movies - var tvProgram = item as LiveTvProgram; - if (tvProgram != null && tvProgram.IsMovie) + if (item is LiveTvProgram tvProgram && tvProgram.IsMovie) { return true; } @@ -28,18 +31,18 @@ namespace MediaBrowser.Providers.Movies } } - public class ImdbPersonExternalId : IExternalId { + /// <inheritdoc /> public string Name => "IMDb"; + /// <inheritdoc /> public string Key => MetadataProviders.Imdb.ToString(); + /// <inheritdoc /> public string UrlFormatString => "https://www.imdb.com/name/{0}"; - public bool Supports(IHasProviderIds item) - { - return item is Person; - } + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Person; } } diff --git a/MediaBrowser.Providers/Movies/MovieMetadataService.cs b/MediaBrowser.Providers/Movies/MovieMetadataService.cs index 41806e49b..c6cc5c7dc 100644 --- a/MediaBrowser.Providers/Movies/MovieMetadataService.cs +++ b/MediaBrowser.Providers/Movies/MovieMetadataService.cs @@ -1,5 +1,4 @@ using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -12,6 +11,17 @@ namespace MediaBrowser.Providers.Movies { public class MovieMetadataService : MetadataService<Movie, MovieInfo> { + public MovieMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) + { + } + + /// <inheritdoc /> protected override bool IsFullLocalMetadata(Movie item) { if (string.IsNullOrWhiteSpace(item.Overview)) @@ -25,6 +35,7 @@ namespace MediaBrowser.Providers.Movies return base.IsFullLocalMetadata(item); } + /// <inheritdoc /> protected override void MergeData(MetadataResult<Movie> source, MetadataResult<Movie> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); @@ -37,40 +48,5 @@ namespace MediaBrowser.Providers.Movies targetItem.CollectionName = sourceItem.CollectionName; } } - - public MovieMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) - { - } } - - public class TrailerMetadataService : MetadataService<Trailer, TrailerInfo> - { - protected override bool IsFullLocalMetadata(Trailer item) - { - if (string.IsNullOrWhiteSpace(item.Overview)) - { - return false; - } - if (!item.ProductionYear.HasValue) - { - return false; - } - return base.IsFullLocalMetadata(item); - } - - protected override void MergeData(MetadataResult<Trailer> source, MetadataResult<Trailer> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) - { - ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); - - if (replaceData || target.Item.TrailerTypes.Length == 0) - { - target.Item.TrailerTypes = source.Item.TrailerTypes; - } - } - - public TrailerMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) - { - } - } - } diff --git a/MediaBrowser.Providers/Movies/TrailerMetadataService.cs b/MediaBrowser.Providers/Movies/TrailerMetadataService.cs new file mode 100644 index 000000000..53b556940 --- /dev/null +++ b/MediaBrowser.Providers/Movies/TrailerMetadataService.cs @@ -0,0 +1,49 @@ +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Providers.Manager; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Providers.Movies +{ + public class TrailerMetadataService : MetadataService<Trailer, TrailerInfo> + { + public TrailerMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) + { + } + + /// <inheritdoc /> + protected override bool IsFullLocalMetadata(Trailer item) + { + if (string.IsNullOrWhiteSpace(item.Overview)) + { + return false; + } + if (!item.ProductionYear.HasValue) + { + return false; + } + return base.IsFullLocalMetadata(item); + } + + /// <inheritdoc /> + protected override void MergeData(MetadataResult<Trailer> source, MetadataResult<Trailer> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) + { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); + + if (replaceData || target.Item.TrailerTypes.Length == 0) + { + target.Item.TrailerTypes = source.Item.TrailerTypes; + } + } + } +} diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs index 4e59b4119..69133c1c1 100644 --- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs +++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs @@ -20,9 +20,8 @@ namespace MediaBrowser.Providers.Music ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, - IUserDataManager userDataManager, ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { } @@ -37,10 +36,7 @@ namespace MediaBrowser.Providers.Music /// <inheritdoc /> protected override IList<BaseItem> GetChildrenForMetadataUpdates(MusicAlbum item) - { - return item.GetRecursiveChildren(i => i is Audio) - .ToList(); - } + => item.GetRecursiveChildren(i => i is Audio); /// <inheritdoc /> protected override ItemUpdateType UpdateMetadataFromChildren(MusicAlbum item, IList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType) @@ -53,20 +49,18 @@ namespace MediaBrowser.Providers.Music { var name = children.Select(i => i.Album).FirstOrDefault(i => !string.IsNullOrEmpty(i)); - if (!string.IsNullOrEmpty(name)) + if (!string.IsNullOrEmpty(name) + && !string.Equals(item.Name, name, StringComparison.Ordinal)) { - if (!string.Equals(item.Name, name, StringComparison.Ordinal)) - { - item.Name = name; - updateType = updateType | ItemUpdateType.MetadataEdit; - } + item.Name = name; + updateType |= ItemUpdateType.MetadataEdit; } } var songs = children.Cast<Audio>().ToArray(); - updateType = updateType | SetAlbumArtistFromSongs(item, songs); - updateType = updateType | SetArtistsFromSongs(item, songs); + updateType |= SetAlbumArtistFromSongs(item, songs); + updateType |= SetArtistsFromSongs(item, songs); } return updateType; diff --git a/MediaBrowser.Providers/Music/ArtistMetadataService.cs b/MediaBrowser.Providers/Music/ArtistMetadataService.cs index 5b8782554..1f099c60f 100644 --- a/MediaBrowser.Providers/Music/ArtistMetadataService.cs +++ b/MediaBrowser.Providers/Music/ArtistMetadataService.cs @@ -13,26 +13,35 @@ namespace MediaBrowser.Providers.Music { public class ArtistMetadataService : MetadataService<MusicArtist, ArtistInfo> { - protected override IList<BaseItem> GetChildrenForMetadataUpdates(MusicArtist item) + public ArtistMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { - return item.IsAccessedByName ? - item.GetTaggedItems(new InternalItemsQuery - { - Recursive = true, - IsFolder = false - }) : - item.GetRecursiveChildren(i => i is IHasArtist && !i.IsFolder); } + /// <inheritdoc /> protected override bool EnableUpdatingGenresFromChildren => true; - protected override void MergeData(MetadataResult<MusicArtist> source, MetadataResult<MusicArtist> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) + /// <inheritdoc /> + protected override IList<BaseItem> GetChildrenForMetadataUpdates(MusicArtist item) { - ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); + return item.IsAccessedByName + ? item.GetTaggedItems(new InternalItemsQuery + { + Recursive = true, + IsFolder = false + }) + : item.GetRecursiveChildren(i => i is IHasArtist && !i.IsFolder); } - public ArtistMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + /// <inheritdoc /> + protected override void MergeData(MetadataResult<MusicArtist> source, MetadataResult<MusicArtist> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } } } diff --git a/MediaBrowser.Providers/Music/AudioDbAlbumImageProvider.cs b/MediaBrowser.Providers/Music/AudioDbAlbumImageProvider.cs index c2a8431cd..85a87630d 100644 --- a/MediaBrowser.Providers/Music/AudioDbAlbumImageProvider.cs +++ b/MediaBrowser.Providers/Music/AudioDbAlbumImageProvider.cs @@ -25,6 +25,14 @@ namespace MediaBrowser.Providers.Music _json = json; } + /// <inheritdoc /> + public string Name => "TheAudioDB"; + + /// <inheritdoc /> + // After embedded and fanart + public int Order => 2; + + /// <inheritdoc /> public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { return new List<ImageType> @@ -34,6 +42,7 @@ namespace MediaBrowser.Providers.Music }; } + /// <inheritdoc /> public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { var id = item.GetProviderId(MetadataProviders.MusicBrainzReleaseGroup); @@ -82,6 +91,7 @@ namespace MediaBrowser.Providers.Music return list; } + /// <inheritdoc /> public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClient.GetResponse(new HttpRequestOptions @@ -91,13 +101,7 @@ namespace MediaBrowser.Providers.Music }); } - public string Name => "TheAudioDB"; - // After embedded and fanart - public int Order => 2; - - public bool Supports(BaseItem item) - { - return item is MusicAlbum; - } + /// <inheritdoc /> + public bool Supports(BaseItem item) => item is MusicAlbum; } } diff --git a/MediaBrowser.Providers/Music/AudioDbAlbumProvider.cs b/MediaBrowser.Providers/Music/AudioDbAlbumProvider.cs index 7ccf7cffa..e61d8792c 100644 --- a/MediaBrowser.Providers/Music/AudioDbAlbumProvider.cs +++ b/MediaBrowser.Providers/Music/AudioDbAlbumProvider.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; @@ -26,8 +27,6 @@ namespace MediaBrowser.Providers.Music public static AudioDbAlbumProvider Current; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - public AudioDbAlbumProvider(IServerConfigurationManager config, IFileSystem fileSystem, IHttpClient httpClient, IJsonSerializer json) { _config = config; @@ -38,11 +37,18 @@ namespace MediaBrowser.Providers.Music Current = this; } + /// <inheritdoc /> + public string Name => "TheAudioDB"; + + /// <inheritdoc /> + // After music brainz + public int Order => 1; + + /// <inheritdoc /> public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancellationToken) - { - return Task.FromResult((IEnumerable<RemoteSearchResult>)new List<RemoteSearchResult>()); - } + => Task.FromResult(Enumerable.Empty<RemoteSearchResult>()); + /// <inheritdoc /> public async Task<MetadataResult<MusicAlbum>> GetMetadata(AlbumInfo info, CancellationToken cancellationToken) { var result = new MetadataResult<MusicAlbum>(); @@ -77,7 +83,7 @@ namespace MediaBrowser.Providers.Music if (!string.IsNullOrEmpty(result.intYearReleased)) { - item.ProductionYear = int.Parse(result.intYearReleased, _usCulture); + item.ProductionYear = int.Parse(result.intYearReleased, CultureInfo.InvariantCulture); } if (!string.IsNullOrEmpty(result.strGenre)) @@ -126,8 +132,6 @@ namespace MediaBrowser.Providers.Music item.Overview = (overview ?? string.Empty).StripHtml(); } - public string Name => "TheAudioDB"; - internal Task EnsureInfo(string musicBrainzReleaseGroupId, CancellationToken cancellationToken) { var xmlPath = GetAlbumInfoPath(_config.ApplicationPaths, musicBrainzReleaseGroupId); @@ -155,20 +159,18 @@ namespace MediaBrowser.Providers.Music Directory.CreateDirectory(Path.GetDirectoryName(path)); - using (var httpResponse = await _httpClient.SendAsync(new HttpRequestOptions - { - Url = url, - CancellationToken = cancellationToken + using (var httpResponse = await _httpClient.SendAsync( + new HttpRequestOptions + { + Url = url, + CancellationToken = cancellationToken - }, "GET").ConfigureAwait(false)) + }, + "GET").ConfigureAwait(false)) + using (var response = httpResponse.Content) + using (var xmlFileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) { - using (var response = httpResponse.Content) - { - using (var xmlFileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) - { - await response.CopyToAsync(xmlFileStream).ConfigureAwait(false); - } - } + await response.CopyToAsync(xmlFileStream).ConfigureAwait(false); } } @@ -192,8 +194,6 @@ namespace MediaBrowser.Providers.Music return Path.Combine(dataPath, "album.json"); } - // After music brainz - public int Order => 1; public class Album { @@ -242,6 +242,7 @@ namespace MediaBrowser.Providers.Music public List<Album> album { get; set; } } + /// <inheritdoc /> public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) { throw new NotImplementedException(); diff --git a/MediaBrowser.Providers/Music/AudioDbArtistImageProvider.cs b/MediaBrowser.Providers/Music/AudioDbArtistImageProvider.cs index c59e9551c..b9315744f 100644 --- a/MediaBrowser.Providers/Music/AudioDbArtistImageProvider.cs +++ b/MediaBrowser.Providers/Music/AudioDbArtistImageProvider.cs @@ -25,6 +25,14 @@ namespace MediaBrowser.Providers.Music _httpClient = httpClient; } + /// <inheritdoc /> + public string Name => "TheAudioDB"; + + /// <inheritdoc /> + // After fanart + public int Order => 1; + + /// <inheritdoc /> public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { return new List<ImageType> @@ -36,6 +44,7 @@ namespace MediaBrowser.Providers.Music }; } + /// <inheritdoc /> public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { var id = item.GetProviderId(MetadataProviders.MusicBrainzArtist); @@ -133,13 +142,7 @@ namespace MediaBrowser.Providers.Music }); } - public string Name => "TheAudioDB"; - - public bool Supports(BaseItem item) - { - return item is MusicArtist; - } - // After fanart - public int Order => 1; + /// <inheritdoc /> + public bool Supports(BaseItem item) => item is MusicArtist; } } diff --git a/MediaBrowser.Providers/Music/AudioDbArtistProvider.cs b/MediaBrowser.Providers/Music/AudioDbArtistProvider.cs index 2540a6047..7e5893d49 100644 --- a/MediaBrowser.Providers/Music/AudioDbArtistProvider.cs +++ b/MediaBrowser.Providers/Music/AudioDbArtistProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; @@ -37,11 +38,18 @@ namespace MediaBrowser.Providers.Music Current = this; } + /// <inheritdoc /> + public string Name => "TheAudioDB"; + + /// <inheritdoc /> + // After musicbrainz + public int Order => 1; + + /// <inheritdoc /> public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken) - { - return Task.FromResult((IEnumerable<RemoteSearchResult>)new List<RemoteSearchResult>()); - } + => Task.FromResult(Enumerable.Empty<RemoteSearchResult>()); + /// <inheritdoc /> public async Task<MetadataResult<MusicArtist>> GetMetadata(ArtistInfo info, CancellationToken cancellationToken) { var result = new MetadataResult<MusicArtist>(); @@ -114,20 +122,16 @@ namespace MediaBrowser.Providers.Music item.Overview = (overview ?? string.Empty).StripHtml(); } - public string Name => "TheAudioDB"; - internal Task EnsureArtistInfo(string musicBrainzId, CancellationToken cancellationToken) { var xmlPath = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId); var fileInfo = _fileSystem.GetFileSystemInfo(xmlPath); - if (fileInfo.Exists) + if (fileInfo.Exists + && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) { - if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) - { - return Task.CompletedTask; - } + return Task.CompletedTask; } return DownloadArtistInfo(musicBrainzId, cancellationToken); @@ -141,22 +145,21 @@ namespace MediaBrowser.Providers.Music var path = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId); - using (var httpResponse = await _httpClient.SendAsync(new HttpRequestOptions + using (var httpResponse = await _httpClient.SendAsync( + new HttpRequestOptions + { + Url = url, + CancellationToken = cancellationToken, + BufferContent = true + }, + "GET").ConfigureAwait(false)) + using (var response = httpResponse.Content) { - Url = url, - CancellationToken = cancellationToken, - BufferContent = true + Directory.CreateDirectory(Path.GetDirectoryName(path)); - }, "GET").ConfigureAwait(false)) - { - using (var response = httpResponse.Content) + using (var xmlFileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) { - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - using (var xmlFileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) - { - await response.CopyToAsync(xmlFileStream).ConfigureAwait(false); - } + await response.CopyToAsync(xmlFileStream).ConfigureAwait(false); } } } @@ -168,11 +171,7 @@ namespace MediaBrowser.Providers.Music /// <param name="musicBrainzArtistId">The music brainz artist identifier.</param> /// <returns>System.String.</returns> private static string GetArtistDataPath(IApplicationPaths appPaths, string musicBrainzArtistId) - { - var dataPath = Path.Combine(GetArtistDataPath(appPaths), musicBrainzArtistId); - - return dataPath; - } + => Path.Combine(GetArtistDataPath(appPaths), musicBrainzArtistId); /// <summary> /// Gets the artist data path. @@ -180,11 +179,7 @@ namespace MediaBrowser.Providers.Music /// <param name="appPaths">The application paths.</param> /// <returns>System.String.</returns> private static string GetArtistDataPath(IApplicationPaths appPaths) - { - var dataPath = Path.Combine(appPaths.CachePath, "audiodb-artist"); - - return dataPath; - } + => Path.Combine(appPaths.CachePath, "audiodb-artist"); internal static string GetArtistInfoPath(IApplicationPaths appPaths, string musicBrainzArtistId) { @@ -242,9 +237,8 @@ namespace MediaBrowser.Providers.Music { public List<Artist> artists { get; set; } } - // After musicbrainz - public int Order => 1; + /// <inheritdoc /> public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) { throw new NotImplementedException(); diff --git a/MediaBrowser.Providers/Music/AudioDbExternalIds.cs b/MediaBrowser.Providers/Music/AudioDbExternalIds.cs index 3e1022d7d..c866d12de 100644 --- a/MediaBrowser.Providers/Music/AudioDbExternalIds.cs +++ b/MediaBrowser.Providers/Music/AudioDbExternalIds.cs @@ -6,58 +6,63 @@ namespace MediaBrowser.Providers.Music { public class AudioDbAlbumExternalId : IExternalId { + /// <inheritdoc /> public string Name => "TheAudioDb"; + /// <inheritdoc /> public string Key => MetadataProviders.AudioDbAlbum.ToString(); + /// <inheritdoc /> public string UrlFormatString => "https://www.theaudiodb.com/album/{0}"; + /// <inheritdoc /> public bool Supports(IHasProviderIds item) - { - return item is MusicAlbum; - } + => item is MusicAlbum; } public class AudioDbOtherAlbumExternalId : IExternalId { + /// <inheritdoc /> public string Name => "TheAudioDb Album"; + /// <inheritdoc /> public string Key => MetadataProviders.AudioDbAlbum.ToString(); + /// <inheritdoc /> public string UrlFormatString => "https://www.theaudiodb.com/album/{0}"; - public bool Supports(IHasProviderIds item) - { - return item is Audio; - } + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Audio; } public class AudioDbArtistExternalId : IExternalId { + /// <inheritdoc /> public string Name => "TheAudioDb"; + /// <inheritdoc /> public string Key => MetadataProviders.AudioDbArtist.ToString(); + /// <inheritdoc /> public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}"; - public bool Supports(IHasProviderIds item) - { - return item is MusicArtist; - } + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is MusicArtist; } public class AudioDbOtherArtistExternalId : IExternalId { + /// <inheritdoc /> public string Name => "TheAudioDb Artist"; + /// <inheritdoc /> public string Key => MetadataProviders.AudioDbArtist.ToString(); + /// <inheritdoc /> public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}"; + /// <inheritdoc /> public bool Supports(IHasProviderIds item) - { - return item is Audio || item is MusicAlbum; - } + => item is Audio || item is MusicAlbum; } - } diff --git a/MediaBrowser.Providers/Music/AudioMetadataService.cs b/MediaBrowser.Providers/Music/AudioMetadataService.cs index 3bf854b91..4d4739cef 100644 --- a/MediaBrowser.Providers/Music/AudioMetadataService.cs +++ b/MediaBrowser.Providers/Music/AudioMetadataService.cs @@ -16,9 +16,8 @@ namespace MediaBrowser.Providers.Music ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, - IUserDataManager userDataManager, ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { } diff --git a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs index 179e953f4..8e71b625e 100644 --- a/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs +++ b/MediaBrowser.Providers/Music/MusicBrainzAlbumProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Net; @@ -14,7 +15,6 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -22,16 +22,6 @@ namespace MediaBrowser.Providers.Music { public class MusicBrainzAlbumProvider : IRemoteMetadataProvider<MusicAlbum, AlbumInfo>, IHasOrder { - internal static MusicBrainzAlbumProvider Current; - - private readonly IHttpClient _httpClient; - private readonly IApplicationHost _appHost; - private readonly ILogger _logger; - private readonly IJsonSerializer _json; - private Stopwatch _stopWatchMusicBrainz = new Stopwatch(); - - public readonly string MusicBrainzBaseUrl; - /// <summary> /// The Jellyfin user-agent is unrestricted but source IP must not exceed /// one request per second, therefore we rate limit to avoid throttling. @@ -47,19 +37,27 @@ namespace MediaBrowser.Providers.Music /// </summary> private const uint MusicBrainzQueryAttempts = 5u; + internal static MusicBrainzAlbumProvider Current; + + private readonly IHttpClient _httpClient; + private readonly IApplicationHost _appHost; + private readonly ILogger _logger; + + private readonly string _musicBrainzBaseUrl; + + private Stopwatch _stopWatchMusicBrainz = new Stopwatch(); + public MusicBrainzAlbumProvider( IHttpClient httpClient, IApplicationHost appHost, ILogger logger, - IJsonSerializer json, IConfiguration configuration) { _httpClient = httpClient; _appHost = appHost; _logger = logger; - _json = json; - MusicBrainzBaseUrl = configuration["MusicBrainz:BaseUrl"]; + _musicBrainzBaseUrl = configuration["MusicBrainz:BaseUrl"]; // Use a stopwatch to ensure we don't exceed the MusicBrainz rate limit _stopWatchMusicBrainz.Start(); @@ -67,6 +65,13 @@ namespace MediaBrowser.Providers.Music Current = this; } + /// <inheritdoc /> + public string Name => "MusicBrainz"; + + /// <inheritdoc /> + public int Order => 0; + + /// <inheritdoc /> public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(AlbumInfo searchInfo, CancellationToken cancellationToken) { var releaseId = searchInfo.GetReleaseId(); @@ -76,11 +81,11 @@ namespace MediaBrowser.Providers.Music if (!string.IsNullOrEmpty(releaseId)) { - url = string.Format("/ws/2/release/?query=reid:{0}", releaseId); + url = "/ws/2/release/?query=reid:" + releaseId.ToString(CultureInfo.InvariantCulture); } else if (!string.IsNullOrEmpty(releaseGroupId)) { - url = string.Format("/ws/2/release?release-group={0}", releaseGroupId); + url = "/ws/2/release?release-group=" + releaseGroupId.ToString(CultureInfo.InvariantCulture); } else { @@ -88,7 +93,9 @@ namespace MediaBrowser.Providers.Music if (!string.IsNullOrWhiteSpace(artistMusicBrainzId)) { - url = string.Format("/ws/2/release/?query=\"{0}\" AND arid:{1}", + url = string.Format( + CultureInfo.InvariantCulture, + "/ws/2/release/?query=\"{0}\" AND arid:{1}", WebUtility.UrlEncode(searchInfo.Name), artistMusicBrainzId); } @@ -97,7 +104,9 @@ namespace MediaBrowser.Providers.Music // I'm sure there is a better way but for now it resolves search for 12" Mixes var queryName = searchInfo.Name.Replace("\"", string.Empty); - url = string.Format("/ws/2/release/?query=\"{0}\" AND artist:\"{1}\"", + url = string.Format( + CultureInfo.InvariantCulture, + "/ws/2/release/?query=\"{0}\" AND artist:\"{1}\"", WebUtility.UrlEncode(queryName), WebUtility.UrlEncode(searchInfo.GetAlbumArtist())); } @@ -106,11 +115,9 @@ namespace MediaBrowser.Providers.Music if (!string.IsNullOrWhiteSpace(url)) { using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) + using (var stream = response.Content) { - using (var stream = response.Content) - { - return GetResultsFromResponse(stream); - } + return GetResultsFromResponse(stream); } } @@ -156,6 +163,7 @@ namespace MediaBrowser.Providers.Music { result.SetProviderId(MetadataProviders.MusicBrainzAlbum, i.ReleaseId); } + if (!string.IsNullOrWhiteSpace(i.ReleaseGroupId)) { result.SetProviderId(MetadataProviders.MusicBrainzReleaseGroup, i.ReleaseGroupId); @@ -168,6 +176,7 @@ namespace MediaBrowser.Providers.Music } } + /// <inheritdoc /> public async Task<MetadataResult<MusicAlbum>> GetMetadata(AlbumInfo id, CancellationToken cancellationToken) { var releaseId = id.GetReleaseId(); @@ -238,8 +247,6 @@ namespace MediaBrowser.Providers.Music return result; } - public string Name => "MusicBrainz"; - private Task<ReleaseResult> GetReleaseResult(string artistMusicBrainId, string artistName, string albumName, CancellationToken cancellationToken) { if (!string.IsNullOrEmpty(artistMusicBrainId)) @@ -282,7 +289,9 @@ namespace MediaBrowser.Providers.Music private async Task<ReleaseResult> GetReleaseResultByArtistName(string albumName, string artistName, CancellationToken cancellationToken) { - var url = string.Format("/ws/2/release/?query=\"{0}\" AND artist:\"{1}\"", + var url = string.Format( + CultureInfo.InvariantCulture, + "/ws/2/release/?query=\"{0}\" AND artist:\"{1}\"", WebUtility.UrlEncode(albumName), WebUtility.UrlEncode(artistName)); @@ -334,6 +343,7 @@ namespace MediaBrowser.Providers.Music reader.Read(); continue; } + using (var subReader = reader.ReadSubtree()) { return ParseReleaseList(subReader).ToList(); @@ -601,7 +611,7 @@ namespace MediaBrowser.Providers.Music private async Task<string> GetReleaseIdFromReleaseGroupId(string releaseGroupId, CancellationToken cancellationToken) { - var url = string.Format("/ws/2/release?release-group={0}", releaseGroupId); + var url = "/ws/2/release?release-group=" + releaseGroupId.ToString(CultureInfo.InvariantCulture); using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) using (var stream = response.Content) @@ -637,7 +647,7 @@ namespace MediaBrowser.Providers.Music /// <returns>Task{System.String}.</returns> private async Task<string> GetReleaseGroupFromReleaseId(string releaseEntryId, CancellationToken cancellationToken) { - var url = string.Format("/ws/2/release-group/?query=reid:{0}", releaseEntryId); + var url = "/ws/2/release-group/?query=reid:" + releaseEntryId.ToString(CultureInfo.InvariantCulture); using (var response = await GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) using (var stream = response.Content) @@ -687,6 +697,7 @@ namespace MediaBrowser.Providers.Music reader.Read(); } } + return null; } } @@ -734,11 +745,15 @@ namespace MediaBrowser.Providers.Music { var options = new HttpRequestOptions { - Url = MusicBrainzBaseUrl.TrimEnd('/') + url, + Url = _musicBrainzBaseUrl.TrimEnd('/') + url, CancellationToken = cancellationToken, // MusicBrainz request a contact email address is supplied, as comment, in user agent field: // https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting#User-Agent - UserAgent = string.Format("{0} ( {1} )", _appHost.ApplicationUserAgent, _appHost.ApplicationUserAgentAddress), + UserAgent = string.Format( + CultureInfo.InvariantCulture, + "{0} ( {1} )", + _appHost.ApplicationUserAgent, + _appHost.ApplicationUserAgentAddress), BufferContent = false }; @@ -768,7 +783,7 @@ namespace MediaBrowser.Providers.Music while (attempts < MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable); // Log error if unable to query MB database due to throttling - if (attempts == MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable ) + if (attempts == MusicBrainzQueryAttempts && response.StatusCode == HttpStatusCode.ServiceUnavailable) { _logger.LogError("GetMusicBrainzResponse: 503 Service Unavailable (throttled) response received {0} times whilst requesting {1}", attempts, options.Url); } @@ -776,8 +791,7 @@ namespace MediaBrowser.Providers.Music return response; } - public int Order => 0; - + /// <inheritdoc /> public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) { throw new NotImplementedException(); diff --git a/MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs b/MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs index 728f7731a..5d675392c 100644 --- a/MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs +++ b/MediaBrowser.Providers/Music/MusicBrainzArtistProvider.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Net; @@ -18,25 +19,19 @@ namespace MediaBrowser.Providers.Music { public class MusicBrainzArtistProvider : IRemoteMetadataProvider<MusicArtist, ArtistInfo> { - public MusicBrainzArtistProvider() - { - - } - + /// <inheritdoc /> public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(ArtistInfo searchInfo, CancellationToken cancellationToken) { var musicBrainzId = searchInfo.GetMusicBrainzArtistId(); if (!string.IsNullOrWhiteSpace(musicBrainzId)) { - var url = string.Format("/ws/2/artist/?query=arid:{0}", musicBrainzId); + var url = "/ws/2/artist/?query=arid:{0}" + musicBrainzId.ToString(CultureInfo.InvariantCulture); using (var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) + using (var stream = response.Content) { - using (var stream = response.Content) - { - return GetResultsFromResponse(stream); - } + return GetResultsFromResponse(stream); } } else @@ -47,15 +42,13 @@ namespace MediaBrowser.Providers.Music var url = string.Format("/ws/2/artist/?query=\"{0}\"&dismax=true", UrlEncode(nameToSearch)); using (var response = await MusicBrainzAlbumProvider.Current.GetMusicBrainzResponse(url, cancellationToken).ConfigureAwait(false)) + using (var stream = response.Content) { - using (var stream = response.Content) - { - var results = GetResultsFromResponse(stream).ToList(); + var results = GetResultsFromResponse(stream).ToList(); - if (results.Count > 0) - { - return results; - } + if (results.Count > 0) + { + return results; } } diff --git a/MediaBrowser.Providers/Music/MusicExternalIds.cs b/MediaBrowser.Providers/Music/MusicExternalIds.cs index 614de11ca..585c98af9 100644 --- a/MediaBrowser.Providers/Music/MusicExternalIds.cs +++ b/MediaBrowser.Providers/Music/MusicExternalIds.cs @@ -7,99 +7,112 @@ namespace MediaBrowser.Providers.Music { public class MusicBrainzReleaseGroupExternalId : IExternalId { + /// <inheritdoc /> public string Name => "MusicBrainz Release Group"; + /// <inheritdoc /> public string Key => MetadataProviders.MusicBrainzReleaseGroup.ToString(); + /// <inheritdoc /> public string UrlFormatString => "https://musicbrainz.org/release-group/{0}"; + /// <inheritdoc /> public bool Supports(IHasProviderIds item) - { - return item is Audio || item is MusicAlbum; - } + => item is Audio || item is MusicAlbum; } public class MusicBrainzAlbumArtistExternalId : IExternalId { + /// <inheritdoc /> public string Name => "MusicBrainz Album Artist"; + /// <inheritdoc /> public string Key => MetadataProviders.MusicBrainzAlbumArtist.ToString(); + /// <inheritdoc /> public string UrlFormatString => "https://musicbrainz.org/artist/{0}"; + /// <inheritdoc /> public bool Supports(IHasProviderIds item) - { - return item is Audio; - } + => item is Audio; } public class MusicBrainzAlbumExternalId : IExternalId { + /// <inheritdoc /> public string Name => "MusicBrainz Album"; + /// <inheritdoc /> public string Key => MetadataProviders.MusicBrainzAlbum.ToString(); + /// <inheritdoc /> public string UrlFormatString => "https://musicbrainz.org/release/{0}"; + /// <inheritdoc /> public bool Supports(IHasProviderIds item) - { - return item is Audio || item is MusicAlbum; - } + => item is Audio || item is MusicAlbum; } public class MusicBrainzArtistExternalId : IExternalId { + /// <inheritdoc /> public string Name => "MusicBrainz"; + /// <inheritdoc /> public string Key => MetadataProviders.MusicBrainzArtist.ToString(); + /// <inheritdoc /> public string UrlFormatString => "https://musicbrainz.org/artist/{0}"; - public bool Supports(IHasProviderIds item) - { - return item is MusicArtist; - } + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is MusicArtist; } public class MusicBrainzOtherArtistExternalId : IExternalId { + /// <inheritdoc /> public string Name => "MusicBrainz Artist"; + /// <inheritdoc /> + public string Key => MetadataProviders.MusicBrainzArtist.ToString(); + /// <inheritdoc /> public string UrlFormatString => "https://musicbrainz.org/artist/{0}"; + /// <inheritdoc /> public bool Supports(IHasProviderIds item) - { - return item is Audio || item is MusicAlbum; - } + => item is Audio || item is MusicAlbum; } public class MusicBrainzTrackId : IExternalId { + /// <inheritdoc /> public string Name => "MusicBrainz Track"; + /// <inheritdoc /> public string Key => MetadataProviders.MusicBrainzTrack.ToString(); + /// <inheritdoc /> public string UrlFormatString => "https://musicbrainz.org/track/{0}"; - public bool Supports(IHasProviderIds item) - { - return item is Audio; - } + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Audio; } public class ImvdbId : IExternalId { + /// <inheritdoc /> public string Name => "IMVDb"; + /// <inheritdoc /> public string Key => "IMVDb"; + /// <inheritdoc /> public string UrlFormatString => null; + /// <inheritdoc /> public bool Supports(IHasProviderIds item) - { - return item is MusicVideo; - } + => item is MusicVideo; } } diff --git a/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs b/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs index c743ffcb0..bbf0cd8db 100644 --- a/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs +++ b/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs @@ -16,9 +16,8 @@ namespace MediaBrowser.Providers.Music ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, - IUserDataManager userDataManager, ILibraryManager libraryManager) - : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { } diff --git a/MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs b/MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs index d25ddb220..d74e91ad6 100644 --- a/MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs +++ b/MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs @@ -11,13 +11,20 @@ namespace MediaBrowser.Providers.MusicGenres { public class MusicGenreMetadataService : MetadataService<MusicGenre, ItemLookupInfo> { - protected override void MergeData(MetadataResult<MusicGenre> source, MetadataResult<MusicGenre> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) + public MusicGenreMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { - ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } - public MusicGenreMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + /// <inheritdoc /> + protected override void MergeData(MetadataResult<MusicGenre> source, MetadataResult<MusicGenre> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } } } diff --git a/MediaBrowser.Providers/People/PersonMetadataService.cs b/MediaBrowser.Providers/People/PersonMetadataService.cs index adbeb94ea..cdc3c77b7 100644 --- a/MediaBrowser.Providers/People/PersonMetadataService.cs +++ b/MediaBrowser.Providers/People/PersonMetadataService.cs @@ -11,13 +11,20 @@ namespace MediaBrowser.Providers.People { public class PersonMetadataService : MetadataService<Person, PersonLookupInfo> { - protected override void MergeData(MetadataResult<Person> source, MetadataResult<Person> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) + public PersonMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { - ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } - public PersonMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + /// <inheritdoc /> + protected override void MergeData(MetadataResult<Person> source, MetadataResult<Person> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } } } diff --git a/MediaBrowser.Providers/People/TvdbPersonImageProvider.cs b/MediaBrowser.Providers/People/TvdbPersonImageProvider.cs index 8c8b99e89..50476044b 100644 --- a/MediaBrowser.Providers/People/TvdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/People/TvdbPersonImageProvider.cs @@ -32,23 +32,22 @@ namespace MediaBrowser.Providers.People _tvDbClientManager = tvDbClientManager; } - public string Name => ProviderName; + /// <inheritdoc /> + public string Name => "TheTVDB"; - public static string ProviderName => "TheTVDB"; + /// <inheritdoc /> + public int Order => 1; - public bool Supports(BaseItem item) - { - return item is Person; - } + /// <inheritdoc /> + public bool Supports(BaseItem item) => item is Person; + /// <inheritdoc /> public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { - return new List<ImageType> - { - ImageType.Primary - }; + yield return ImageType.Primary; } + /// <inheritdoc /> public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken) { var seriesWithPerson = _libraryManager.GetItemList(new InternalItemsQuery @@ -104,8 +103,7 @@ namespace MediaBrowser.Providers.People } } - public int Order => 1; - + /// <inheritdoc /> public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClient.GetResponse(new HttpRequestOptions diff --git a/MediaBrowser.Providers/Photos/PhotoAlbumMetadataService.cs b/MediaBrowser.Providers/Photos/PhotoAlbumMetadataService.cs index 993581cca..845404dfb 100644 --- a/MediaBrowser.Providers/Photos/PhotoAlbumMetadataService.cs +++ b/MediaBrowser.Providers/Photos/PhotoAlbumMetadataService.cs @@ -11,13 +11,20 @@ namespace MediaBrowser.Providers.Photos { public class PhotoAlbumMetadataService : MetadataService<PhotoAlbum, ItemLookupInfo> { - protected override void MergeData(MetadataResult<PhotoAlbum> source, MetadataResult<PhotoAlbum> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) + public PhotoAlbumMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { - ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } - public PhotoAlbumMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + /// <inheritdoc /> + protected override void MergeData(MetadataResult<PhotoAlbum> source, MetadataResult<PhotoAlbum> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } } } diff --git a/MediaBrowser.Providers/Photos/PhotoMetadataService.cs b/MediaBrowser.Providers/Photos/PhotoMetadataService.cs index b739c5765..5d6ff8814 100644 --- a/MediaBrowser.Providers/Photos/PhotoMetadataService.cs +++ b/MediaBrowser.Providers/Photos/PhotoMetadataService.cs @@ -11,13 +11,20 @@ namespace MediaBrowser.Providers.Photos { public class PhotoMetadataService : MetadataService<Photo, ItemLookupInfo> { - protected override void MergeData(MetadataResult<Photo> source, MetadataResult<Photo> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) + public PhotoMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { - ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } - public PhotoMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + /// <inheritdoc /> + protected override void MergeData(MetadataResult<Photo> source, MetadataResult<Photo> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } } } diff --git a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs index 30ce5c64c..32bd6c282 100644 --- a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs +++ b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs @@ -13,11 +13,30 @@ namespace MediaBrowser.Providers.Playlists { public class PlaylistMetadataService : MetadataService<Playlist, ItemLookupInfo> { - protected override IList<BaseItem> GetChildrenForMetadataUpdates(Playlist item) + public PlaylistMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { - return item.GetLinkedChildren(); } + /// <inheritdoc /> + protected override bool EnableUpdatingGenresFromChildren => true; + + /// <inheritdoc /> + protected override bool EnableUpdatingOfficialRatingFromChildren => true; + + /// <inheritdoc /> + protected override bool EnableUpdatingStudiosFromChildren => true; + + /// <inheritdoc /> + protected override IList<BaseItem> GetChildrenForMetadataUpdates(Playlist item) + => item.GetLinkedChildren(); + + /// <inheritdoc /> protected override void MergeData(MetadataResult<Playlist> source, MetadataResult<Playlist> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); @@ -32,15 +51,5 @@ namespace MediaBrowser.Providers.Playlists targetItem.Shares = sourceItem.Shares; } } - - public PlaylistMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) - { - } - - protected override bool EnableUpdatingGenresFromChildren => true; - - protected override bool EnableUpdatingOfficialRatingFromChildren => true; - - protected override bool EnableUpdatingStudiosFromChildren => true; } } diff --git a/MediaBrowser.Providers/Studios/StudioMetadataService.cs b/MediaBrowser.Providers/Studios/StudioMetadataService.cs index 78204dccf..6fa30c753 100644 --- a/MediaBrowser.Providers/Studios/StudioMetadataService.cs +++ b/MediaBrowser.Providers/Studios/StudioMetadataService.cs @@ -11,13 +11,19 @@ namespace MediaBrowser.Providers.Studios { public class StudioMetadataService : MetadataService<Studio, ItemLookupInfo> { - protected override void MergeData(MetadataResult<Studio> source, MetadataResult<Studio> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) + public StudioMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { - ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } - public StudioMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + /// <inheritdoc /> + protected override void MergeData(MetadataResult<Studio> source, MetadataResult<Studio> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } } } diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index b4a4c36e5..37d1230e2 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -19,6 +19,7 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using Microsoft.Extensions.Logging; +using static MediaBrowser.Model.IO.StreamDefaults; namespace MediaBrowser.Providers.Subtitles { @@ -30,8 +31,6 @@ namespace MediaBrowser.Providers.Subtitles private readonly ILibraryMonitor _monitor; private readonly IMediaSourceManager _mediaSourceManager; - public event EventHandler<SubtitleDownloadFailureEventArgs> SubtitleDownloadFailure; - private ILocalizationManager _localization; public SubtitleManager( @@ -48,17 +47,18 @@ namespace MediaBrowser.Providers.Subtitles _localization = localizationManager; } + /// <inheritdoc /> + public event EventHandler<SubtitleDownloadFailureEventArgs> SubtitleDownloadFailure; + + /// <inheritdoc /> public void AddParts(IEnumerable<ISubtitleProvider> subtitleProviders) { _subtitleProviders = subtitleProviders - .OrderBy(i => - { - var hasOrder = i as IHasOrder; - return hasOrder == null ? 0 : hasOrder.Order; - }) + .OrderBy(i => i is IHasOrder hasOrder ? hasOrder.Order : 0) .ToArray(); } + /// <inheritdoc /> public async Task<RemoteSubtitleInfo[]> SearchSubtitles(SubtitleSearchRequest request, CancellationToken cancellationToken) { if (request.Language != null) @@ -104,7 +104,7 @@ namespace MediaBrowser.Providers.Subtitles _logger.LogError(ex, "Error downloading subtitles from {Provider}", provider.Name); } } - return new RemoteSubtitleInfo[] { }; + return Array.Empty<RemoteSubtitleInfo>(); } var tasks = providers.Select(async i => @@ -120,7 +120,7 @@ namespace MediaBrowser.Providers.Subtitles catch (Exception ex) { _logger.LogError(ex, "Error downloading subtitles from {0}", i.Name); - return new RemoteSubtitleInfo[] { }; + return Array.Empty<RemoteSubtitleInfo>(); } }); @@ -129,6 +129,7 @@ namespace MediaBrowser.Providers.Subtitles return results.SelectMany(i => i).ToArray(); } + /// <inheritdoc /> public Task DownloadSubtitles(Video video, string subtitleId, CancellationToken cancellationToken) { var libraryOptions = BaseItem.LibraryManager.GetLibraryOptions(video); @@ -136,7 +137,9 @@ namespace MediaBrowser.Providers.Subtitles return DownloadSubtitles(video, libraryOptions, subtitleId, cancellationToken); } - public async Task DownloadSubtitles(Video video, + /// <inheritdoc /> + public async Task DownloadSubtitles( + Video video, LibraryOptions libraryOptions, string subtitleId, CancellationToken cancellationToken) @@ -151,31 +154,29 @@ namespace MediaBrowser.Providers.Subtitles var response = await GetRemoteSubtitles(subtitleId, cancellationToken).ConfigureAwait(false); using (var stream = response.Stream) + using (var memoryStream = new MemoryStream()) { - using (var memoryStream = new MemoryStream()) - { - await stream.CopyToAsync(memoryStream).ConfigureAwait(false); - memoryStream.Position = 0; + await stream.CopyToAsync(memoryStream).ConfigureAwait(false); + memoryStream.Position = 0; - var savePaths = new List<string>(); - var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant(); + var savePaths = new List<string>(); + var saveFileName = Path.GetFileNameWithoutExtension(video.Path) + "." + response.Language.ToLowerInvariant(); - if (response.IsForced) - { - saveFileName += ".forced"; - } + if (response.IsForced) + { + saveFileName += ".forced"; + } - saveFileName += "." + response.Format.ToLowerInvariant(); + saveFileName += "." + response.Format.ToLowerInvariant(); - if (saveInMediaFolder) - { - savePaths.Add(Path.Combine(video.ContainingFolderPath, saveFileName)); - } + if (saveInMediaFolder) + { + savePaths.Add(Path.Combine(video.ContainingFolderPath, saveFileName)); + } - savePaths.Add(Path.Combine(video.GetInternalMetadataPath(), saveFileName)); + savePaths.Add(Path.Combine(video.GetInternalMetadataPath(), saveFileName)); - await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false); - } + await TrySaveToFiles(memoryStream, savePaths).ConfigureAwait(false); } } catch (RateLimitExceededException) @@ -209,7 +210,7 @@ namespace MediaBrowser.Providers.Subtitles { Directory.CreateDirectory(Path.GetDirectoryName(savePath)); - using (var fs = _fileSystem.GetFileStream(savePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) + using (var fs = new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.Read, DefaultFileStreamBufferSize, true)) { await stream.CopyToAsync(fs).ConfigureAwait(false); } @@ -237,11 +238,12 @@ namespace MediaBrowser.Providers.Subtitles } } + /// <inheritdoc /> public Task<RemoteSubtitleInfo[]> SearchSubtitles(Video video, string language, bool? isPerfectMatch, CancellationToken cancellationToken) { if (video.VideoType != VideoType.VideoFile) { - return Task.FromResult(new RemoteSubtitleInfo[] { }); + return Task.FromResult(Array.Empty<RemoteSubtitleInfo>()); } VideoContentType mediaType; @@ -257,7 +259,7 @@ namespace MediaBrowser.Providers.Subtitles else { // These are the only supported types - return Task.FromResult(new RemoteSubtitleInfo[] { }); + return Task.FromResult(Array.Empty<RemoteSubtitleInfo>()); } var request = new SubtitleSearchRequest @@ -274,9 +276,7 @@ namespace MediaBrowser.Providers.Subtitles IsPerfectMatch = isPerfectMatch ?? false }; - var episode = video as Episode; - - if (episode != null) + if (video is Episode episode) { request.IndexNumberEnd = episode.IndexNumberEnd; request.SeriesName = episode.SeriesName; @@ -303,6 +303,7 @@ namespace MediaBrowser.Providers.Subtitles return _subtitleProviders.First(i => string.Equals(id, GetProviderId(i.Name))); } + /// <inheritdoc /> public Task DeleteSubtitles(BaseItem item, int index) { var stream = _mediaSourceManager.GetMediaStreams(new MediaStreamQuery @@ -328,16 +329,18 @@ namespace MediaBrowser.Providers.Subtitles return item.RefreshMetadata(CancellationToken.None); } + /// <inheritdoc /> public Task<SubtitleResponse> GetRemoteSubtitles(string id, CancellationToken cancellationToken) { var parts = id.Split(new[] { '_' }, 2); - var provider = GetProvider(parts.First()); + var provider = GetProvider(parts[0]); id = parts.Last(); return provider.GetSubtitles(id, cancellationToken); } + /// <inheritdoc /> public SubtitleProviderInfo[] GetSupportedProviders(BaseItem video) { VideoContentType mediaType; @@ -353,7 +356,7 @@ namespace MediaBrowser.Providers.Subtitles else { // These are the only supported types - return new SubtitleProviderInfo[] { }; + return Array.Empty<SubtitleProviderInfo>(); } return _subtitleProviders diff --git a/MediaBrowser.Providers/TV/DummySeasonProvider.cs b/MediaBrowser.Providers/TV/DummySeasonProvider.cs index bd58fc5de..4a6676cb9 100644 --- a/MediaBrowser.Providers/TV/DummySeasonProvider.cs +++ b/MediaBrowser.Providers/TV/DummySeasonProvider.cs @@ -16,17 +16,17 @@ namespace MediaBrowser.Providers.TV { public class DummySeasonProvider { - private readonly IServerConfigurationManager _config; private readonly ILogger _logger; private readonly ILocalizationManager _localization; private readonly ILibraryManager _libraryManager; private readonly IFileSystem _fileSystem; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - public DummySeasonProvider(IServerConfigurationManager config, ILogger logger, ILocalizationManager localization, ILibraryManager libraryManager, IFileSystem fileSystem) + public DummySeasonProvider( + ILogger logger, + ILocalizationManager localization, + ILibraryManager libraryManager, + IFileSystem fileSystem) { - _config = config; _logger = logger; _localization = localization; _libraryManager = libraryManager; @@ -127,9 +127,22 @@ namespace MediaBrowser.Providers.TV bool isVirtualItem, CancellationToken cancellationToken) { - var seasonName = seasonNumber == 0 ? - _libraryManager.GetLibraryOptions(series).SeasonZeroDisplayName : - (seasonNumber.HasValue ? string.Format(_localization.GetLocalizedString("NameSeasonNumber"), seasonNumber.Value.ToString(_usCulture)) : _localization.GetLocalizedString("NameSeasonUnknown")); + string seasonName; + if (seasonNumber == null) + { + seasonName = _localization.GetLocalizedString("NameSeasonUnknown"); + } + else if (seasonNumber == 0) + { + seasonName = _libraryManager.GetLibraryOptions(series).SeasonZeroDisplayName; + } + else + { + seasonName = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("NameSeasonNumber"), + seasonNumber.Value); + } _logger.LogInformation("Creating Season {0} entry for {1}", seasonName, series.Name); @@ -137,7 +150,9 @@ namespace MediaBrowser.Providers.TV { Name = seasonName, IndexNumber = seasonNumber, - Id = _libraryManager.GetNewItemId((series.Id + (seasonNumber ?? -1).ToString(_usCulture) + seasonName), typeof(Season)), + Id = _libraryManager.GetNewItemId( + series.Id + (seasonNumber ?? -1).ToString(CultureInfo.InvariantCulture) + seasonName, + typeof(Season)), IsVirtualItem = isVirtualItem, SeriesId = series.Id, SeriesName = series.Name @@ -147,7 +162,7 @@ namespace MediaBrowser.Providers.TV series.AddChild(season, cancellationToken); - await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), cancellationToken).ConfigureAwait(false); + await season.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false); return season; } diff --git a/MediaBrowser.Providers/TV/EpisodeMetadataService.cs b/MediaBrowser.Providers/TV/EpisodeMetadataService.cs index 5993f615a..89615f406 100644 --- a/MediaBrowser.Providers/TV/EpisodeMetadataService.cs +++ b/MediaBrowser.Providers/TV/EpisodeMetadataService.cs @@ -12,6 +12,17 @@ namespace MediaBrowser.Providers.TV { public class EpisodeMetadataService : MetadataService<Episode, EpisodeInfo> { + public EpisodeMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) + { + } + + /// <inheritdoc /> protected override ItemUpdateType BeforeSaveInternal(Episode item, bool isFullRefresh, ItemUpdateType currentUpdateType) { var updateType = base.BeforeSaveInternal(item, isFullRefresh, currentUpdateType); @@ -54,6 +65,7 @@ namespace MediaBrowser.Providers.TV return updateType; } + /// <inheritdoc /> protected override void MergeData(MetadataResult<Episode> source, MetadataResult<Episode> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); @@ -81,9 +93,5 @@ namespace MediaBrowser.Providers.TV targetItem.IndexNumberEnd = sourceItem.IndexNumberEnd; } } - - public EpisodeMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) - { - } } } diff --git a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs b/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs index 752c0941d..e72df50de 100644 --- a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs +++ b/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs @@ -19,6 +19,8 @@ namespace MediaBrowser.Providers.TV { public class MissingEpisodeProvider { + private const double UnairedEpisodeThresholdDays = 2; + private readonly IServerConfigurationManager _config; private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; @@ -26,9 +28,6 @@ namespace MediaBrowser.Providers.TV private readonly IFileSystem _fileSystem; private readonly TvDbClientManager _tvDbClientManager; - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - private const double UnairedEpisodeThresholdDays = 2; - public MissingEpisodeProvider( ILogger logger, IServerConfigurationManager config, @@ -61,7 +60,7 @@ namespace MediaBrowser.Providers.TV DateTime.TryParse(i.FirstAired, out var firstAired); var seasonNumber = i.AiredSeason.GetValueOrDefault(-1); var episodeNumber = i.AiredEpisodeNumber.GetValueOrDefault(-1); - return (seasonNumber: seasonNumber, episodeNumber: episodeNumber, firstAired: firstAired); + return (seasonNumber, episodeNumber, firstAired); }) .Where(i => i.seasonNumber != -1 && i.episodeNumber != -1) .OrderBy(i => i.seasonNumber) @@ -247,8 +246,9 @@ namespace MediaBrowser.Providers.TV /// </summary> /// <param name="allRecursiveChildren"></param> /// <param name="episodeLookup">The episode lookup.</param> - /// <returns>Task{System.Boolean}.</returns> - private bool RemoveObsoleteOrMissingSeasons(IList<BaseItem> allRecursiveChildren, + /// <returns><see cref="bool" />.</returns> + private bool RemoveObsoleteOrMissingSeasons( + IList<BaseItem> allRecursiveChildren, IEnumerable<(int seasonNumber, int episodeNumber, DateTime firstAired)> episodeLookup) { var existingSeasons = allRecursiveChildren.OfType<Season>().ToList(); @@ -298,7 +298,6 @@ namespace MediaBrowser.Providers.TV _libraryManager.DeleteItem(seasonToRemove, new DeleteOptions { DeleteFileLocation = true - }, false); hasChanges = true; @@ -322,18 +321,20 @@ namespace MediaBrowser.Providers.TV if (season == null) { - var provider = new DummySeasonProvider(_config, _logger, _localization, _libraryManager, _fileSystem); + var provider = new DummySeasonProvider(_logger, _localization, _libraryManager, _fileSystem); season = await provider.AddSeason(series, seasonNumber, true, cancellationToken).ConfigureAwait(false); } - var name = $"Episode {episodeNumber.ToString(_usCulture)}"; + var name = "Episode " + episodeNumber.ToString(CultureInfo.InvariantCulture); var episode = new Episode { Name = name, IndexNumber = episodeNumber, ParentIndexNumber = seasonNumber, - Id = _libraryManager.GetNewItemId(series.Id + seasonNumber.ToString(_usCulture) + name, typeof(Episode)), + Id = _libraryManager.GetNewItemId( + series.Id + seasonNumber.ToString(CultureInfo.InvariantCulture) + name, + typeof(Episode)), IsVirtualItem = true, SeasonId = season?.Id ?? Guid.Empty, SeriesId = series.Id @@ -341,7 +342,7 @@ namespace MediaBrowser.Providers.TV season.AddChild(episode, cancellationToken); - await episode.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem)), cancellationToken).ConfigureAwait(false); + await episode.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false); } /// <summary> @@ -351,7 +352,7 @@ namespace MediaBrowser.Providers.TV /// <param name="seasonCounts"></param> /// <param name="episodeTuple"></param> /// <returns>Episode.</returns> - private Episode GetExistingEpisode(IList<Episode> existingEpisodes, IReadOnlyDictionary<int, int> seasonCounts, (int seasonNumber, int episodeNumber, DateTime firstAired) episodeTuple) + private Episode GetExistingEpisode(IEnumerable<Episode> existingEpisodes, IReadOnlyDictionary<int, int> seasonCounts, (int seasonNumber, int episodeNumber, DateTime firstAired) episodeTuple) { var seasonNumber = episodeTuple.seasonNumber; var episodeNumber = episodeTuple.episodeNumber; @@ -380,9 +381,6 @@ namespace MediaBrowser.Providers.TV } private Episode GetExistingEpisode(IEnumerable<Episode> existingEpisodes, int season, int episode) - { - return existingEpisodes - .FirstOrDefault(i => i.ParentIndexNumber == season && i.ContainsEpisodeNumber(episode)); - } + => existingEpisodes.FirstOrDefault(i => i.ParentIndexNumber == season && i.ContainsEpisodeNumber(episode)); } } diff --git a/MediaBrowser.Providers/TV/SeasonMetadataService.cs b/MediaBrowser.Providers/TV/SeasonMetadataService.cs index c575057de..0672f886a 100644 --- a/MediaBrowser.Providers/TV/SeasonMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeasonMetadataService.cs @@ -15,6 +15,17 @@ namespace MediaBrowser.Providers.TV { public class SeasonMetadataService : MetadataService<Season, SeasonInfo> { + public SeasonMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) + { + } + + /// <inheritdoc /> protected override ItemUpdateType BeforeSaveInternal(Season item, bool isFullRefresh, ItemUpdateType currentUpdateType) { var updateType = base.BeforeSaveInternal(item, isFullRefresh, currentUpdateType); @@ -54,13 +65,14 @@ namespace MediaBrowser.Providers.TV return updateType; } + /// <inheritdoc /> protected override bool EnableUpdatingPremiereDateFromChildren => true; + /// <inheritdoc /> protected override IList<BaseItem> GetChildrenForMetadataUpdates(Season item) - { - return item.GetEpisodes(); - } + => item.GetEpisodes(); + /// <inheritdoc /> protected override ItemUpdateType UpdateMetadataFromChildren(Season item, IList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType) { var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType); @@ -73,6 +85,7 @@ namespace MediaBrowser.Providers.TV return updateType; } + /// <inheritdoc /> protected override void MergeData(MetadataResult<Season> source, MetadataResult<Season> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); @@ -90,9 +103,5 @@ namespace MediaBrowser.Providers.TV return ItemUpdateType.None; } - - public SeasonMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) - { - } } } diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index d5b0b6fd8..e9e633ce7 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -24,22 +24,21 @@ namespace MediaBrowser.Providers.TV ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, - IUserDataManager userDataManager, ILibraryManager libraryManager, ILocalizationManager localization, - TvDbClientManager tvDbClientManager - ) - : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + TvDbClientManager tvDbClientManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { _localization = localization; _tvDbClientManager = tvDbClientManager; } + /// <inheritdoc /> protected override async Task AfterMetadataRefresh(Series item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) { await base.AfterMetadataRefresh(item, refreshOptions, cancellationToken).ConfigureAwait(false); - var seasonProvider = new DummySeasonProvider(ServerConfigurationManager, Logger, _localization, LibraryManager, FileSystem); + var seasonProvider = new DummySeasonProvider(Logger, _localization, LibraryManager, FileSystem); await seasonProvider.Run(item, cancellationToken).ConfigureAwait(false); // TODO why does it not register this itself omg @@ -60,6 +59,7 @@ namespace MediaBrowser.Providers.TV } } + /// <inheritdoc /> protected override bool IsFullLocalMetadata(Series item) { if (string.IsNullOrWhiteSpace(item.Overview)) @@ -73,6 +73,7 @@ namespace MediaBrowser.Providers.TV return base.IsFullLocalMetadata(item); } + /// <inheritdoc /> protected override void MergeData(MetadataResult<Series> source, MetadataResult<Series> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); diff --git a/MediaBrowser.Providers/TV/TvExternalIds.cs b/MediaBrowser.Providers/TV/TvExternalIds.cs index 3f889fbbe..646dae3e0 100644 --- a/MediaBrowser.Providers/TV/TvExternalIds.cs +++ b/MediaBrowser.Providers/TV/TvExternalIds.cs @@ -7,57 +7,62 @@ namespace MediaBrowser.Providers.TV { public class Zap2ItExternalId : IExternalId { + /// <inheritdoc /> public string Name => "Zap2It"; + /// <inheritdoc /> public string Key => MetadataProviders.Zap2It.ToString(); + /// <inheritdoc /> public string UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}"; - public bool Supports(IHasProviderIds item) - { - return item is Series; - } + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Series; } public class TvdbExternalId : IExternalId { + /// <inheritdoc /> public string Name => "TheTVDB"; + /// <inheritdoc /> public string Key => MetadataProviders.Tvdb.ToString(); + /// <inheritdoc /> public string UrlFormatString => TvdbUtils.TvdbBaseUrl + "?tab=series&id={0}"; - public bool Supports(IHasProviderIds item) - { - return item is Series; - } + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Series; + } public class TvdbSeasonExternalId : IExternalId { + /// <inheritdoc /> public string Name => "TheTVDB"; + /// <inheritdoc /> public string Key => MetadataProviders.Tvdb.ToString(); + /// <inheritdoc /> public string UrlFormatString => null; - public bool Supports(IHasProviderIds item) - { - return item is Season; - } + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Season; } public class TvdbEpisodeExternalId : IExternalId { + /// <inheritdoc /> public string Name => "TheTVDB"; + /// <inheritdoc /> public string Key => MetadataProviders.Tvdb.ToString(); + /// <inheritdoc /> public string UrlFormatString => TvdbUtils.TvdbBaseUrl + "?tab=episode&id={0}"; - public bool Supports(IHasProviderIds item) - { - return item is Episode; - } + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) => item is Episode; } } diff --git a/MediaBrowser.Providers/Tmdb/Models/Search/MovieResult.cs b/MediaBrowser.Providers/Tmdb/Models/Search/MovieResult.cs index 25a211fa8..245162728 100644 --- a/MediaBrowser.Providers/Tmdb/Models/Search/MovieResult.cs +++ b/MediaBrowser.Providers/Tmdb/Models/Search/MovieResult.cs @@ -2,64 +2,64 @@ namespace MediaBrowser.Providers.Tmdb.Models.Search { public class MovieResult { - /// <summary> - /// Gets or sets a value indicating whether this <see cref="TmdbMovieSearchResult" /> is adult. - /// </summary> - /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value> - public bool Adult { get; set; } - /// <summary> - /// Gets or sets the backdrop_path. - /// </summary> - /// <value>The backdrop_path.</value> - public string Backdrop_Path { get; set; } - /// <summary> - /// Gets or sets the id. - /// </summary> - /// <value>The id.</value> - public int Id { get; set; } - /// <summary> - /// Gets or sets the original_title. - /// </summary> - /// <value>The original_title.</value> - public string Original_Title { get; set; } - /// <summary> - /// Gets or sets the original_name. - /// </summary> - /// <value>The original_name.</value> - public string Original_Name { get; set; } - /// <summary> - /// Gets or sets the release_date. - /// </summary> - /// <value>The release_date.</value> - public string Release_Date { get; set; } - /// <summary> - /// Gets or sets the poster_path. - /// </summary> - /// <value>The poster_path.</value> - public string Poster_Path { get; set; } - /// <summary> - /// Gets or sets the popularity. - /// </summary> - /// <value>The popularity.</value> - public double Popularity { get; set; } - /// <summary> - /// Gets or sets the title. - /// </summary> - /// <value>The title.</value> - public string Title { get; set; } - /// <summary> - /// Gets or sets the vote_average. - /// </summary> - /// <value>The vote_average.</value> - public double Vote_Average { get; set; } - /// <summary> - /// For collection search results - /// </summary> - public string Name { get; set; } - /// <summary> - /// Gets or sets the vote_count. - /// </summary> - /// <value>The vote_count.</value> - public int Vote_Count { get; set; } + /// <summary> + /// Gets or sets a value indicating whether this <see cref="MovieResult" /> is adult. + /// </summary> + /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value> + public bool Adult { get; set; } + /// <summary> + /// Gets or sets the backdrop_path. + /// </summary> + /// <value>The backdrop_path.</value> + public string Backdrop_Path { get; set; } + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + public int Id { get; set; } + /// <summary> + /// Gets or sets the original_title. + /// </summary> + /// <value>The original_title.</value> + public string Original_Title { get; set; } + /// <summary> + /// Gets or sets the original_name. + /// </summary> + /// <value>The original_name.</value> + public string Original_Name { get; set; } + /// <summary> + /// Gets or sets the release_date. + /// </summary> + /// <value>The release_date.</value> + public string Release_Date { get; set; } + /// <summary> + /// Gets or sets the poster_path. + /// </summary> + /// <value>The poster_path.</value> + public string Poster_Path { get; set; } + /// <summary> + /// Gets or sets the popularity. + /// </summary> + /// <value>The popularity.</value> + public double Popularity { get; set; } + /// <summary> + /// Gets or sets the title. + /// </summary> + /// <value>The title.</value> + public string Title { get; set; } + /// <summary> + /// Gets or sets the vote_average. + /// </summary> + /// <value>The vote_average.</value> + public double Vote_Average { get; set; } + /// <summary> + /// For collection search results + /// </summary> + public string Name { get; set; } + /// <summary> + /// Gets or sets the vote_count. + /// </summary> + /// <value>The vote_count.</value> + public int Vote_Count { get; set; } } } diff --git a/MediaBrowser.Providers/Users/UserMetadataService.cs b/MediaBrowser.Providers/Users/UserMetadataService.cs index 024c2f959..9c2e27816 100644 --- a/MediaBrowser.Providers/Users/UserMetadataService.cs +++ b/MediaBrowser.Providers/Users/UserMetadataService.cs @@ -11,13 +11,20 @@ namespace MediaBrowser.Providers.Users { public class UserMetadataService : MetadataService<User, ItemLookupInfo> { - protected override void MergeData(MetadataResult<User> source, MetadataResult<User> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) + public UserMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { - ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } - public UserMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + /// <inheritdoc /> + protected override void MergeData(MetadataResult<User> source, MetadataResult<User> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } } } diff --git a/MediaBrowser.Providers/Videos/VideoMetadataService.cs b/MediaBrowser.Providers/Videos/VideoMetadataService.cs index f5f5b8971..996af0368 100644 --- a/MediaBrowser.Providers/Videos/VideoMetadataService.cs +++ b/MediaBrowser.Providers/Videos/VideoMetadataService.cs @@ -11,16 +11,24 @@ namespace MediaBrowser.Providers.Videos { public class VideoMetadataService : MetadataService<Video, ItemLookupInfo> { + public VideoMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) + { + } + + /// <inheritdoc /> // Make sure the type-specific services get picked first public override int Order => 10; + /// <inheritdoc /> protected override void MergeData(MetadataResult<Video> source, MetadataResult<Video> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } - - public VideoMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) - { - } } } diff --git a/MediaBrowser.Providers/Years/YearMetadataService.cs b/MediaBrowser.Providers/Years/YearMetadataService.cs index b94494253..414795e35 100644 --- a/MediaBrowser.Providers/Years/YearMetadataService.cs +++ b/MediaBrowser.Providers/Years/YearMetadataService.cs @@ -11,13 +11,20 @@ namespace MediaBrowser.Providers.Years { public class YearMetadataService : MetadataService<Year, ItemLookupInfo> { - protected override void MergeData(MetadataResult<Year> source, MetadataResult<Year> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) + public YearMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { - ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } - public YearMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + /// <inheritdoc /> + protected override void MergeData(MetadataResult<Year> source, MetadataResult<Year> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { + ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); } } } diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index a43949367..1d256d689 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -16,7 +16,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj index 1ca9e43bb..ecc61a8d8 100644 --- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj +++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj @@ -10,7 +10,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> diff --git a/MediaBrowser.sln b/MediaBrowser.sln index dd4e9f8a6..27c8c1668 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -33,8 +33,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RSSDP", "RSSDP\RSSDP.csproj EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Dlna", "Emby.Dlna\Emby.Dlna.csproj", "{805844AB-E92F-45E6-9D99-4F6D48D129A5}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mono.Nat", "Mono.Nat\Mono.Nat.csproj", "{CB7F2326-6497-4A3D-BA03-48513B17A7BE}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Notifications", "Emby.Notifications\Emby.Notifications.csproj", "{2E030C33-6923-4530-9E54-FA29FA6AD1A9}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Naming", "Emby.Naming\Emby.Naming.csproj", "{E5AF7B26-2239-4CE0-B477-0AA2018EDAA2}" @@ -59,6 +57,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Common.Tests", "te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.MediaEncoding.Tests", "tests\Jellyfin.MediaEncoding.Tests\Jellyfin.MediaEncoding.Tests.csproj", "{28464062-0939-4AA7-9F7B-24DDDA61A7C0}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Naming.Tests", "tests\Jellyfin.Naming.Tests\Jellyfin.Naming.Tests.csproj", "{3998657B-1CCC-49DD-A19F-275DC8495F57}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -129,10 +129,6 @@ Global {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Debug|Any CPU.Build.0 = Debug|Any CPU {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Release|Any CPU.ActiveCfg = Release|Any CPU {805844AB-E92F-45E6-9D99-4F6D48D129A5}.Release|Any CPU.Build.0 = Release|Any CPU - {CB7F2326-6497-4A3D-BA03-48513B17A7BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CB7F2326-6497-4A3D-BA03-48513B17A7BE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CB7F2326-6497-4A3D-BA03-48513B17A7BE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CB7F2326-6497-4A3D-BA03-48513B17A7BE}.Release|Any CPU.Build.0 = Release|Any CPU {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Debug|Any CPU.Build.0 = Debug|Any CPU {2E030C33-6923-4530-9E54-FA29FA6AD1A9}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -165,6 +161,10 @@ Global {28464062-0939-4AA7-9F7B-24DDDA61A7C0}.Debug|Any CPU.Build.0 = Debug|Any CPU {28464062-0939-4AA7-9F7B-24DDDA61A7C0}.Release|Any CPU.ActiveCfg = Release|Any CPU {28464062-0939-4AA7-9F7B-24DDDA61A7C0}.Release|Any CPU.Build.0 = Release|Any CPU + {3998657B-1CCC-49DD-A19F-275DC8495F57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3998657B-1CCC-49DD-A19F-275DC8495F57}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3998657B-1CCC-49DD-A19F-275DC8495F57}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3998657B-1CCC-49DD-A19F-275DC8495F57}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -193,5 +193,6 @@ Global GlobalSection(NestedProjects) = preSolution {DF194677-DFD3-42AF-9F75-D44D5A416478} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} {28464062-0939-4AA7-9F7B-24DDDA61A7C0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {3998657B-1CCC-49DD-A19F-275DC8495F57} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} EndGlobalSection EndGlobal diff --git a/Mono.Nat/AbstractNatDevice.cs b/Mono.Nat/AbstractNatDevice.cs deleted file mode 100644 index 1241170c1..000000000 --- a/Mono.Nat/AbstractNatDevice.cs +++ /dev/null @@ -1,55 +0,0 @@ -// -// Authors: -// Alan McGovern alan.mcgovern@gmail.com -// Ben Motmans <ben.motmans@gmail.com> -// -// Copyright (C) 2006 Alan McGovern -// Copyright (C) 2007 Ben Motmans -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; -using System.Collections.Generic; -using System.Text; -using System.Net; -using System.Threading.Tasks; - -namespace Mono.Nat -{ - public abstract class AbstractNatDevice : INatDevice - { - private DateTime lastSeen; - - protected AbstractNatDevice() - { - } - - public abstract IPAddress LocalAddress { get; } - - public DateTime LastSeen - { - get { return lastSeen; } - set { lastSeen = value; } - } - - public abstract Task CreatePortMap(Mapping mapping); - } -} diff --git a/Mono.Nat/Enums/ProtocolType.cs b/Mono.Nat/Enums/ProtocolType.cs deleted file mode 100644 index 54480598d..000000000 --- a/Mono.Nat/Enums/ProtocolType.cs +++ /dev/null @@ -1,36 +0,0 @@ -// -// Authors: -// Alan McGovern alan.mcgovern@gmail.com -// -// Copyright (C) 2006 Alan McGovern -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; - -namespace Mono.Nat -{ - public enum Protocol - { - Tcp, - Udp - } -} diff --git a/Mono.Nat/EventArgs/DeviceEventArgs.cs b/Mono.Nat/EventArgs/DeviceEventArgs.cs deleted file mode 100644 index 6358a0c29..000000000 --- a/Mono.Nat/EventArgs/DeviceEventArgs.cs +++ /dev/null @@ -1,45 +0,0 @@ -// -// Authors: -// Alan McGovern alan.mcgovern@gmail.com -// -// Copyright (C) 2006 Alan McGovern -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; - -namespace Mono.Nat -{ - public class DeviceEventArgs : EventArgs - { - private INatDevice device; - - public DeviceEventArgs(INatDevice device) - { - this.device = device; - } - - public INatDevice Device - { - get { return this.device; } - } - } -} diff --git a/Mono.Nat/INatDevice.cs b/Mono.Nat/INatDevice.cs deleted file mode 100644 index 6a1509071..000000000 --- a/Mono.Nat/INatDevice.cs +++ /dev/null @@ -1,45 +0,0 @@ -// -// Authors: -// Alan McGovern alan.mcgovern@gmail.com -// Ben Motmans <ben.motmans@gmail.com> -// -// Copyright (C) 2006 Alan McGovern -// Copyright (C) 2007 Ben Motmans -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; -using System.Collections.Generic; -using System.Text; -using System.Net; -using System.Threading.Tasks; - -namespace Mono.Nat -{ - public interface INatDevice - { - Task CreatePortMap (Mapping mapping); - - IPAddress LocalAddress { get; } - - DateTime LastSeen { get; set; } - } -} diff --git a/Mono.Nat/ISearcher.cs b/Mono.Nat/ISearcher.cs deleted file mode 100644 index 95c2f2aa9..000000000 --- a/Mono.Nat/ISearcher.cs +++ /dev/null @@ -1,46 +0,0 @@ -// -// Authors: -// Alan McGovern alan.mcgovern@gmail.com -// Ben Motmans <ben.motmans@gmail.com> -// Nicholas Terry <nick.i.terry@gmail.com> -// -// Copyright (C) 2006 Alan McGovern -// Copyright (C) 2007 Ben Motmans -// Copyright (C) 2014 Nicholas Terry -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; -using System.Collections.Generic; -using System.Text; -using System.Net.Sockets; -using System.Net; - -namespace Mono.Nat -{ - internal interface ISearcher - { - event EventHandler<DeviceEventArgs> DeviceFound; - - void Search(); - void Handle(IPAddress localAddress, byte[] response, IPEndPoint endpoint); - } -} diff --git a/Mono.Nat/Mapping.cs b/Mono.Nat/Mapping.cs deleted file mode 100644 index 5b15d4e14..000000000 --- a/Mono.Nat/Mapping.cs +++ /dev/null @@ -1,121 +0,0 @@ -// -// Authors: -// Alan McGovern alan.mcgovern@gmail.com -// Ben Motmans <ben.motmans@gmail.com> -// -// Copyright (C) 2006 Alan McGovern -// Copyright (C) 2007 Ben Motmans -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; - -namespace Mono.Nat -{ - public class Mapping - { - private string description; - private DateTime expiration; - private int lifetime; - private int privatePort; - private Protocol protocol; - private int publicPort; - - public Mapping(Protocol protocol, int privatePort, int publicPort) - : this (protocol, privatePort, publicPort, 0) - { - } - - public Mapping(Protocol protocol, int privatePort, int publicPort, int lifetime) - { - this.protocol = protocol; - this.privatePort = privatePort; - this.publicPort = publicPort; - this.lifetime = lifetime; - - if (lifetime == int.MaxValue) - this.expiration = DateTime.MaxValue; - else if (lifetime == 0) - this.expiration = DateTime.Now; - else - this.expiration = DateTime.Now.AddSeconds (lifetime); - } - - public string Description - { - get { return description; } - set { description = value; } - } - - public Protocol Protocol - { - get { return protocol; } - internal set { protocol = value; } - } - - public int PrivatePort - { - get { return privatePort; } - internal set { privatePort = value; } - } - - public int PublicPort - { - get { return publicPort; } - internal set { publicPort = value; } - } - - public int Lifetime - { - get { return lifetime; } - internal set { lifetime = value; } - } - - public DateTime Expiration - { - get { return expiration; } - internal set { expiration = value; } - } - - public bool IsExpired() - { - return expiration < DateTime.Now; - } - - public override bool Equals(object obj) - { - var other = obj as Mapping; - return other == null ? false : this.protocol == other.protocol && - this.privatePort == other.privatePort && this.publicPort == other.publicPort; - } - - public override int GetHashCode() - { - return this.protocol.GetHashCode() ^ this.privatePort.GetHashCode() ^ this.publicPort.GetHashCode(); - } - - public override string ToString() - { - return String.Format( "Protocol: {0}, Public Port: {1}, Private Port: {2}, Description: {3}, Expiration: {4}, Lifetime: {5}", - this.protocol, this.publicPort, this.privatePort, this.description, this.expiration, this.lifetime ); - } - } -} diff --git a/Mono.Nat/Mono.Nat.csproj b/Mono.Nat/Mono.Nat.csproj deleted file mode 100644 index edfd5c9bb..000000000 --- a/Mono.Nat/Mono.Nat.csproj +++ /dev/null @@ -1,17 +0,0 @@ -<Project Sdk="Microsoft.NET.Sdk"> - - <ItemGroup> - <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> - <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> - </ItemGroup> - - <ItemGroup> - <Compile Include="..\SharedVersion.cs" /> - </ItemGroup> - - <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> - <GenerateAssemblyInfo>false</GenerateAssemblyInfo> - </PropertyGroup> - -</Project> diff --git a/Mono.Nat/NatManager.cs b/Mono.Nat/NatManager.cs deleted file mode 100644 index 3ed01a6b3..000000000 --- a/Mono.Nat/NatManager.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Net; -using System.Collections.Generic; -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using MediaBrowser.Model.Dlna; -using Microsoft.Extensions.Logging; -using System.Linq; - -namespace Mono.Nat -{ - public class NatManager : IDisposable - { - public event EventHandler<DeviceEventArgs> DeviceFound; - - private List<ISearcher> controllers = new List<ISearcher>(); - - private ILogger Logger; - private IHttpClient HttpClient; - - public NatManager(ILogger logger, IHttpClient httpClient) - { - Logger = logger; - HttpClient = httpClient; - } - - private object _runSyncLock = new object(); - public void StartDiscovery() - { - lock (_runSyncLock) - { - if (controllers.Count > 0) - { - return; - } - - controllers.Add(new PmpSearcher(Logger)); - - foreach (var searcher in controllers) - { - searcher.DeviceFound += Searcher_DeviceFound; - } - } - } - - public void StopDiscovery() - { - lock (_runSyncLock) - { - var disposables = controllers.OfType<IDisposable>().ToList(); - controllers.Clear(); - - foreach (var disposable in disposables) - { - disposable.Dispose(); - } - } - } - - public void Dispose() - { - StopDiscovery(); - } - - public Task Handle(IPAddress localAddress, UpnpDeviceInfo deviceInfo, IPEndPoint endpoint, NatProtocol protocol) - { - switch (protocol) - { - case NatProtocol.Upnp: - var searcher = new UpnpSearcher(Logger, HttpClient); - searcher.DeviceFound += Searcher_DeviceFound; - return searcher.Handle(localAddress, deviceInfo, endpoint); - default: - throw new ArgumentException("Unexpected protocol: " + protocol); - } - } - - private void Searcher_DeviceFound(object sender, DeviceEventArgs e) - { - if (DeviceFound != null) - { - DeviceFound(sender, e); - } - } - } -} diff --git a/Mono.Nat/NatProtocol.cs b/Mono.Nat/NatProtocol.cs deleted file mode 100644 index 2768f133c..000000000 --- a/Mono.Nat/NatProtocol.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Mono.Nat -{ - public enum NatProtocol - { - Upnp = 0, - Pmp = 1 - } -} diff --git a/Mono.Nat/Pmp/PmpConstants.cs b/Mono.Nat/Pmp/PmpConstants.cs deleted file mode 100644 index 83fa8e07c..000000000 --- a/Mono.Nat/Pmp/PmpConstants.cs +++ /dev/null @@ -1,56 +0,0 @@ -// -// Authors: -// Ben Motmans <ben.motmans@gmail.com> -// -// Copyright (C) 2007 Ben Motmans -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; - -namespace Mono.Nat.Pmp -{ - internal static class PmpConstants - { - public const byte Version = (byte)0; - - public const byte OperationCode = (byte)0; - public const byte OperationCodeUdp = (byte)1; - public const byte OperationCodeTcp = (byte)2; - public const byte ServerNoop = (byte)128; - - public const int ClientPort = 5350; - public const int ServerPort = 5351; - - public const int RetryDelay = 250; - public const int RetryAttempts = 9; - - public const int RecommendedLeaseTime = 60 * 60; - public const int DefaultLeaseTime = RecommendedLeaseTime; - - public const short ResultCodeSuccess = 0; - public const short ResultCodeUnsupportedVersion = 1; - public const short ResultCodeNotAuthorized = 2; - public const short ResultCodeNetworkFailure = 3; - public const short ResultCodeOutOfResources = 4; - public const short ResultCodeUnsupportedOperationCode = 5; - } -} diff --git a/Mono.Nat/Pmp/PmpNatDevice.cs b/Mono.Nat/Pmp/PmpNatDevice.cs deleted file mode 100644 index 95bd72a6c..000000000 --- a/Mono.Nat/Pmp/PmpNatDevice.cs +++ /dev/null @@ -1,217 +0,0 @@ -// -// Authors: -// Ben Motmans <ben.motmans@gmail.com> -// -// Copyright (C) 2007 Ben Motmans -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; -using System.IO; -using System.Net; -using System.Net.Sockets; -using System.Threading; -using System.Collections.Generic; -using System.Threading.Tasks; -using MediaBrowser.Model.Extensions; -using Microsoft.Extensions.Logging; - -namespace Mono.Nat.Pmp -{ - internal sealed class PmpNatDevice : AbstractNatDevice, IEquatable<PmpNatDevice> - { - private IPAddress localAddress; - private IPAddress publicAddress; - private ILogger _logger; - - internal PmpNatDevice(IPAddress localAddress, IPAddress publicAddress, ILogger logger) - { - if (localAddress == null) - { - throw new ArgumentNullException(nameof(localAddress)); - } - - this.localAddress = localAddress; - this.publicAddress = publicAddress; - _logger = logger; - } - - public override IPAddress LocalAddress - { - get { return localAddress; } - } - - public override Task CreatePortMap(Mapping mapping) - { - return InternalCreatePortMapAsync(mapping, true); - } - - public override bool Equals(object obj) - { - var device = obj as PmpNatDevice; - return (device == null) ? false : this.Equals(device); - } - - public override int GetHashCode() - { - return this.publicAddress.GetHashCode(); - } - - public bool Equals(PmpNatDevice other) - { - return (other == null) ? false : this.publicAddress.Equals(other.publicAddress); - } - - private async Task<Mapping> InternalCreatePortMapAsync(Mapping mapping, bool create) - { - var package = new List<byte>(); - - package.Add(PmpConstants.Version); - package.Add(mapping.Protocol == Protocol.Tcp ? PmpConstants.OperationCodeTcp : PmpConstants.OperationCodeUdp); - package.Add(0); //reserved - package.Add(0); //reserved - package.AddRange(BitConverter.GetBytes(IPAddress.HostToNetworkOrder((short)mapping.PrivatePort))); - package.AddRange( - BitConverter.GetBytes(create ? IPAddress.HostToNetworkOrder((short)mapping.PublicPort) : (short)0)); - package.AddRange(BitConverter.GetBytes(IPAddress.HostToNetworkOrder(mapping.Lifetime))); - - try - { - byte[] buffer = package.ToArray(); - int attempt = 0; - int delay = PmpConstants.RetryDelay; - - using (var udpClient = new UdpClient()) - { - var cancellationTokenSource = new CancellationTokenSource(); - - while (attempt < PmpConstants.RetryAttempts) - { - await udpClient.SendAsync(buffer, buffer.Length, new IPEndPoint(LocalAddress, PmpConstants.ServerPort)); - - if (attempt == 0) - { - await Task.Run(() => CreatePortMapListen(udpClient, mapping, cancellationTokenSource.Token)); - } - - attempt++; - delay *= 2; - await Task.Delay(delay).ConfigureAwait(false); - } - - cancellationTokenSource.Cancel(); - } - } - catch (OperationCanceledException) - { - - } - catch (Exception e) - { - string type = create ? "create" : "delete"; - string message = String.Format("Failed to {0} portmap (protocol={1}, private port={2}) {3}", - type, - mapping.Protocol, - mapping.PrivatePort, - e.Message); - _logger.LogDebug(message); - throw e; - } - - return mapping; - } - - private async void CreatePortMapListen(UdpClient udpClient, Mapping mapping, CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - try - { - var result = await udpClient.ReceiveAsync().ConfigureAwait(false); - var endPoint = result.RemoteEndPoint; - byte[] data = data = result.Buffer; - - if (data.Length < 16) - continue; - - if (data[0] != PmpConstants.Version) - continue; - - var opCode = (byte)(data[1] & 127); - - var protocol = Protocol.Tcp; - if (opCode == PmpConstants.OperationCodeUdp) - protocol = Protocol.Udp; - - short resultCode = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, 2)); - int epoch = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(data, 4)); - - short privatePort = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, 8)); - short publicPort = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(data, 10)); - - var lifetime = (uint)IPAddress.NetworkToHostOrder(BitConverter.ToInt32(data, 12)); - - if (privatePort < 0 || publicPort < 0 || resultCode != PmpConstants.ResultCodeSuccess) - { - var errors = new[] - { - "Success", - "Unsupported Version", - "Not Authorized/Refused (e.g. box supports mapping, but user has turned feature off)" - , - "Network Failure (e.g. NAT box itself has not obtained a DHCP lease)", - "Out of resources (NAT box cannot create any more mappings at this time)", - "Unsupported opcode" - }; - - var errorMsg = errors[resultCode]; - _logger.LogDebug("Error in CreatePortMapListen: " + errorMsg); - return; - } - - if (lifetime == 0) return; //mapping was deleted - - //mapping was created - //TODO: verify that the private port+protocol are a match - mapping.PublicPort = publicPort; - mapping.Protocol = protocol; - mapping.Expiration = DateTime.Now.AddSeconds(lifetime); - return; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in CreatePortMapListen"); - return; - } - } - } - - /// <summary> - /// Overridden. - /// </summary> - /// <returns></returns> - public override string ToString() - { - return String.Format("PmpNatDevice - Local Address: {0}, Public IP: {1}, Last Seen: {2}", - this.localAddress, this.publicAddress, this.LastSeen); - } - } -} diff --git a/Mono.Nat/Pmp/PmpSearcher.cs b/Mono.Nat/Pmp/PmpSearcher.cs deleted file mode 100644 index 46c2e9d80..000000000 --- a/Mono.Nat/Pmp/PmpSearcher.cs +++ /dev/null @@ -1,235 +0,0 @@ -// -// Authors: -// Alan McGovern alan.mcgovern@gmail.com -// Ben Motmans <ben.motmans@gmail.com> -// Nicholas Terry <nick.i.terry@gmail.com> -// -// Copyright (C) 2006 Alan McGovern -// Copyright (C) 2007 Ben Motmans -// Copyright (C) 2014 Nicholas Terry -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - - -using System; -using System.Collections.Generic; -using System.Text; -using System.Net; -using Mono.Nat.Pmp; -using System.Net.NetworkInformation; -using System.Net.Sockets; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using System.Linq; - -namespace Mono.Nat -{ - internal class PmpSearcher : ISearcher, IDisposable - { - private ILogger _logger; - - private int timeout = 250; - private DateTime nextSearch; - public event EventHandler<DeviceEventArgs> DeviceFound; - - public PmpSearcher(ILogger logger) - { - _logger = logger; - - CreateSocketsAndAddGateways(); - } - - public void Dispose() - { - var list = sockets.ToList(); - sockets.Clear(); - - foreach (var s in list) - { - using (s) - { - s.Close(); - } - } - } - - private List<UdpClient> sockets; - private Dictionary<UdpClient, List<IPEndPoint>> gatewayLists; - - private void CreateSocketsAndAddGateways() - { - sockets = new List<UdpClient>(); - gatewayLists = new Dictionary<UdpClient, List<IPEndPoint>>(); - - try - { - foreach (var n in NetworkInterface.GetAllNetworkInterfaces()) - { - if (n.OperationalStatus != OperationalStatus.Up && n.OperationalStatus != OperationalStatus.Unknown) - continue; - IPInterfaceProperties properties = n.GetIPProperties(); - var gatewayList = new List<IPEndPoint>(); - - foreach (GatewayIPAddressInformation gateway in properties.GatewayAddresses) - { - if (gateway.Address.AddressFamily == AddressFamily.InterNetwork) - { - gatewayList.Add(new IPEndPoint(gateway.Address, PmpConstants.ServerPort)); - } - } - if (gatewayList.Count == 0) - { - /* Mono on OSX doesn't give any gateway addresses, so check DNS entries */ - foreach (var gw2 in properties.DnsAddresses) - { - if (gw2.AddressFamily == AddressFamily.InterNetwork) - { - gatewayList.Add(new IPEndPoint(gw2, PmpConstants.ServerPort)); - } - } - foreach (UnicastIPAddressInformation unicast in properties.UnicastAddresses) - { - if (/*unicast.DuplicateAddressDetectionState == DuplicateAddressDetectionState.Preferred - && unicast.AddressPreferredLifetime != UInt32.MaxValue - && */unicast.Address.AddressFamily == AddressFamily.InterNetwork) - { - var bytes = unicast.Address.GetAddressBytes(); - bytes[3] = 1; - gatewayList.Add(new IPEndPoint(new IPAddress(bytes), PmpConstants.ServerPort)); - } - } - } - - if (gatewayList.Count > 0) - { - foreach (var address in properties.UnicastAddresses) - { - if (address.Address.AddressFamily == AddressFamily.InterNetwork) - { - UdpClient client; - - try - { - client = new UdpClient(new IPEndPoint(address.Address, 0)); - } - catch (SocketException) - { - continue; // Move on to the next address. - } - - gatewayLists.Add(client, gatewayList); - sockets.Add(client); - } - } - } - } - } - catch (Exception) - { - // NAT-PMP does not use multicast, so there isn't really a good fallback. - } - } - - public async void Search() - { - foreach (UdpClient s in sockets) - { - try - { - await Search(s).ConfigureAwait(false); - } - catch - { - // Ignore any search errors - } - } - } - - async Task Search(UdpClient client) - { - // Sort out the time for the next search first. The spec says the - // timeout should double after each attempt. Once it reaches 64 seconds - // (and that attempt fails), assume no devices available - nextSearch = DateTime.Now.AddMilliseconds(timeout); - timeout *= 2; - - // We've tried 9 times as per spec, try searching again in 5 minutes - if (timeout == 128 * 1000) - { - timeout = 250; - nextSearch = DateTime.Now.AddMinutes(10); - return; - } - - // The nat-pmp search message. Must be sent to GatewayIP:53531 - byte[] buffer = new byte[] { PmpConstants.Version, PmpConstants.OperationCode }; - foreach (IPEndPoint gatewayEndpoint in gatewayLists[client]) - { - await client.SendAsync(buffer, buffer.Length, gatewayEndpoint).ConfigureAwait(false); - } - } - - bool IsSearchAddress(IPAddress address) - { - foreach (var gatewayList in gatewayLists.Values) - foreach (var gatewayEndpoint in gatewayList) - if (gatewayEndpoint.Address.Equals(address)) - return true; - return false; - } - - public void Handle(IPAddress localAddress, byte[] response, IPEndPoint endpoint) - { - if (!IsSearchAddress(endpoint.Address)) - return; - if (response.Length != 12) - return; - if (response[0] != PmpConstants.Version) - return; - if (response[1] != PmpConstants.ServerNoop) - return; - int errorcode = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(response, 2)); - if (errorcode != 0) - _logger.LogDebug("Non zero error: {0}", errorcode); - - var publicIp = new IPAddress(new byte[] { response[8], response[9], response[10], response[11] }); - nextSearch = DateTime.Now.AddMinutes(5); - timeout = 250; - - OnDeviceFound(new DeviceEventArgs(new PmpNatDevice(endpoint.Address, publicIp, _logger))); - } - - public DateTime NextSearch - { - get { return nextSearch; } - } - private void OnDeviceFound(DeviceEventArgs args) - { - if (DeviceFound != null) - DeviceFound(this, args); - } - - public NatProtocol Protocol - { - get { return NatProtocol.Pmp; } - } - } -} diff --git a/Mono.Nat/Properties/AssemblyInfo.cs b/Mono.Nat/Properties/AssemblyInfo.cs deleted file mode 100644 index dc47f2ffe..000000000 --- a/Mono.Nat/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Reflection; -using System.Resources; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Mono.Nat")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2006 Alan McGovern. Copyright © 2007 Ben Motmans. Code releases under the MIT license. Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: NeutralResourcesLanguage("en")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] diff --git a/Mono.Nat/Upnp/Messages/GetServicesMessage.cs b/Mono.Nat/Upnp/Messages/GetServicesMessage.cs deleted file mode 100644 index f619f5ca4..000000000 --- a/Mono.Nat/Upnp/Messages/GetServicesMessage.cs +++ /dev/null @@ -1,64 +0,0 @@ -// -// Authors: -// Alan McGovern alan.mcgovern@gmail.com -// -// Copyright (C) 2006 Alan McGovern -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; -using System.Net; -using MediaBrowser.Common.Net; - -namespace Mono.Nat.Upnp -{ - internal class GetServicesMessage : MessageBase - { - private string _servicesDescriptionUrl; - private EndPoint _hostAddress; - - public GetServicesMessage(string description, EndPoint hostAddress) - : base(null) - { - if (string.IsNullOrEmpty(description)) - { - throw new ArgumentException("Description is null/empty", nameof(description)); - } - - this._servicesDescriptionUrl = description; - this._hostAddress = hostAddress ?? throw new ArgumentNullException(nameof(hostAddress)); - } - - public override string Method => "GET"; - - public override HttpRequestOptions Encode() - { - var req = new HttpRequestOptions() - { - Url = $"http://{this._hostAddress}{this._servicesDescriptionUrl}" - }; - - req.RequestHeaders.Add("ACCEPT-LANGUAGE", "en"); - - return req; - } - } -} diff --git a/Mono.Nat/Upnp/Messages/Requests/CreatePortMappingMessage.cs b/Mono.Nat/Upnp/Messages/Requests/CreatePortMappingMessage.cs deleted file mode 100644 index 7d6844e32..000000000 --- a/Mono.Nat/Upnp/Messages/Requests/CreatePortMappingMessage.cs +++ /dev/null @@ -1,75 +0,0 @@ -// -// Authors: -// Alan McGovern alan.mcgovern@gmail.com -// -// Copyright (C) 2006 Alan McGovern -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System.Net; -using System.IO; -using System.Globalization; -using System.Text; -using System.Xml; -using MediaBrowser.Common.Net; - -namespace Mono.Nat.Upnp -{ - internal class CreatePortMappingMessage : MessageBase - { - #region Private Fields - - private IPAddress localIpAddress; - private Mapping mapping; - - #endregion - - - #region Constructors - public CreatePortMappingMessage(Mapping mapping, IPAddress localIpAddress, UpnpNatDevice device) - : base(device) - { - this.mapping = mapping; - this.localIpAddress = localIpAddress; - } - #endregion - - public override HttpRequestOptions Encode() - { - var culture = CultureInfo.InvariantCulture; - - var builder = new StringBuilder(256); - XmlWriter writer = CreateWriter(builder); - - WriteFullElement(writer, "NewRemoteHost", string.Empty); - WriteFullElement(writer, "NewExternalPort", this.mapping.PublicPort.ToString(culture)); - WriteFullElement(writer, "NewProtocol", this.mapping.Protocol == Protocol.Tcp ? "TCP" : "UDP"); - WriteFullElement(writer, "NewInternalPort", this.mapping.PrivatePort.ToString(culture)); - WriteFullElement(writer, "NewInternalClient", this.localIpAddress.ToString()); - WriteFullElement(writer, "NewEnabled", "1"); - WriteFullElement(writer, "NewPortMappingDescription", string.IsNullOrEmpty(mapping.Description) ? "Mono.Nat" : mapping.Description); - WriteFullElement(writer, "NewLeaseDuration", mapping.Lifetime.ToString()); - - writer.Flush(); - return CreateRequest("AddPortMapping", builder.ToString()); - } - } -} diff --git a/Mono.Nat/Upnp/Messages/UpnpMessage.cs b/Mono.Nat/Upnp/Messages/UpnpMessage.cs deleted file mode 100644 index d47241d4a..000000000 --- a/Mono.Nat/Upnp/Messages/UpnpMessage.cs +++ /dev/null @@ -1,84 +0,0 @@ -// -// Authors: -// Alan McGovern alan.mcgovern@gmail.com -// -// Copyright (C) 2006 Alan McGovern -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System.Xml; -using System.Text; -using MediaBrowser.Common.Net; - -namespace Mono.Nat.Upnp -{ - internal abstract class MessageBase - { - protected UpnpNatDevice device; - - protected MessageBase(UpnpNatDevice device) - { - this.device = device; - } - - protected HttpRequestOptions CreateRequest(string upnpMethod, string methodParameters) - { - var req = new HttpRequestOptions() - { - Url = $"http://{this.device.HostEndPoint}{this.device.ControlUrl}", - EnableKeepAlive = false, - RequestContentType = "text/xml", - RequestContent = "<s:Envelope " - + "xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" " - + "s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" - + "<s:Body>" - + "<u:" + upnpMethod + " " - + "xmlns:u=\"" + device.ServiceType + "\">" - + methodParameters - + "</u:" + upnpMethod + ">" - + "</s:Body>" - + "</s:Envelope>\r\n\r\n" - }; - - req.RequestHeaders.Add("SOAPACTION", "\"" + device.ServiceType + "#" + upnpMethod + "\""); - - return req; - } - - public abstract HttpRequestOptions Encode(); - - public virtual string Method => "POST"; - - protected void WriteFullElement(XmlWriter writer, string element, string value) - { - writer.WriteStartElement(element); - writer.WriteString(value); - writer.WriteEndElement(); - } - - protected XmlWriter CreateWriter(StringBuilder sb) - { - var settings = new XmlWriterSettings(); - settings.ConformanceLevel = ConformanceLevel.Fragment; - return XmlWriter.Create(sb, settings); - } - } -} diff --git a/Mono.Nat/Upnp/Searchers/UpnpSearcher.cs b/Mono.Nat/Upnp/Searchers/UpnpSearcher.cs deleted file mode 100644 index 3b54c4680..000000000 --- a/Mono.Nat/Upnp/Searchers/UpnpSearcher.cs +++ /dev/null @@ -1,111 +0,0 @@ -// -// Authors: -// Alan McGovern alan.mcgovern@gmail.com -// Ben Motmans <ben.motmans@gmail.com> -// Nicholas Terry <nick.i.terry@gmail.com> -// -// Copyright (C) 2006 Alan McGovern -// Copyright (C) 2007 Ben Motmans -// Copyright (C) 2014 Nicholas Terry -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; -using System.Collections.Generic; -using System.Text; -using System.Net; -using Mono.Nat.Upnp; -using System.Diagnostics; -using System.Net.Sockets; -using System.Net.NetworkInformation; -using MediaBrowser.Common.Net; -using Microsoft.Extensions.Logging; -using MediaBrowser.Model.Dlna; -using System.Threading.Tasks; - -namespace Mono.Nat -{ - internal class UpnpSearcher : ISearcher - { - public event EventHandler<DeviceEventArgs> DeviceFound; - - private readonly ILogger _logger; - private readonly IHttpClient _httpClient; - - public UpnpSearcher(ILogger logger, IHttpClient httpClient) - { - _logger = logger; - _httpClient = httpClient; - } - - public void Search() - { - } - - public async Task Handle(IPAddress localAddress, UpnpDeviceInfo deviceInfo, IPEndPoint endpoint) - { - if (localAddress == null) - { - throw new ArgumentNullException(nameof(localAddress)); - } - - try - { - /* For UPnP Port Mapping we need ot find either WANPPPConnection or WANIPConnection. - * Any other device type is no good to us for this purpose. See the IGP overview paper - * page 5 for an overview of device types and their hierarchy. - * http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v1-Device.pdf */ - - /* TODO: Currently we are assuming version 1 of the protocol. We should figure out which - * version it is and apply the correct URN. */ - - /* Some routers don't correctly implement the version ID on the URN, so we only search for the type - * prefix. */ - - // We have an internet gateway device now - var d = new UpnpNatDevice(localAddress, deviceInfo, endpoint, string.Empty, _logger, _httpClient); - - await d.GetServicesList().ConfigureAwait(false); - - OnDeviceFound(new DeviceEventArgs(d)); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error decoding device response"); - } - } - - public void Handle(IPAddress localAddress, byte[] response, IPEndPoint endpoint) - { - } - - private void OnDeviceFound(DeviceEventArgs args) - { - if (DeviceFound != null) - DeviceFound(this, args); - } - - public NatProtocol Protocol - { - get { return NatProtocol.Upnp; } - } - } -} diff --git a/Mono.Nat/Upnp/UpnpNatDevice.cs b/Mono.Nat/Upnp/UpnpNatDevice.cs deleted file mode 100644 index 3ff1eeb90..000000000 --- a/Mono.Nat/Upnp/UpnpNatDevice.cs +++ /dev/null @@ -1,267 +0,0 @@ -// -// Authors: -// Alan McGovern alan.mcgovern@gmail.com -// Ben Motmans <ben.motmans@gmail.com> -// -// Copyright (C) 2006 Alan McGovern -// Copyright (C) 2007 Ben Motmans -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -using System; -using System.Net; -using System.Xml; -using System.Text; -using System.Threading.Tasks; -using MediaBrowser.Common.Net; -using Microsoft.Extensions.Logging; -using MediaBrowser.Model.Dlna; - -namespace Mono.Nat.Upnp -{ - public sealed class UpnpNatDevice : AbstractNatDevice, IEquatable<UpnpNatDevice> - { - private EndPoint hostEndPoint; - private IPAddress localAddress; - private string serviceDescriptionUrl; - private string controlUrl; - private string serviceType; - private readonly ILogger _logger; - private readonly IHttpClient _httpClient; - - public override IPAddress LocalAddress - { - get { return localAddress; } - } - - internal UpnpNatDevice(IPAddress localAddress, UpnpDeviceInfo deviceInfo, IPEndPoint hostEndPoint, string serviceType, ILogger logger, IHttpClient httpClient) - { - if (localAddress == null) - { - throw new ArgumentNullException(nameof(localAddress)); - } - - this.LastSeen = DateTime.Now; - this.localAddress = localAddress; - - // Split the string at the "location" section so i can extract the ipaddress and service description url - string locationDetails = deviceInfo.Location.ToString(); - this.serviceType = serviceType; - _logger = logger; - _httpClient = httpClient; - - // Make sure we have no excess whitespace - locationDetails = locationDetails.Trim(); - - // FIXME: Is this reliable enough. What if we get a hostname as opposed to a proper http address - // Are we going to get addresses with the "http://" attached? - if (locationDetails.StartsWith("http://", StringComparison.OrdinalIgnoreCase)) - { - _logger.LogDebug("Found device at: {0}", locationDetails); - // This bit strings out the "http://" from the string - locationDetails = locationDetails.Substring(7); - - this.hostEndPoint = hostEndPoint; - - // The service description URL is the remainder of the "locationDetails" string. The bit that was originally after the ip - // and port information - this.serviceDescriptionUrl = locationDetails.Substring(locationDetails.IndexOf('/')); - } - else - { - _logger.LogDebug("Couldn't decode address. Please send following string to the developer: "); - } - } - - public async Task GetServicesList() - { - // Create a HTTPWebRequest to download the list of services the device offers - var message = new GetServicesMessage(this.serviceDescriptionUrl, this.hostEndPoint); - - using (var response = await _httpClient.SendAsync(message.Encode(), message.Method).ConfigureAwait(false)) - { - OnServicesReceived(response); - } - } - - private void OnServicesReceived(HttpResponseInfo response) - { - int abortCount = 0; - int bytesRead = 0; - byte[] buffer = new byte[10240]; - var servicesXml = new StringBuilder(); - var xmldoc = new XmlDocument(); - - using (var s = response.Content) - { - if (response.StatusCode != HttpStatusCode.OK) - { - _logger.LogDebug("{0}: Couldn't get services list: {1}", HostEndPoint, response.StatusCode); - return; // FIXME: This the best thing to do?? - } - - while (true) - { - bytesRead = s.Read(buffer, 0, buffer.Length); - servicesXml.Append(Encoding.UTF8.GetString(buffer, 0, bytesRead)); - try - { - xmldoc.LoadXml(servicesXml.ToString()); - break; - } - catch (XmlException) - { - // If we can't receive the entire XML within 500ms, then drop the connection - // Unfortunately not all routers supply a valid ContentLength (mine doesn't) - // so this hack is needed to keep testing our recieved data until it gets successfully - // parsed by the xmldoc. Without this, the code will never pick up my router. - if (abortCount++ > 50) - { - return; - } - _logger.LogDebug("{0}: Couldn't parse services list", HostEndPoint); - System.Threading.Thread.Sleep(10); - } - } - - var ns = new XmlNamespaceManager(xmldoc.NameTable); - ns.AddNamespace("ns", "urn:schemas-upnp-org:device-1-0"); - XmlNodeList nodes = xmldoc.SelectNodes("//*/ns:serviceList", ns); - - foreach (XmlNode node in nodes) - { - //Go through each service there - foreach (XmlNode service in node.ChildNodes) - { - //If the service is a WANIPConnection, then we have what we want - string type = service["serviceType"].InnerText; - _logger.LogDebug("{0}: Found service: {1}", HostEndPoint, type); - - // TODO: Add support for version 2 of UPnP. - if (string.Equals(type, "urn:schemas-upnp-org:service:WANPPPConnection:1", StringComparison.OrdinalIgnoreCase) || - string.Equals(type, "urn:schemas-upnp-org:service:WANIPConnection:1", StringComparison.OrdinalIgnoreCase)) - { - this.controlUrl = service["controlURL"].InnerText; - _logger.LogDebug("{0}: Found upnp service at: {1}", HostEndPoint, controlUrl); - - Uri u; - if (Uri.TryCreate(controlUrl, UriKind.RelativeOrAbsolute, out u)) - { - if (u.IsAbsoluteUri) - { - var old = hostEndPoint; - IPAddress parsedHostIpAddress; - if (IPAddress.TryParse(u.Host, out parsedHostIpAddress)) - { - this.hostEndPoint = new IPEndPoint(parsedHostIpAddress, u.Port); - //_logger.LogDebug("{0}: Absolute URI detected. Host address is now: {1}", old, HostEndPoint); - this.controlUrl = controlUrl.Substring(u.GetLeftPart(UriPartial.Authority).Length); - //_logger.LogDebug("{0}: New control url: {1}", HostEndPoint, controlUrl); - } - } - } - else - { - _logger.LogDebug("{0}: Assuming control Uri is relative: {1}", HostEndPoint, controlUrl); - } - return; - } - } - } - - //If we get here, it means that we didn't get WANIPConnection service, which means no uPnP forwarding - //So we don't invoke the callback, so this device is never added to our lists - } - } - - /// <summary> - /// The EndPoint that the device is at - /// </summary> - internal EndPoint HostEndPoint - { - get { return this.hostEndPoint; } - } - - /// <summary> - /// The relative url of the xml file that describes the list of services is at - /// </summary> - internal string ServiceDescriptionUrl - { - get { return this.serviceDescriptionUrl; } - } - - /// <summary> - /// The relative url that we can use to control the port forwarding - /// </summary> - internal string ControlUrl - { - get { return this.controlUrl; } - } - - /// <summary> - /// The service type we're using on the device - /// </summary> - public string ServiceType - { - get { return serviceType; } - } - - public override async Task CreatePortMap(Mapping mapping) - { - var message = new CreatePortMappingMessage(mapping, localAddress, this); - using (await _httpClient.SendAsync(message.Encode(), message.Method).ConfigureAwait(false)) - { - - } - } - - public override bool Equals(object obj) - { - var device = obj as UpnpNatDevice; - return (device == null) ? false : this.Equals((device)); - } - - - public bool Equals(UpnpNatDevice other) - { - return (other == null) ? false : (this.hostEndPoint.Equals(other.hostEndPoint) - //&& this.controlUrl == other.controlUrl - && this.serviceDescriptionUrl == other.serviceDescriptionUrl); - } - - public override int GetHashCode() - { - return (this.hostEndPoint.GetHashCode() ^ this.controlUrl.GetHashCode() ^ this.serviceDescriptionUrl.GetHashCode()); - } - - /// <summary> - /// Overridden. - /// </summary> - /// <returns></returns> - public override string ToString() - { - //GetExternalIP is blocking and can throw exceptions, can't use it here. - return String.Format( - "UpnpNatDevice - EndPoint: {0}, External IP: {1}, Control Url: {2}, Service Description Url: {3}, Service Type: {4}, Last Seen: {5}", - this.hostEndPoint, "Manually Check" /*this.GetExternalIP()*/, this.controlUrl, this.serviceDescriptionUrl, this.serviceType, this.LastSeen); - } - } -} diff --git a/RSSDP/RSSDP.csproj b/RSSDP/RSSDP.csproj index 456a93aa8..9753ae9b1 100644 --- a/RSSDP/RSSDP.csproj +++ b/RSSDP/RSSDP.csproj @@ -7,7 +7,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> </PropertyGroup> diff --git a/benches/Jellyfin.Common.Benches/HexDecodeBenches.cs b/benches/Jellyfin.Common.Benches/HexDecodeBenches.cs new file mode 100644 index 000000000..d9a107b69 --- /dev/null +++ b/benches/Jellyfin.Common.Benches/HexDecodeBenches.cs @@ -0,0 +1,45 @@ +using System; +using System.Globalization; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; +using MediaBrowser.Common; + +namespace Jellyfin.Common.Benches +{ + [MemoryDiagnoser] + public class HexDecodeBenches + { + private string _data; + + [Params(0, 10, 100, 1000, 10000, 1000000)] + public int N { get; set; } + + [GlobalSetup] + public void GlobalSetup() + { + var bytes = new byte[N]; + new Random(42).NextBytes(bytes); + _data = Hex.Encode(bytes); + } + + [Benchmark] + public byte[] Decode() => Hex.Decode(_data); + + [Benchmark] + public byte[] DecodeSubString() => DecodeSubString(_data); + + private static byte[] DecodeSubString(string str) + { + byte[] bytes = new byte[str.Length / 2]; + for (int i = 0; i < str.Length; i += 2) + { + bytes[i / 2] = byte.Parse( + str.Substring(i, 2), + NumberStyles.HexNumber, + CultureInfo.InvariantCulture); + } + + return bytes; + } + } +} diff --git a/benches/Jellyfin.Common.Benches/HexEncodeBenches.cs b/benches/Jellyfin.Common.Benches/HexEncodeBenches.cs new file mode 100644 index 000000000..7abf93c51 --- /dev/null +++ b/benches/Jellyfin.Common.Benches/HexEncodeBenches.cs @@ -0,0 +1,32 @@ +using System; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; +using MediaBrowser.Common; + +namespace Jellyfin.Common.Benches +{ + [MemoryDiagnoser] + public class HexEncodeBenches + { + private byte[] _data; + + [Params(0, 10, 100, 1000, 10000, 1000000)] + public int N { get; set; } + + [GlobalSetup] + public void GlobalSetup() + { + _data = new byte[N]; + new Random(42).NextBytes(_data); + } + + [Benchmark] + public string HexEncode() => Hex.Encode(_data); + + [Benchmark] + public string BitConverterToString() => BitConverter.ToString(_data); + + [Benchmark] + public string BitConverterToStringWithReplace() => BitConverter.ToString(_data).Replace("-", ""); + } +} diff --git a/benches/Jellyfin.Common.Benches/Jellyfin.Common.Benches.csproj b/benches/Jellyfin.Common.Benches/Jellyfin.Common.Benches.csproj new file mode 100644 index 000000000..4d5046bf9 --- /dev/null +++ b/benches/Jellyfin.Common.Benches/Jellyfin.Common.Benches.csproj @@ -0,0 +1,16 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>netcoreapp3.0</TargetFramework> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="BenchmarkDotNet" Version="0.11.5" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" /> + </ItemGroup> + +</Project> diff --git a/benches/Jellyfin.Common.Benches/Program.cs b/benches/Jellyfin.Common.Benches/Program.cs new file mode 100644 index 000000000..b218b0dc1 --- /dev/null +++ b/benches/Jellyfin.Common.Benches/Program.cs @@ -0,0 +1,14 @@ +using System; +using BenchmarkDotNet.Running; + +namespace Jellyfin.Common.Benches +{ + public static class Program + { + public static void Main(string[] args) + { + _ = BenchmarkRunner.Run<HexEncodeBenches>(); + _ = BenchmarkRunner.Run<HexDecodeBenches>(); + } + } +} diff --git a/deployment/centos-package-x64/Dockerfile b/deployment/centos-package-x64/Dockerfile index 855b0a479..04daef93c 100644 --- a/deployment/centos-package-x64/Dockerfile +++ b/deployment/centos-package-x64/Dockerfile @@ -3,7 +3,7 @@ FROM centos:7 ARG SOURCE_DIR=/jellyfin ARG PLATFORM_DIR=/jellyfin/deployment/centos-package-x64 ARG ARTIFACT_DIR=/dist -ARG SDK_VERSION=2.2 +ARG SDK_VERSION=3.0 # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist @@ -13,13 +13,12 @@ RUN yum update -y \ && yum install -y epel-release # Install build dependencies -RUN yum install -y @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel wget git +RUN yum install -y @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git # Install recent NodeJS and Yarn -RUN wget -O- https://raw.githubusercontent.com/creationix/nvm/v0.35.0/install.sh | /bin/bash \ - && source "$HOME/.nvm/nvm.sh" \ - && nvm install v8 \ - && npm install -g yarn +RUN curl -fSsLo /etc/yum.repos.d/yarn.repo https://dl.yarnpkg.com/rpm/yarn.repo \ + && rpm -i https://rpm.nodesource.com/pub_8.x/el/7/x86_64/nodesource-release-el7-1.noarch.rpm \ + && yum install -y yarn # Install DotNET SDK RUN rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm \ diff --git a/deployment/centos-package-x64/docker-build.sh b/deployment/centos-package-x64/docker-build.sh index 18e10661c..62dd144e5 100755 --- a/deployment/centos-package-x64/docker-build.sh +++ b/deployment/centos-package-x64/docker-build.sh @@ -8,76 +8,9 @@ set -o xtrace # Move to source directory pushd ${SOURCE_DIR} -VERSION="$( grep '^Version:' ${SOURCE_DIR}/SOURCES/pkg-src/jellyfin.spec | awk '{ print $NF }' )" - -# Clone down and build Web frontend -web_build_dir="$( mktemp -d )" -web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web" -git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/ -pushd ${web_build_dir} -if [[ -n ${web_branch} ]]; then - checkout -b origin/${web_branch} -fi -source "$HOME/.nvm/nvm.sh" -nvm use v8 -yarn install -mkdir -p ${web_target} -mv dist/* ${web_target}/ -popd -rm -rf ${web_build_dir} - -# Create RPM source archive -GNU_TAR=1 -echo "Bundling all sources for RPM build." -tar \ ---transform "s,^\.,jellyfin-${VERSION}," \ ---exclude='.git*' \ ---exclude='**/.git' \ ---exclude='**/.hg' \ ---exclude='**/.vs' \ ---exclude='**/.vscode' \ ---exclude='deployment' \ ---exclude='**/bin' \ ---exclude='**/obj' \ ---exclude='**/.nuget' \ ---exclude='*.deb' \ ---exclude='*.rpm' \ --czf "${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-${VERSION}.tar.gz" \ --C ${SOURCE_DIR} ./ || GNU_TAR=0 - -if [ $GNU_TAR -eq 0 ]; then - echo "The installed tar binary did not support --transform. Using workaround." - package_temporary_dir="$( mktemp -d )" - mkdir -p "${package_temporary_dir}/jellyfin" - # Not GNU tar - tar \ - --exclude='.git*' \ - --exclude='**/.git' \ - --exclude='**/.hg' \ - --exclude='**/.vs' \ - --exclude='**/.vscode' \ - --exclude='deployment' \ - --exclude='**/bin' \ - --exclude='**/obj' \ - --exclude='**/.nuget' \ - --exclude='*.deb' \ - --exclude='*.rpm' \ - -czf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" \ - -C ${SOURCE_DIR} ./ - echo "Extracting filtered package." - mkdir -p "${package_temporary_dir}/jellyfin-${VERSION}" - tar -xzf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}/jellyfin-${VERSION}" - echo "Removing filtered package." - rm -f "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" - echo "Repackaging package into final tarball." - tar -czf "${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}" "jellyfin-${VERSION}" - rm -rf ${package_temporary_dir} -fi - # Build RPM -spectool -g -R SPECS/jellyfin.spec -rpmbuild -bs SPECS/jellyfin.spec --define "_sourcedir ${SOURCE_DIR}/SOURCES/pkg-src/" -rpmbuild -bb SPECS/jellyfin.spec --define "_sourcedir ${SOURCE_DIR}/SOURCES/pkg-src/" +make -f .copr/Makefile srpm outdir=/root/rpmbuild/SRPMS +rpmbuild --rebuild -bb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm # Move the artifacts out mkdir -p ${ARTIFACT_DIR}/rpm diff --git a/deployment/fedora-package-x64/Dockerfile b/deployment/fedora-package-x64/Dockerfile index b8226b173..769c62ab2 100644 --- a/deployment/fedora-package-x64/Dockerfile +++ b/deployment/fedora-package-x64/Dockerfile @@ -3,7 +3,7 @@ FROM fedora:29 ARG SOURCE_DIR=/jellyfin ARG PLATFORM_DIR=/jellyfin/deployment/fedora-package-x64 ARG ARTIFACT_DIR=/dist -ARG SDK_VERSION=2.2 +ARG SDK_VERSION=3.0 # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist @@ -12,17 +12,13 @@ ENV ARTIFACT_DIR=/dist RUN dnf update -y # Install build dependencies -RUN dnf install -y @buildsys-build rpmdevtools dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel nodejs wget git +RUN dnf install -y @buildsys-build rpmdevtools dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel nodejs-yarn # Install DotNET SDK RUN dnf copr enable -y @dotnet-sig/dotnet \ && rpmdev-setuptree \ && dnf install -y dotnet-sdk-${SDK_VERSION} dotnet-runtime-${SDK_VERSION} -# Install yarn package manager -RUN wget -q -O /etc/yum.repos.d/yarn.repo https://dl.yarnpkg.com/rpm/yarn.repo \ - && dnf install -y yarn - # Create symlinks and directories RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \ && mkdir -p ${SOURCE_DIR}/SPECS \ diff --git a/deployment/fedora-package-x64/docker-build.sh b/deployment/fedora-package-x64/docker-build.sh index 014f582f0..740e8d35c 100755 --- a/deployment/fedora-package-x64/docker-build.sh +++ b/deployment/fedora-package-x64/docker-build.sh @@ -8,74 +8,9 @@ set -o xtrace # Move to source directory pushd ${SOURCE_DIR} -VERSION="$( grep '^Version:' ${SOURCE_DIR}/SOURCES/pkg-src/jellyfin.spec | awk '{ print $NF }' )" - -# Clone down and build Web frontend -web_build_dir="$( mktemp -d )" -web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web" -git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/ -pushd ${web_build_dir} -if [[ -n ${web_branch} ]]; then - checkout -b origin/${web_branch} -fi -yarn install -mkdir -p ${web_target} -mv dist/* ${web_target}/ -popd -rm -rf ${web_build_dir} - -# Create RPM source archive -GNU_TAR=1 -echo "Bundling all sources for RPM build." -tar \ ---transform "s,^\.,jellyfin-${VERSION}," \ ---exclude='.git*' \ ---exclude='**/.git' \ ---exclude='**/.hg' \ ---exclude='**/.vs' \ ---exclude='**/.vscode' \ ---exclude='deployment' \ ---exclude='**/bin' \ ---exclude='**/obj' \ ---exclude='**/.nuget' \ ---exclude='*.deb' \ ---exclude='*.rpm' \ --czf "${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-${VERSION}.tar.gz" \ --C ${SOURCE_DIR} ./ || GNU_TAR=0 - -if [ $GNU_TAR -eq 0 ]; then - echo "The installed tar binary did not support --transform. Using workaround." - package_temporary_dir="$( mktemp -d )" - mkdir -p "${package_temporary_dir}/jellyfin" - # Not GNU tar - tar \ - --exclude='.git*' \ - --exclude='**/.git' \ - --exclude='**/.hg' \ - --exclude='**/.vs' \ - --exclude='**/.vscode' \ - --exclude='deployment' \ - --exclude='**/bin' \ - --exclude='**/obj' \ - --exclude='**/.nuget' \ - --exclude='*.deb' \ - --exclude='*.rpm' \ - -czf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" \ - -C ${SOURCE_DIR} ./ - echo "Extracting filtered package." - mkdir -p "${package_temporary_dir}/jellyfin-${VERSION}" - tar -xzf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}/jellyfin-${VERSION}" - echo "Removing filtered package." - rm -f "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" - echo "Repackaging package into final tarball." - tar -czf "${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}" "jellyfin-${VERSION}" - rm -rf ${package_temporary_dir} -fi - # Build RPM -spectool -g -R SPECS/jellyfin.spec -rpmbuild -bs SPECS/jellyfin.spec --define "_sourcedir ${SOURCE_DIR}/SOURCES/pkg-src/" -rpmbuild -bb SPECS/jellyfin.spec --define "_sourcedir ${SOURCE_DIR}/SOURCES/pkg-src/" +make -f .copr/Makefile srpm outdir=/root/rpmbuild/SRPMS +rpmbuild -rb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm # Move the artifacts out mkdir -p ${ARTIFACT_DIR}/rpm diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.spec b/deployment/fedora-package-x64/pkg-src/jellyfin.spec index 0c6bf7180..7118fcf3f 100644 --- a/deployment/fedora-package-x64/pkg-src/jellyfin.spec +++ b/deployment/fedora-package-x64/pkg-src/jellyfin.spec @@ -12,28 +12,36 @@ Release: 1%{?dist} Summary: The Free Software Media Browser License: GPLv2 URL: https://jellyfin.media -Source0: %{name}-%{version}.tar.gz -Source1: jellyfin.service -Source2: jellyfin.env -Source3: jellyfin.sudoers -Source4: restart.sh -Source5: jellyfin.override.conf -Source6: jellyfin-firewalld.xml +# Jellyfin Server tarball created by `make -f .copr/Makefile srpm`, real URL ends with `v%{version}.tar.gz` +Source0: https://github.com/%{name}/%{name}/archive/%{name}-%{version}.tar.gz +# Jellyfin Webinterface downloaded by `make -f .copr/Makefile srpm`, real URL ends with `v%{version}.tar.gz` +Source1: https://github.com/%{name}/%{name}-web/archive/%{name}-web-%{version}.tar.gz +Source11: jellyfin.service +Source12: jellyfin.env +Source13: jellyfin.sudoers +Source14: restart.sh +Source15: jellyfin.override.conf +Source16: jellyfin-firewalld.xml %{?systemd_requires} BuildRequires: systemd Requires(pre): shadow-utils BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel, glibc-devel, libicu-devel +%if 0%{?fedora} +BuildRequires: nodejs-yarn +%else +# Requirements not packaged in main repos +# From https://rpm.nodesource.com/pub_8.x/el/7/x86_64/ +BuildRequires: nodejs >= 8 yarn +%endif Requires: libcurl, fontconfig, freetype, openssl, glibc libicu # Requirements not packaged in main repos -# COPR @dotnet-sig/dotnet -BuildRequires: dotnet-runtime-2.2, dotnet-sdk-2.2 +# COPR @dotnet-sig/dotnet or +# https://packages.microsoft.com/rhel/7/prod/ +BuildRequires: dotnet-runtime-3.0, dotnet-sdk-3.0 # RPMfusion free Requires: ffmpeg -# Fedora has openssl1.1 which is incompatible with dotnet -%{?fedora:Requires: compat-openssl10} - # Disable Automatic Dependency Processing AutoReqProv: no @@ -42,7 +50,18 @@ Jellyfin is a free software media system that puts you in control of managing an %prep -%autosetup -n %{name}-%{version} +%autosetup -n %{name}-%{version} -b 0 -b 1 +web_build_dir="$(mktemp -d)" +web_target="$PWD/MediaBrowser.WebDashboard/jellyfin-web" +pushd ../jellyfin-web-%{version} || pushd ../jellyfin-web-master +%if 0%{?fedora} +nodejs-yarn install +%else +yarn install +%endif +mkdir -p ${web_target} +mv dist/* ${web_target}/ +popd %build @@ -52,7 +71,7 @@ export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 dotnet publish --configuration Release --output='%{buildroot}%{_libdir}/jellyfin' --self-contained --runtime %{dotnet_runtime} \ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" Jellyfin.Server %{__install} -D -m 0644 LICENSE %{buildroot}%{_datadir}/licenses/%{name}/LICENSE -%{__install} -D -m 0644 %{SOURCE5} %{buildroot}%{_sysconfdir}/systemd/system/%{name}.service.d/override.conf +%{__install} -D -m 0644 %{SOURCE15} %{buildroot}%{_sysconfdir}/systemd/system/%{name}.service.d/override.conf %{__install} -D -m 0644 Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/%{name}/logging.json %{__mkdir} -p %{buildroot}%{_bindir} tee %{buildroot}%{_bindir}/jellyfin << EOF @@ -64,11 +83,11 @@ EOF %{__mkdir} -p %{buildroot}%{_var}/log/jellyfin %{__mkdir} -p %{buildroot}%{_var}/cache/jellyfin -%{__install} -D -m 0644 %{SOURCE1} %{buildroot}%{_unitdir}/%{name}.service -%{__install} -D -m 0644 %{SOURCE2} %{buildroot}%{_sysconfdir}/sysconfig/%{name} -%{__install} -D -m 0600 %{SOURCE3} %{buildroot}%{_sysconfdir}/sudoers.d/%{name}-sudoers -%{__install} -D -m 0755 %{SOURCE4} %{buildroot}%{_libexecdir}/%{name}/restart.sh -%{__install} -D -m 0644 %{SOURCE6} %{buildroot}%{_prefix}/lib/firewalld/services/%{name}.xml +%{__install} -D -m 0644 %{SOURCE11} %{buildroot}%{_unitdir}/%{name}.service +%{__install} -D -m 0644 %{SOURCE12} %{buildroot}%{_sysconfdir}/sysconfig/%{name} +%{__install} -D -m 0600 %{SOURCE13} %{buildroot}%{_sysconfdir}/sudoers.d/%{name}-sudoers +%{__install} -D -m 0755 %{SOURCE14} %{buildroot}%{_libexecdir}/%{name}/restart.sh +%{__install} -D -m 0644 %{SOURCE16} %{buildroot}%{_prefix}/lib/firewalld/services/%{name}.xml %files %{_libdir}/%{name}/jellyfin-web/* @@ -80,7 +99,7 @@ EOF %{_libdir}/%{name}/createdump # Needs 755 else only root can run it since binary build by dotnet is 722 %attr(755,root,root) %{_libdir}/%{name}/jellyfin -%{_libdir}/%{name}/sosdocsunix.txt +%{_libdir}/%{name}/SOS_README.md %{_unitdir}/%{name}.service %{_libexecdir}/%{name}/restart.sh %{_prefix}/lib/firewalld/services/%{name}.xml diff --git a/deployment/windows/build-jellyfin.ps1 b/deployment/windows/build-jellyfin.ps1 index 5e06b9c06..dde6eb8fc 100644 --- a/deployment/windows/build-jellyfin.ps1 +++ b/deployment/windows/build-jellyfin.ps1 @@ -8,6 +8,7 @@ param( [switch]$GenerateZip, [string]$InstallLocation = "./dist/jellyfin-win-nsis", [string]$UXLocation = "../jellyfin-ux", + [switch]$InstallTrayApp, [ValidateSet('Debug','Release')][string]$BuildType = 'Release', [ValidateSet('Quiet','Minimal', 'Normal')][string]$DotNetVerbosity = 'Minimal', [ValidateSet('win','win7', 'win8','win81','win10')][string]$WindowsVersion = 'win', @@ -132,6 +133,23 @@ function Cleanup-NSIS { Remove-Item "$tempdir/nsis/" -Recurse -Force -ErrorAction Continue | Write-Verbose Remove-Item "$tempdir/nsis.zip" -Force -ErrorAction Continue | Write-Verbose } + +function Install-TrayApp { + param( + [string]$ResolvedInstallLocation, + [string]$Architecture + ) + Write-Verbose "Checking Architecture" + if($Architecture -ne 'x64'){ + Write-Warning "No builds available for your selected architecture of $Architecture" + Write-Warning "The tray app will not be available." + }else{ + Write-Verbose "Downloading Tray App and copying to Jellyfin location" + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-WebRequest -Uri https://github.com/jellyfin/jellyfin-windows-tray/releases/latest/download/JellyfinTray.exe -UseBasicParsing -OutFile "$installLocation/JellyfinTray.exe" | Write-Verbose + } +} + if(-not $SkipJellyfinBuild.IsPresent -and -not ($InstallNSIS -eq $true)){ Write-Verbose "Starting Build Process: Selected Environment is $WindowsVersion-$Architecture" Build-JellyFin @@ -144,6 +162,10 @@ if($InstallNSSM.IsPresent -or ($InstallNSSM -eq $true)){ Write-Verbose "Starting NSSM Install" Install-NSSM $ResolvedInstallLocation $Architecture } +if($InstallTrayApp.IsPresent -or ($InstallTrayApp -eq $true)){ + Write-Verbose "Downloading Windows Tray App" + Install-TrayApp $ResolvedInstallLocation $Architecture +} #Copy-Item .\deployment\windows\install-jellyfin.ps1 $ResolvedInstallLocation\install-jellyfin.ps1 #Copy-Item .\deployment\windows\install.bat $ResolvedInstallLocation\install.bat Copy-Item .\LICENSE $ResolvedInstallLocation\LICENSE diff --git a/deployment/windows/dialogs/setuptype.nsddef b/deployment/windows/dialogs/setuptype.nsddef new file mode 100644 index 000000000..b55ceeaaa --- /dev/null +++ b/deployment/windows/dialogs/setuptype.nsddef @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +This file was created by NSISDialogDesigner 1.4.4.0 +http://coolsoft.altervista.org/nsisdialogdesigner +Do not edit manually! +--> +<Dialog Name="setuptype" Title="Setup Type" Subtitle="Control how Jellyfin is installed."> + <Label Name="InstallasaServiceLabel" Location="12, 115" Size="426, 46" Text="Install Jellyfin as a service. This method is recommended for Advanced Users. Additional setup is required to access network shares." TabIndex="0" /> + <RadioButton Name="InstallasaService" Location="12, 88" Size="426, 24" Text="Install as a Service (Advanced Users)" TabIndex="1" /> + <Label Name="BasicInstallLabel" Location="12, 39" Size="426, 46" Text="The basic install will run Jellyfin in your current user account.$\nThis is recommended for new users and those with existing Jellyfin installs older than 10.4." TabIndex="2" /> + <RadioButton Name="BasicInstall" Location="12, 12" Size="426, 24" Text="Basic Install (Recommended)" Font="Microsoft Sans Serif, 8.25pt, style=Bold" Checked="True" TabIndex="3" /> +</Dialog>
\ No newline at end of file diff --git a/deployment/windows/dialogs/setuptype.nsdinc b/deployment/windows/dialogs/setuptype.nsdinc new file mode 100644 index 000000000..8746ad2cc --- /dev/null +++ b/deployment/windows/dialogs/setuptype.nsdinc @@ -0,0 +1,50 @@ +; ========================================================= +; This file was generated by NSISDialogDesigner 1.4.4.0 +; http://coolsoft.altervista.org/nsisdialogdesigner +; +; Do not edit it manually, use NSISDialogDesigner instead! +; ========================================================= + +; handle variables +Var hCtl_setuptype +Var hCtl_setuptype_InstallasaServiceLabel +Var hCtl_setuptype_InstallasaService +Var hCtl_setuptype_BasicInstallLabel +Var hCtl_setuptype_BasicInstall +Var hCtl_setuptype_Font1 + + +; dialog create function +Function fnc_setuptype_Create + + ; custom font definitions + CreateFont $hCtl_setuptype_Font1 "Microsoft Sans Serif" "8.25" "700" + + ; === setuptype (type: Dialog) === + nsDialogs::Create 1018 + Pop $hCtl_setuptype + ${If} $hCtl_setuptype == error + Abort + ${EndIf} + !insertmacro MUI_HEADER_TEXT "Setup Type" "Control how Jellyfin is installed." + + ; === InstallasaServiceLabel (type: Label) === + ${NSD_CreateLabel} 8u 71u 280u 28u "Install Jellyfin as a service. This method is recommended for Advanced Users. Additional setup is required to access network shares." + Pop $hCtl_setuptype_InstallasaServiceLabel + + ; === InstallasaService (type: RadioButton) === + ${NSD_CreateRadioButton} 8u 54u 280u 15u "Install as a Service (Advanced Users)" + Pop $hCtl_setuptype_InstallasaService + ${NSD_AddStyle} $hCtl_setuptype_InstallasaService ${WS_GROUP} + + ; === BasicInstallLabel (type: Label) === + ${NSD_CreateLabel} 8u 24u 280u 28u "The basic install will run Jellyfin in your current user account.$\nThis is recommended for new users and those with existing Jellyfin installs older than 10.4." + Pop $hCtl_setuptype_BasicInstallLabel + + ; === BasicInstall (type: RadioButton) === + ${NSD_CreateRadioButton} 8u 7u 280u 15u "Basic Install (Recommended)" + Pop $hCtl_setuptype_BasicInstall + SendMessage $hCtl_setuptype_BasicInstall ${WM_SETFONT} $hCtl_setuptype_Font1 0 + ${NSD_Check} $hCtl_setuptype_BasicInstall + +FunctionEnd diff --git a/deployment/windows/jellyfin.nsi b/deployment/windows/jellyfin.nsi index e33efde91..5666d30f0 100644 --- a/deployment/windows/jellyfin.nsi +++ b/deployment/windows/jellyfin.nsi @@ -16,11 +16,14 @@ ShowUninstDetails show ; Global variables that we'll use Var _JELLYFINVERSION_ Var _JELLYFINDATADIR_ + Var _SETUPTYPE_ Var _INSTALLSERVICE_ Var _SERVICESTART_ Var _SERVICEACCOUNTTYPE_ Var _EXISTINGINSTALLATION_ Var _EXISTINGSERVICE_ + Var _MAKESHORTCUTS_ + Var _FOLDEREXISTS_ ; !ifdef x64 !define ARCH "x64" @@ -86,7 +89,12 @@ ShowUninstDetails show !insertmacro MUI_PAGE_WELCOME ; License Page !insertmacro MUI_PAGE_LICENSE "$%InstallLocation%\LICENSE" ; picking up generic GPL + +; Setup Type Page + Page custom ShowSetupTypePage SetupTypePage_Config + ; Components Page + !define MUI_PAGE_CUSTOMFUNCTION_PRE HideComponentsPage !insertmacro MUI_PAGE_COMPONENTS !define MUI_PAGE_CUSTOMFUNCTION_PRE HideInstallDirectoryPage ; Controls when to hide / show !define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Install folder" ; shows just above the folder selection dialog @@ -102,6 +110,7 @@ ShowUninstDetails show !insertmacro MUI_PAGE_DIRECTORY ; Custom Dialogs + !include "dialogs\setuptype.nsdinc" !include "dialogs\service-config.nsdinc" !include "dialogs\confirmation.nsdinc" @@ -155,7 +164,9 @@ Section "!Jellyfin Server (required)" InstallJellyfinServer SetOutPath "$INSTDIR" + File "/oname=icon.ico" "${UXPATH}\branding\NSIS\modern-install.ico" File /r $%InstallLocation%\* + ; Write the InstallFolder, DataFolder, Network Service info into the registry for later use WriteRegExpandStr HKLM "${REG_CONFIG_KEY}" "InstallFolder" "$INSTDIR" @@ -170,7 +181,7 @@ Section "!Jellyfin Server (required)" InstallJellyfinServer WriteRegExpandStr HKLM "${REG_UNINST_KEY}" "UninstallString" '"$INSTDIR\Uninstall.exe"' WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayIcon" '"$INSTDIR\Uninstall.exe",0' WriteRegStr HKLM "${REG_UNINST_KEY}" "Publisher" "The Jellyfin Project" - WriteRegStr HKLM "${REG_UNINST_KEY}" "URLInfoAbout" "https://jellyfin.media/" + WriteRegStr HKLM "${REG_UNINST_KEY}" "URLInfoAbout" "https://jellyfin.org/" WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayVersion" "$_JELLYFINVERSION_" WriteRegDWORD HKLM "${REG_UNINST_KEY}" "NoModify" 1 WriteRegDWORD HKLM "${REG_UNINST_KEY}" "NoRepair" 1 @@ -180,12 +191,12 @@ Section "!Jellyfin Server (required)" InstallJellyfinServer SectionEnd Section "Jellyfin Server Service" InstallService - +${If} $_INSTALLSERVICE_ == "Yes" ; Only run this if we're going to install the service! ExecWait '"$INSTDIR\nssm.exe" statuscode JellyfinServer' $0 DetailPrint "Jellyfin Server service statuscode, $0" ${If} $0 == 0 InstallRetry: - ExecWait '"$INSTDIR\nssm.exe" install JellyfinServer "$INSTDIR\jellyfin.exe" --datadir \"$_JELLYFINDATADIR_\"' $0 + ExecWait '"$INSTDIR\nssm.exe" install JellyfinServer "$INSTDIR\jellyfin.exe" --service --datadir \"$_JELLYFINDATADIR_\"' $0 ${If} $0 <> 0 !insertmacro ShowError "Could not install the Jellyfin Server service." InstallRetry ${EndIf} @@ -201,7 +212,7 @@ Section "Jellyfin Server Service" InstallService DetailPrint "Jellyfin Server Service setting (Application), $0" ConfigureAppParametersRetry: - ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer AppParameters --datadir \"$_JELLYFINDATADIR_\"' $0 + ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer AppParameters --service --datadir \"$_JELLYFINDATADIR_\"' $0 ${If} $0 <> 0 !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureAppParametersRetry ${EndIf} @@ -241,6 +252,15 @@ Section "Jellyfin Server Service" InstallService DetailPrint "Jellyfin Server service account change, $0" ${EndIf} + Sleep 3000 + ConfigureDefaultAppExit: + ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer AppExit Default Exit' $0 + ${If} $0 <> 0 + !insertmacro ShowError "Could not configure the Jellyfin Server service app exit action." ConfigureDefaultAppExit + ${EndIf} + DetailPrint "Jellyfin Server service exit action set, $0" +${EndIf} + SectionEnd Section "-start service" StartService @@ -255,6 +275,16 @@ ${AndIf} $_INSTALLSERVICE_ == "Yes" ${EndIf} SectionEnd +Section "Create Shortcuts" CreateWinShortcuts + ${If} $_MAKESHORTCUTS_ == "Yes" + CreateDirectory "$SMPROGRAMS\Jellyfin Server" + CreateShortCut "$SMPROGRAMS\Jellyfin Server\Jellyfin (View Console).lnk" "$INSTDIR\jellyfin.exe" "--datadir $\"$_JELLYFINDATADIR_$\"" "$INSTDIR\icon.ico" 0 SW_SHOWMAXIMIZED + CreateShortCut "$SMPROGRAMS\Jellyfin Server\Jellyfin Tray App.lnk" "$INSTDIR\jellyfintray.exe" "" "$INSTDIR\icon.ico" 0 + ;CreateShortCut "$DESKTOP\Jellyfin Server.lnk" "$INSTDIR\jellyfin.exe" "--datadir $\"$_JELLYFINDATADIR_$\"" "$INSTDIR\icon.ico" 0 SW_SHOWMINIMIZED + CreateShortCut "$DESKTOP\Jellyfin Server\Jellyfin Server.lnk" "$INSTDIR\jellyfintray.exe" "" "$INSTDIR\icon.ico" 0 + ${EndIf} +SectionEnd + ;-------------------------------- ;Descriptions @@ -275,6 +305,7 @@ Section "Uninstall" ReadRegStr $INSTDIR HKLM "${REG_CONFIG_KEY}" "InstallFolder" ; read the installation folder ReadRegStr $_JELLYFINDATADIR_ HKLM "${REG_CONFIG_KEY}" "DataFolder" ; read the data folder + ReadRegStr $_SERVICEACCOUNTTYPE_ HKLM "${REG_CONFIG_KEY}" "ServiceAccountType" ; read the account name DetailPrint "Jellyfin Install location: $INSTDIR" DetailPrint "Jellyfin Data folder: $_JELLYFINDATADIR_" @@ -307,13 +338,18 @@ Section "Uninstall" Sleep 3000 ; Give time for Windows to catchup - NoServiceUninstall: ; existing install was present but no service was detected + NoServiceUninstall: ; existing install was present but no service was detected. Remove shortcuts if account is set to none + ${If} $_SERVICEACCOUNTTYPE_ == "None" + RMDir /r "$SMPROGRAMS\Jellyfin Server" + Delete "$DESKTOP\Jellyfin Server.lnk" + DetailPrint "Removed old shortcuts..." + ${EndIf} Delete "$INSTDIR\*.*" RMDir /r /REBOOTOK "$INSTDIR\jellyfin-web" Delete "$INSTDIR\Uninstall.exe" RMDir /r /REBOOTOK "$INSTDIR" - + DeleteRegKey HKLM "Software\Jellyfin" DeleteRegKey HKLM "${REG_UNINST_KEY}" @@ -326,6 +362,7 @@ Function .onInit StrCpy $_SERVICEACCOUNTTYPE_ "NetworkService" StrCpy $_EXISTINGINSTALLATION_ "No" StrCpy $_EXISTINGSERVICE_ "No" + StrCpy $_MAKESHORTCUTS_ "No" SetShellVarContext current StrCpy $_JELLYFINDATADIR_ "$%ProgramData%\Jellyfin\Server" @@ -353,6 +390,16 @@ Function .onInit StrCpy $_EXISTINGINSTALLATION_ "Yes" ; Set our flag to be used later SectionSetText ${InstallJellyfinServer} "Upgrade Jellyfin Server (required)" ; Change install text to "Upgrade" + ; check if service was run using Network Service account + ClearErrors + ReadRegStr $_SERVICEACCOUNTTYPE_ HKLM "${REG_CONFIG_KEY}" "ServiceAccountType" ; in case of error _SERVICEACCOUNTTYPE_ will be NetworkService as default + + ClearErrors + ReadRegStr $_JELLYFINDATADIR_ HKLM "${REG_CONFIG_KEY}" "DataFolder" ; in case of error, the default holds + + ; Hide sections which will not be needed in case of previous install + ; SectionSetText ${InstallService} "" + ; check if there is a service called Jellyfin, there should be ; hack : nssm statuscode Jellyfin will return non zero return code in case it exists ExecWait '"$INSTDIR\nssm.exe" statuscode JellyfinServer' $0 @@ -363,18 +410,17 @@ Function .onInit StrCpy $_EXISTINGSERVICE_ "Yes" StrCpy $_INSTALLSERVICE_ "Yes" StrCpy $_SERVICESTART_ "Yes" + StrCpy $_MAKESHORTCUTS_ "No" + SectionSetText ${CreateWinShortcuts} "" - ; check if service was run using Network Service account - ClearErrors - ReadRegStr $_SERVICEACCOUNTTYPE_ HKLM "${REG_CONFIG_KEY}" "ServiceAccountType" ; in case of error _SERVICEACCOUNTTYPE_ will be NetworkService as default - - ClearErrors - ReadRegStr $_JELLYFINDATADIR_ HKLM "${REG_CONFIG_KEY}" "DataFolder" ; in case of error, the default holds - - ; Hide sections which will not be needed in case of previous install - ; SectionSetText ${InstallService} "" - + NoService: ; existing install was present but no service was detected + ${If} $_SERVICEACCOUNTTYPE_ == "None" + StrCpy $_SETUPTYPE_ "Basic" + StrCpy $_INSTALLSERVICE_ "No" + StrCpy $_SERVICESTART_ "No" + StrCpy $_MAKESHORTCUTS_ "Yes" + ${EndIf} ; Let the user know that we'll upgrade and provide an option to quit. MessageBox MB_OKCANCEL|MB_ICONINFORMATION "Existing installation of Jellyfin Server was detected, it'll be upgraded, settings will be retained. \ @@ -383,8 +429,7 @@ Function .onInit ProceedWithUpgrade: - NoExisitingInstall: -; by this time, the variables have been correctly set to reflect previous install details + NoExisitingInstall: ; by this time, the variables have been correctly set to reflect previous install details FunctionEnd @@ -413,6 +458,25 @@ Function HideConfirmationPage ${EndIf} FunctionEnd +Function HideSetupTypePage + ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for SetupType + Abort + ${EndIf} +FunctionEnd + +Function HideComponentsPage + ${If} $_SETUPTYPE_ == "Basic" ; Basic installation chosen, don't show components choice + Abort + ${EndIf} +FunctionEnd + +; Setup Type dialog show function +Function ShowSetupTypePage + Call HideSetupTypePage + Call fnc_setuptype_Create + nsDialogs::Show +FunctionEnd + ; Service Config dialog show function Function ShowServiceConfigPage Call HideServiceConfigPage @@ -431,6 +495,46 @@ FunctionEnd Var StartServiceAfterInstall Var UseNetworkServiceAccount Var UseLocalSystemAccount +Var BasicInstall + + +Function SetupTypePage_Config +${NSD_GetState} $hCtl_setuptype_BasicInstall $BasicInstall + IfFileExists "$LOCALAPPDATA\Jellyfin" folderfound foldernotfound ; if the folder exists, use this, otherwise, go with new default + folderfound: + StrCpy $_FOLDEREXISTS_ "Yes" + Goto InstallCheck + foldernotfound: + StrCpy $_FOLDEREXISTS_ "No" + Goto InstallCheck + +InstallCheck: +${If} $BasicInstall == 1 + StrCpy $_SETUPTYPE_ "Basic" + StrCpy $_INSTALLSERVICE_ "No" + StrCpy $_SERVICESTART_ "No" + StrCpy $_SERVICEACCOUNTTYPE_ "None" + StrCpy $_MAKESHORTCUTS_ "Yes" + ${If} $_FOLDEREXISTS_ == "Yes" + StrCpy $_JELLYFINDATADIR_ "$LOCALAPPDATA\Jellyfin\" + ${EndIf} +${Else} + StrCpy $_SETUPTYPE_ "Advanced" + StrCpy $_INSTALLSERVICE_ "Yes" + StrCpy $_MAKESHORTCUTS_ "No" + ${If} $_FOLDEREXISTS_ == "Yes" + MessageBox MB_OKCANCEL|MB_ICONINFORMATION "An existing data folder was detected.\ + $\r$\nBasic Setup is highly recommended.\ + $\r$\nIf you proceed, you will need to set up Jellyfin again." IDOK GoAhead IDCANCEL GoBack + GoBack: + Abort + ${EndIf} + GoAhead: + StrCpy $_JELLYFINDATADIR_ "$%ProgramData%\Jellyfin\Server" + SectionSetText ${CreateWinShortcuts} "" +${EndIf} + +FunctionEnd Function ServiceConfigPage_Config ${NSD_GetState} $hCtl_service_config_StartServiceAfterInstall $StartServiceAfterInstall diff --git a/jellyfin.ruleset b/jellyfin.ruleset index e259131b0..16d68567c 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -10,6 +10,8 @@ <Rule Id="SA1101" Action="None" /> <!-- disable warning SA1108: Block statements should not contain embedded comments --> <Rule Id="SA1108" Action="None" /> + <!-- disable warning SA1128:: Put constructor initializers on their own line --> + <Rule Id="SA1128" Action="None" /> <!-- disable warning SA1130: Use lambda syntax --> <Rule Id="SA1130" Action="None" /> <!-- disable warning SA1200: 'using' directive must appear within a namespace declaration --> diff --git a/tests/Jellyfin.Common.Tests/HexTests.cs b/tests/Jellyfin.Common.Tests/HexTests.cs new file mode 100644 index 000000000..5b578d38c --- /dev/null +++ b/tests/Jellyfin.Common.Tests/HexTests.cs @@ -0,0 +1,19 @@ +using MediaBrowser.Common; +using Xunit; + +namespace Jellyfin.Common.Tests +{ + public class HexTests + { + [Theory] + [InlineData("")] + [InlineData("00")] + [InlineData("01")] + [InlineData("000102030405060708090a0b0c0d0e0f")] + [InlineData("0123456789abcdef")] + public void RoundTripTest(string data) + { + Assert.Equal(data, Hex.Encode(Hex.Decode(data))); + } + } +} diff --git a/tests/Jellyfin.Common.Tests/PasswordHashTests.cs b/tests/Jellyfin.Common.Tests/PasswordHashTests.cs index 5fa86f3bd..03523dbc4 100644 --- a/tests/Jellyfin.Common.Tests/PasswordHashTests.cs +++ b/tests/Jellyfin.Common.Tests/PasswordHashTests.cs @@ -1,6 +1,6 @@ +using MediaBrowser.Common; using MediaBrowser.Common.Cryptography; using Xunit; -using static MediaBrowser.Common.HexHelper; namespace Jellyfin.Common.Tests { @@ -15,8 +15,8 @@ namespace Jellyfin.Common.Tests { var pass = PasswordHash.Parse(passwordHash); Assert.Equal(id, pass.Id); - Assert.Equal(salt, ToHexString(pass.Salt)); - Assert.Equal(hash, ToHexString(pass.Hash)); + Assert.Equal(salt, Hex.Encode(pass.Salt, false)); + Assert.Equal(hash, Hex.Encode(pass.Hash, false)); } [Theory] diff --git a/tests/Jellyfin.Naming.Tests/EpisodePathParserTest.cs b/tests/Jellyfin.Naming.Tests/EpisodePathParserTest.cs new file mode 100644 index 000000000..28ccd6df1 --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/EpisodePathParserTest.cs @@ -0,0 +1,54 @@ +namespace Emby.Naming.TV +{ + using Emby.Naming.Common; + using Xunit; + + public class EpisodePathParserTest + { + [Theory] + [InlineData("/media/Foo/Foo-S01E01", "Foo", 1, 1)] + [InlineData("/media/Foo - S04E011", "Foo", 4, 11)] + [InlineData("/media/Foo/Foo s01x01", "Foo", 1, 1)] + [InlineData("/media/Foo (2019)/Season 4/Foo (2019).S04E03", "Foo (2019)", 4, 3)] + public void ParseEpisodesCorrectly(string path, string name, int season, int episode) + { + NamingOptions o = new NamingOptions(); + EpisodePathParser p = new EpisodePathParser(o); + var res = p.Parse(path, false); + + Assert.True(res.Success); + Assert.Equal(name, res.SeriesName); + Assert.Equal(season, res.SeasonNumber); + Assert.Equal(episode, res.EpisodeNumber); + + //testing other paths delimeter + var res2 = p.Parse(path.Replace("/", "\\"), false); + Assert.True(res2.Success); + Assert.Equal(name, res2.SeriesName); + Assert.Equal(season, res2.SeasonNumber); + Assert.Equal(episode, res2.EpisodeNumber); + } + + [Theory] + [InlineData("/media/Foo/Foo 889", "Foo", 889)] + [InlineData("/media/Foo/[Bar] Foo Baz - 11 [1080p]", "Foo Baz", 11)] + public void ParseEpisodeWithoutSeason(string path, string name, int episode) + { + NamingOptions o = new NamingOptions(); + EpisodePathParser p = new EpisodePathParser(o); + var res = p.Parse(path, true, null, null, true); + + Assert.True(res.Success); + Assert.Equal(name, res.SeriesName); + Assert.True(res.SeasonNumber == null); + Assert.Equal(episode, res.EpisodeNumber); + + //testing other paths delimeter + var res2 = p.Parse(path.Replace("/", "\\"), false, null, null, true); + Assert.True(res2.Success); + Assert.Equal(name, res2.SeriesName); + Assert.True(res2.SeasonNumber == null); + Assert.Equal(episode, res2.EpisodeNumber); + } + } +}
\ No newline at end of file diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj new file mode 100644 index 000000000..f5e151348 --- /dev/null +++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj @@ -0,0 +1,20 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>netcoreapp3.0</TargetFramework> + + <IsPackable>false</IsPackable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" /> + <PackageReference Include="xunit" Version="2.4.0" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" /> + <PackageReference Include="coverlet.collector" Version="1.0.1" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\Emby.Naming\Emby.Naming.csproj" /> + </ItemGroup> + +</Project> |
