diff options
| author | Andrew Mahone <andrew.mahone@gmail.com> | 2019-11-04 10:03:18 -0500 |
|---|---|---|
| committer | Andrew Mahone <andrew.mahone@gmail.com> | 2019-11-04 10:03:18 -0500 |
| commit | 1600d5b53f89ad4f1af39a6a53d0ef251341b278 (patch) | |
| tree | 12f5483eba2396ad9b8da2e46f6a0ced073cf5e7 | |
| parent | 90dfe729bb9f8929bf584b93f93aabd84abbef3d (diff) | |
| parent | 9756bdb76ea625c19b6743a34ec95759d3f6d050 (diff) | |
Merge remote-tracking branch 'origin/master' into media-attachments-clean
143 files changed, 1754 insertions, 1195 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/.vscode/launch.json b/.vscode/launch.json index 21b8323ea..e2a09c0f1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp2.1/jellyfin.dll", + "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.0/jellyfin.dll", "args": [], "cwd": "${workspaceFolder}/Jellyfin.Server", // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window @@ -25,4 +25,4 @@ "processId": "${command:pickProcess}" } ,] -}
\ No newline at end of file +} diff --git a/Dockerfile b/Dockerfile index 118acfc0f..c24c32181 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ -ARG DOTNET_VERSION=2.2 +ARG DOTNET_VERSION=3.0 ARG FFMPEG_VERSION=latest FROM node:alpine as web-builder -ARG JELLYFIN_WEB_VERSION=v10.5.0 +ARG JELLYFIN_WEB_VERSION=master RUN apk add curl \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && cd jellyfin-web-* \ @@ -10,33 +10,39 @@ RUN apk add curl \ && yarn build \ && mv dist /dist -FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder +FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION}-buster as builder WORKDIR /repo COPY . . ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" FROM jellyfin/ffmpeg:${FFMPEG_VERSION} as ffmpeg +FROM debian:buster-slim -FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION} -COPY --from=ffmpeg / / +COPY --from=ffmpeg /opt/ffmpeg /opt/ffmpeg COPY --from=builder /jellyfin /jellyfin COPY --from=web-builder /dist /jellyfin/jellyfin-web # Install dependencies: # libfontconfig1: needed for Skia +# libgomp1: needed for ffmpeg +# libva-drm2: needed for ffmpeg # mesa-va-drivers: needed for VAAPI RUN apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y \ - libfontconfig1 mesa-va-drivers \ + libfontconfig1 libgomp1 libva-drm2 mesa-va-drivers openssl \ && apt-get clean autoclean \ && apt-get autoremove \ && rm -rf /var/lib/apt/lists/* \ && mkdir -p /cache /config /media \ - && chmod 777 /cache /config /media + && chmod 777 /cache /config /media \ + && ln -s /opt/ffmpeg/bin/ffmpeg /usr/local/bin \ + && ln -s /opt/ffmpeg/bin/ffprobe /usr/local/bin + +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 EXPOSE 8096 VOLUME /cache /config /media -ENTRYPOINT dotnet /jellyfin/jellyfin.dll \ +ENTRYPOINT ./jellyfin/jellyfin \ --datadir /config \ --cachedir /cache \ --ffmpeg /usr/local/bin/ffmpeg diff --git a/Dockerfile.arm b/Dockerfile.arm index ec710620a..f8c8511ae 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -4,7 +4,7 @@ ARG DOTNET_VERSION=3.0 FROM node:alpine as web-builder -ARG JELLYFIN_WEB_VERSION=v10.5.0 +ARG JELLYFIN_WEB_VERSION=master RUN apk add curl \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && cd jellyfin-web-* \ @@ -17,8 +17,6 @@ FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder WORKDIR /repo COPY . . ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 -# TODO Remove or update the sed line when we update dotnet version. -RUN find . -type f -exec sed -i 's/netcoreapp2.1/netcoreapp3.0/g' {} \; # Discard objs - may cause failures if exists RUN find . -type d -name obj | xargs -r rm -r # Build @@ -26,7 +24,7 @@ RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" FROM multiarch/qemu-user-static:x86_64-arm as qemu -FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}-stretch-slim-arm32v7 +FROM debian:stretch-slim-arm32v7 COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin RUN apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \ @@ -36,9 +34,11 @@ RUN apt-get update \ COPY --from=builder /jellyfin /jellyfin COPY --from=web-builder /dist /jellyfin/jellyfin-web +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 + EXPOSE 8096 VOLUME /cache /config /media -ENTRYPOINT dotnet /jellyfin/jellyfin.dll \ +ENTRYPOINT ./jellyfin/jellyfin \ --datadir /config \ --cachedir /cache \ --ffmpeg /usr/bin/ffmpeg diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 30de0bab4..9b343659f 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -4,7 +4,7 @@ ARG DOTNET_VERSION=3.0 FROM node:alpine as web-builder -ARG JELLYFIN_WEB_VERSION=v10.5.0 +ARG JELLYFIN_WEB_VERSION=master RUN apk add curl \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && cd jellyfin-web-* \ @@ -17,8 +17,6 @@ FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder WORKDIR /repo COPY . . ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 -# TODO Remove or update the sed line when we update dotnet version. -RUN find . -type f -exec sed -i 's/netcoreapp2.1/netcoreapp3.0/g' {} \; # Discard objs - may cause failures if exists RUN find . -type d -name obj | xargs -r rm -r # Build @@ -26,7 +24,7 @@ RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu -FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}-stretch-slim-arm64v8 +FROM debian:stretch-slim-arm64v8 COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin RUN apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \ @@ -36,9 +34,11 @@ RUN apt-get update \ COPY --from=builder /jellyfin /jellyfin COPY --from=web-builder /dist /jellyfin/jellyfin-web +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 + EXPOSE 8096 VOLUME /cache /config /media -ENTRYPOINT dotnet /jellyfin/jellyfin.dll \ +ENTRYPOINT ./jellyfin/jellyfin \ --datadir /config \ --cachedir /cache \ --ffmpeg /usr/bin/ffmpeg 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.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/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 bcd99ffe4..ac37cfe07 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -364,7 +364,7 @@ namespace Emby.Server.Implementations { _configuration = configuration; - XmlSerializer = new MyXmlSerializer(fileSystem, loggerFactory); + XmlSerializer = new MyXmlSerializer(); NetworkManager = networkManager; networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets; @@ -912,7 +912,7 @@ namespace Emby.Server.Implementations _displayPreferencesRepository.Initialize(); - var userDataRepo = new SqliteUserDataRepository(LoggerFactory, ApplicationPaths); + var userDataRepo = new SqliteUserDataRepository(LoggerFactory.CreateLogger<SqliteUserDataRepository>(), ApplicationPaths); SetStaticProperties(); 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 d02aa9b1f..08f53f0f5 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -563,7 +563,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(); @@ -2003,7 +2003,7 @@ namespace Emby.Server.Implementations.Data throw new ArgumentNullException(nameof(chapters)); } - var idBlob = id.ToGuidBlob(); + var idBlob = id.ToByteArray(); using (var connection = GetConnection()) { @@ -3782,7 +3782,7 @@ namespace Emby.Server.Implementations.Data if (statement != null) { - statement.TryBind(paramName, personId.ToGuidBlob()); + statement.TryBind(paramName, personId.ToByteArray()); } index++; } @@ -3993,7 +3993,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++; } @@ -4012,7 +4012,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++; } @@ -4031,7 +4031,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++; } @@ -4050,7 +4050,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++; } @@ -4069,7 +4069,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++; } @@ -4088,7 +4088,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++; } @@ -4159,7 +4159,7 @@ namespace Emby.Server.Implementations.Data if (statement != null) { - statement.TryBind(paramName, studioId.ToGuidBlob()); + statement.TryBind(paramName, studioId.ToByteArray()); } index++; } @@ -4935,7 +4935,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); @@ -5054,7 +5054,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)) @@ -5062,7 +5062,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(); @@ -5131,7 +5131,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(); @@ -5165,7 +5165,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)); } @@ -5630,7 +5630,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); @@ -5654,10 +5654,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; } @@ -5710,7 +5713,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); @@ -5829,7 +5832,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) { @@ -5871,7 +5874,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..d6ca19e3f 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" /> @@ -29,9 +29,9 @@ <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="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..d55dc2f18 100644 --- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -27,6 +27,11 @@ namespace Emby.Server.Implementations.EntryPoints private NatManager _natManager; + private readonly object _createdRulesLock = new object(); + private List<string> _createdRules = new List<string>(); + private readonly object _usnsHandledLock = new object(); + private List<string> _usnsHandled = new List<string>(); + public ExternalPortForwarding(ILoggerFactory loggerFactory, IServerApplicationHost appHost, IServerConfigurationManager config, IDeviceDiscovery deviceDiscovery, IHttpClient httpClient) { _logger = loggerFactory.CreateLogger("PortMapper"); @@ -127,12 +132,13 @@ namespace Emby.Server.Implementations.EntryPoints return; } - lock (_usnsHandled) + lock (_usnsHandledLock) { if (_usnsHandled.Contains(identifier)) { return; } + _usnsHandled.Add(identifier); } @@ -186,11 +192,12 @@ namespace Emby.Server.Implementations.EntryPoints private void ClearCreatedRules(object state) { - lock (_createdRules) + lock (_createdRulesLock) { _createdRules.Clear(); } - lock (_usnsHandled) + + lock (_usnsHandledLock) { _usnsHandled.Clear(); } @@ -216,8 +223,6 @@ namespace Emby.Server.Implementations.EntryPoints } } - private List<string> _createdRules = new List<string>(); - private List<string> _usnsHandled = new List<string>(); private async void CreateRules(INatDevice device) { if (_disposed) @@ -231,7 +236,7 @@ namespace Emby.Server.Implementations.EntryPoints var addressString = address.ToString(); - lock (_createdRules) + lock (_createdRulesLock) { if (!_createdRules.Contains(addressString)) { 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 87e951f25..13857c1e8 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); @@ -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; @@ -2193,7 +2193,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 @@ -2261,7 +2261,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 @@ -2338,7 +2338,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 diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index e069ebd17..2e24b847b 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -160,12 +160,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/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index 6bfedb712..9805992be 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -5,7 +5,7 @@ "Artists": "Artistes", "AuthenticationSucceededWithUserName": "{0} s'est authentifié avec succès", "Books": "Livres", - "CameraImageUploadedFrom": "Une image de caméra a été chargée depuis {0}", + "CameraImageUploadedFrom": "Une nouvelle image de caméra a été chargée depuis {0}", "Channels": "Chaînes", "ChapterNameValue": "Chapitre {0}", "Collections": "Collections", @@ -17,7 +17,7 @@ "Genres": "Genres", "HeaderAlbumArtists": "Artistes de l'album", "HeaderCameraUploads": "Photos transférées", - "HeaderContinueWatching": "Reprendre", + "HeaderContinueWatching": "Continuer à regarder", "HeaderFavoriteAlbums": "Albums favoris", "HeaderFavoriteArtists": "Artistes favoris", "HeaderFavoriteEpisodes": "Épisodes favoris", @@ -34,14 +34,14 @@ "LabelRunningTimeValue": "Durée : {0}", "Latest": "Derniers", "MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour", - "MessageApplicationUpdatedTo": "Jellyfin Serveur a été mis à jour en version {0}", + "MessageApplicationUpdatedTo": "Le serveur Jellyfin a été mis à jour vers {0}", "MessageNamedServerConfigurationUpdatedWithValue": "La configuration de la section {0} du serveur a été mise à jour", "MessageServerConfigurationUpdated": "La configuration du serveur a été mise à jour", "MixedContent": "Contenu mixte", "Movies": "Films", "Music": "Musique", "MusicVideos": "Vidéos musicales", - "NameInstallFailed": "{0} échec d'installation", + "NameInstallFailed": "{0} échec de l'installation", "NameSeasonNumber": "Saison {0}", "NameSeasonUnknown": "Saison Inconnue", "NewVersionIsAvailable": "Une nouvelle version de Jellyfin Serveur est disponible au téléchargement.", @@ -50,7 +50,7 @@ "NotificationOptionAudioPlayback": "Lecture audio démarrée", "NotificationOptionAudioPlaybackStopped": "Lecture audio arrêtée", "NotificationOptionCameraImageUploaded": "L'image de l'appareil photo a été transférée", - "NotificationOptionInstallationFailed": "Échec d'installation", + "NotificationOptionInstallationFailed": "Échec de l'installation", "NotificationOptionNewLibraryContent": "Nouveau contenu ajouté", "NotificationOptionPluginError": "Erreur d'extension", "NotificationOptionPluginInstalled": "Extension installée", @@ -91,7 +91,7 @@ "UserPolicyUpdatedWithName": "La politique de l'utilisateur a été mise à jour pour {0}", "UserStartedPlayingItemWithValues": "{0} est en train de lire {1} sur {2}", "UserStoppedPlayingItemWithValues": "{0} vient d'arrêter la lecture de {1} sur {2}", - "ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre librairie", + "ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque", "ValueSpecialEpisodeName": "Spécial - {0}", "VersionNumber": "Version {0}" } 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/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index 3cc95e46e..3b5ce29c7 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -5,26 +5,26 @@ "Artists": "Sanatçılar", "AuthenticationSucceededWithUserName": "{0} kimlik başarıyla doğrulandı", "Books": "Kitaplar", - "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi", "Channels": "Kanallar", - "ChapterNameValue": "Chapter {0}", - "Collections": "Collections", - "DeviceOfflineWithName": "{0} has disconnected", - "DeviceOnlineWithName": "{0} is connected", - "FailedLoginAttemptWithUserName": "Failed login attempt from {0}", - "Favorites": "Favorites", - "Folders": "Folders", - "Genres": "Genres", - "HeaderAlbumArtists": "Album Artists", - "HeaderCameraUploads": "Camera Uploads", + "ChapterNameValue": "Bölüm {0}", + "Collections": "Koleksiyonlar", + "DeviceOfflineWithName": "{0} bağlantısı kesildi", + "DeviceOnlineWithName": "{0} bağlı", + "FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu", + "Favorites": "Favoriler", + "Folders": "Klasörler", + "Genres": "Türler", + "HeaderAlbumArtists": "Albüm Sanatçıları", + "HeaderCameraUploads": "Kamera Yüklemeleri", "HeaderContinueWatching": "İzlemeye Devam Et", "HeaderFavoriteAlbums": "Favori Albümler", - "HeaderFavoriteArtists": "Favorite Artists", - "HeaderFavoriteEpisodes": "Favorite Episodes", - "HeaderFavoriteShows": "Favori Showlar", - "HeaderFavoriteSongs": "Favorite Songs", - "HeaderLiveTV": "Live TV", - "HeaderNextUp": "Next Up", + "HeaderFavoriteArtists": "Favori Sanatçılar", + "HeaderFavoriteEpisodes": "Favori Bölümler", + "HeaderFavoriteShows": "Favori Diziler", + "HeaderFavoriteSongs": "Favori Şarkılar", + "HeaderLiveTV": "Canlı TV", + "HeaderNextUp": "Sonraki hafta", "HeaderRecordingGroups": "Recording Groups", "HomeVideos": "Home videos", "Inherit": "Inherit", @@ -38,7 +38,7 @@ "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageServerConfigurationUpdated": "Server configuration has been updated", "MixedContent": "Mixed content", - "Movies": "Movies", + "Movies": "Filmler", "Music": "Müzik", "MusicVideos": "Müzik videoları", "NameInstallFailed": "{0} kurulum başarısız", @@ -61,8 +61,8 @@ "NotificationOptionUserLockedOut": "User locked out", "NotificationOptionVideoPlayback": "Video playback started", "NotificationOptionVideoPlaybackStopped": "Video playback stopped", - "Photos": "Photos", - "Playlists": "Playlists", + "Photos": "Fotoğraflar", + "Playlists": "Çalma listeleri", "Plugin": "Plugin", "PluginInstalledWithName": "{0} was installed", "PluginUninstalledWithName": "{0} was uninstalled", @@ -71,13 +71,13 @@ "ScheduledTaskFailedWithName": "{0} failed", "ScheduledTaskStartedWithName": "{0} started", "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", - "Shows": "Shows", - "Songs": "Songs", + "Shows": "Diziler", + "Songs": "Şarkılar", "StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "SubtitlesDownloadedForItem": "Subtitles downloaded for {0}", - "Sync": "Sync", + "Sync": "Eşitle", "System": "System", "TvShows": "TV Shows", "User": "User", @@ -92,6 +92,6 @@ "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", - "ValueSpecialEpisodeName": "Special - {0}", + "ValueSpecialEpisodeName": "Özel -{0}", "VersionNumber": "Version {0}" } 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/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 3ab19769a..e8bd0cd30 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -376,7 +376,7 @@ namespace Jellyfin.Server return new ConfigurationBuilder() .SetBasePath(appPaths.ConfigurationDirectoryPath) - .AddJsonFile("logging.json") + .AddJsonFile("logging.json", false, true) .AddEnvironmentVariables("JELLYFIN_") .AddInMemoryCollection(ConfigurationOptions.Configuration) .Build(); 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/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 2e2b465a0..cba2c9dda 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1345,7 +1345,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() @@ -2198,7 +2198,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/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..bd4eeea18 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -15,7 +15,7 @@ <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.Text.Json" Version="4.6.0" /> </ItemGroup> 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/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/Mono.Nat/Mono.Nat.csproj b/Mono.Nat/Mono.Nat.csproj index edfd5c9bb..c143000b3 100644 --- a/Mono.Nat/Mono.Nat.csproj +++ b/Mono.Nat/Mono.Nat.csproj @@ -10,7 +10,7 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> </PropertyGroup> 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/win-x64/docker-build.sh b/deployment/win-x64/docker-build.sh index 20bf430c8..3f1ad78b5 100755 --- a/deployment/win-x64/docker-build.sh +++ b/deployment/win-x64/docker-build.sh @@ -7,7 +7,7 @@ set -o xtrace # Version variables NSSM_VERSION="nssm-2.24-101-g897c7ad" -NSSM_URL="https://nssm.cc/ci/${NSSM_VERSION}.zip" +NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip" FFMPEG_VERSION="ffmpeg-4.0.2-win64-static" FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win64/static/${FFMPEG_VERSION}.zip" diff --git a/deployment/win-x86/docker-build.sh b/deployment/win-x86/docker-build.sh index c5f6e82e7..7d79ba495 100755 --- a/deployment/win-x86/docker-build.sh +++ b/deployment/win-x86/docker-build.sh @@ -7,7 +7,7 @@ set -o xtrace # Version variables NSSM_VERSION="nssm-2.24-101-g897c7ad" -NSSM_URL="https://nssm.cc/ci/${NSSM_VERSION}.zip" +NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip" FFMPEG_VERSION="ffmpeg-4.0.2-win32-static" FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win32/static/${FFMPEG_VERSION}.zip" diff --git a/deployment/windows/build-jellyfin.ps1 b/deployment/windows/build-jellyfin.ps1 index c4fb4b995..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', @@ -84,8 +85,9 @@ function Install-NSSM { Write-Warning "NSSM will not be installed" }else{ Write-Verbose "Downloading NSSM" - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - Invoke-WebRequest -Uri https://nssm.cc/ci/nssm-2.24-101-g897c7ad.zip -UseBasicParsing -OutFile "$tempdir/nssm.zip" | Write-Verbose + # [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + # Temporary workaround, file is hosted in an azure blob with a custom domain in front for brevity + Invoke-WebRequest -Uri http://files.evilt.win/nssm/nssm-2.24-101-g897c7ad.zip -UseBasicParsing -OutFile "$tempdir/nssm.zip" | Write-Verbose } Expand-Archive "$tempdir/nssm.zip" -DestinationPath "$tempdir/nssm/" -Force | Write-Verbose @@ -131,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 @@ -143,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] |
