diff options
349 files changed, 4205 insertions, 5045 deletions
diff --git a/.ci/azure-pipelines-compat.yml b/.ci/azure-pipelines-compat.yml index 762bbdcb2..de60d2ccb 100644 --- a/.ci/azure-pipelines-compat.yml +++ b/.ci/azure-pipelines-compat.yml @@ -23,7 +23,7 @@ jobs: NugetPackageName: ${{ Package.value.NugetPackageName }} AssemblyFileName: ${{ Package.value.AssemblyFileName }} maxParallel: 2 - dependsOn: MainBuild + dependsOn: Build steps: - checkout: none diff --git a/.ci/azure-pipelines-main.yml b/.ci/azure-pipelines-main.yml index e33ab72f2..d155624ab 100644 --- a/.ci/azure-pipelines-main.yml +++ b/.ci/azure-pipelines-main.yml @@ -4,15 +4,14 @@ parameters: DotNetSdkVersion: 3.1.100 jobs: - - job: MainBuild - displayName: Main Build + - job: Build + displayName: Build strategy: matrix: Release: BuildConfiguration: Release Debug: BuildConfiguration: Debug - maxParallel: 2 pool: vmImage: "${{ parameters.LinuxImage }}" steps: @@ -22,13 +21,13 @@ jobs: persistCredentials: true - task: CmdLine@2 - displayName: "Clone Web Client (Master, Release, or Tag)" + displayName: "Clone Web Branch" condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion')) inputs: script: "git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web" - task: CmdLine@2 - displayName: "Clone Web Client (PR)" + displayName: "Clone Web Target" condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest')) inputs: script: "git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web" @@ -37,13 +36,13 @@ jobs: displayName: "Install Node" condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion')) inputs: - versionSpec: "10.x" + versionSpec: "12.x" - task: CmdLine@2 displayName: "Build Web Client" condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), eq(variables['BuildConfiguration'], 'Release'), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion')) inputs: - script: yarn install && yarn build + script: yarn install workingDirectory: $(Agent.TempDirectory)/jellyfin-web - task: CopyFiles@2 @@ -69,33 +68,33 @@ jobs: command: publish publishWebProjects: false projects: "${{ parameters.RestoreBuildProjects }}" - arguments: "--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)" + arguments: "--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)" zipAfterPublish: false - task: PublishPipelineArtifact@0 displayName: "Publish Artifact Naming" condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release')) inputs: - targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/Emby.Naming.dll" + targetPath: "$(build.ArtifactStagingDirectory)/Jellyfin.Server/Emby.Naming.dll" artifactName: "Jellyfin.Naming" - task: PublishPipelineArtifact@0 displayName: "Publish Artifact Controller" condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release')) inputs: - targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Controller.dll" + targetPath: "$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Controller.dll" artifactName: "Jellyfin.Controller" - task: PublishPipelineArtifact@0 displayName: "Publish Artifact Model" condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release')) inputs: - targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Model.dll" + targetPath: "$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Model.dll" artifactName: "Jellyfin.Model" - task: PublishPipelineArtifact@0 displayName: "Publish Artifact Common" condition: and(succeeded(), eq(variables['BuildConfiguration'], 'Release')) inputs: - targetPath: "$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll" + targetPath: "$(build.ArtifactStagingDirectory)/Jellyfin.Server/MediaBrowser.Common.dll" artifactName: "Jellyfin.Common" diff --git a/.ci/azure-pipelines-windows.yml b/.ci/azure-pipelines-windows.yml deleted file mode 100644 index 11856f9c8..000000000 --- a/.ci/azure-pipelines-windows.yml +++ /dev/null @@ -1,82 +0,0 @@ -parameters: - WindowsImage: "windows-latest" - TestProjects: "tests/**/*Tests.csproj" - DotNetSdkVersion: 3.1.100 - -jobs: - - job: PublishWindows - displayName: Publish Windows - pool: - vmImage: ${{ parameters.WindowsImage }} - steps: - - checkout: self - clean: true - submodules: true - persistCredentials: true - - - task: CmdLine@2 - displayName: "Clone Web Client (Master, Release, or Tag)" - condition: and(succeeded(), or(contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master'), contains(variables['Build.SourceBranch'], 'tag')), in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI', 'BuildCompletion')) - inputs: - script: "git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web" - - - task: CmdLine@2 - displayName: "Clone Web Client (PR)" - condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest')) - inputs: - script: "git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web" - - - task: NodeTool@0 - displayName: "Install Node" - condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion')) - inputs: - versionSpec: "10.x" - - - task: CmdLine@2 - displayName: "Build Web Client" - condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion')) - inputs: - script: yarn install && yarn build - workingDirectory: $(Agent.TempDirectory)/jellyfin-web - - - task: CopyFiles@2 - displayName: "Copy Web Client" - condition: and(succeeded(), or(contains(variables['System.PullRequest.TargetBranch'], 'release'), contains(variables['System.PullRequest.TargetBranch'], 'master'), contains(variables['Build.SourceBranch'], 'release'), contains(variables['Build.SourceBranch'], 'master')), in(variables['Build.Reason'], 'PullRequest', 'IndividualCI', 'BatchedCI', 'BuildCompletion')) - inputs: - sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist - contents: "**" - targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web - cleanTargetFolder: true - overWrite: true - flattenFolders: false - - - task: CmdLine@2 - displayName: "Clone UX Repository" - inputs: - script: git clone --depth=1 https://github.com/jellyfin/jellyfin-ux $(Agent.TempDirectory)\jellyfin-ux - - - task: PowerShell@2 - displayName: "Build NSIS Installer" - inputs: - targetType: "filePath" - filePath: ./deployment/windows/build-jellyfin.ps1 - arguments: -InstallFFMPEG -InstallNSSM -MakeNSIS -InstallTrayApp -UXLocation $(Agent.TempDirectory)\jellyfin-ux -InstallLocation $(build.artifactstagingdirectory) - errorActionPreference: "stop" - workingDirectory: $(Build.SourcesDirectory) - - - task: CopyFiles@2 - displayName: "Copy NSIS Installer" - inputs: - sourceFolder: $(Build.SourcesDirectory)/deployment/windows/ - contents: "jellyfin*.exe" - targetFolder: $(System.ArtifactsDirectory)/setup - cleanTargetFolder: true - overWrite: true - flattenFolders: true - - - task: PublishPipelineArtifact@0 - displayName: "Publish Artifact Setup" - condition: succeeded() - inputs: - targetPath: "$(build.artifactstagingdirectory)/setup" - artifactName: "Jellyfin Server Setup" diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml index f79a85b21..3522cbf00 100644 --- a/.ci/azure-pipelines.yml +++ b/.ci/azure-pipelines.yml @@ -27,11 +27,6 @@ jobs: Windows: "windows-latest" macOS: "macos-latest" - - template: azure-pipelines-windows.yml - parameters: - WindowsImage: "windows-latest" - TestProjects: $(TestProjects) - - template: azure-pipelines-compat.yml parameters: Packages: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..d886c6487 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,13 @@ +--- +name: Feature Request +about: Request a new feature +title: '' +labels: feature-request +assignees: '' +--- + +**PLEASE DO NOT OPEN FEATURE REQUEST ISSUES ON GITHUB** + +**Feature requests should be opened on our dedicated [feature request](https://features.jellyfin.org/) hub so they can be appropriately discussed and prioritized.** + +However, if you are willing to contribute to the project by adding a new feature yourself, then please ensure that you first review our [documentation](https://docs.jellyfin.org/general/contributing/development.html) on contributing code. Once you have reviewed the documentation, feel free to come back here and open an issue here outlining your proposed approach so that it can be documented, tracked, and discussed by other team members. diff --git a/.gitignore b/.gitignore index 42243f01a..523c45a7e 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ ProgramData*/ CorePlugins*/ ProgramData-Server*/ ProgramData-UI*/ +MediaBrowser.WebDashboard/jellyfin-web/** ################# ## Visual Studio diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index f195c125f..ce956176e 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -22,6 +22,7 @@ - [cvium](https://github.com/cvium) - [dannymichel](https://github.com/dannymichel) - [DaveChild](https://github.com/DaveChild) + - [Delgan](https://github.com/Delgan) - [dcrdev](https://github.com/dcrdev) - [dhartung](https://github.com/dhartung) - [dinki](https://github.com/dinki) @@ -128,6 +129,7 @@ - [xosdy](https://github.com/xosdy) - [XVicarious](https://github.com/XVicarious) - [YouKnowBlom](https://github.com/YouKnowBlom) + - [KristupasSavickas](https://github.com/KristupasSavickas) # Emby Contributors diff --git a/Dockerfile b/Dockerfile index 73ab3d790..6e834d4e0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,11 @@ ARG DOTNET_VERSION=3.1 -ARG FFMPEG_VERSION=latest FROM node:alpine as web-builder ARG JELLYFIN_WEB_VERSION=master -RUN apk add curl git \ +RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && cd jellyfin-web-* \ && yarn install \ - && yarn build \ && mv dist /dist FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION}-buster as builder @@ -18,7 +16,6 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 # see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting RUN dotnet publish Jellyfin.Server --disable-parallel --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 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable @@ -28,37 +25,36 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn # https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support) ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility" -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 +# mesa-va-drivers: needed for AMD VAAPI RUN apt-get update \ + && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https \ + && wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \ + && echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \ + && apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y \ - libfontconfig1 \ - libgomp1 \ - libva-drm2 \ mesa-va-drivers \ + jellyfin-ffmpeg \ openssl \ - ca-certificates \ - vainfo \ - i965-va-driver \ - && apt-get clean autoclean -y\ - && apt-get autoremove -y\ + locales \ + && apt-get remove gnupg wget apt-transport-https -y \ + && apt-get clean autoclean -y \ + && apt-get autoremove -y \ && rm -rf /var/lib/apt/lists/* \ && mkdir -p /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 + && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 +ENV LC_ALL en_US.UTF-8 +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US:en EXPOSE 8096 VOLUME /cache /config /media ENTRYPOINT ["./jellyfin/jellyfin", \ "--datadir", "/config", \ "--cachedir", "/cache", \ - "--ffmpeg", "/usr/local/bin/ffmpeg"] + "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"] diff --git a/Dockerfile.arm b/Dockerfile.arm index 07780e27b..39beaa479 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -7,11 +7,10 @@ ARG DOTNET_VERSION=3.1 FROM node:alpine as web-builder ARG JELLYFIN_WEB_VERSION=master -RUN apk add curl git \ +RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && cd jellyfin-web-* \ && yarn install \ - && yarn build \ && mv dist /dist @@ -53,20 +52,26 @@ RUN apt-get update \ libraspberrypi0 \ vainfo \ libva2 \ + locales \ && apt-get remove curl gnupg -y \ && apt-get clean autoclean -y \ && apt-get autoremove -y \ && rm -rf /var/lib/apt/lists/* \ && mkdir -p /cache /config /media \ - && chmod 777 /cache /config /media + && chmod 777 /cache /config /media \ + && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen + COPY --from=builder /jellyfin /jellyfin COPY --from=web-builder /dist /jellyfin/jellyfin-web ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 +ENV LC_ALL en_US.UTF-8 +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US:en EXPOSE 8096 VOLUME /cache /config /media ENTRYPOINT ["./jellyfin/jellyfin", \ "--datadir", "/config", \ "--cachedir", "/cache", \ - "--ffmpeg", "/usr/lib/jellyfin-ffmpeg"] + "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"] diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 9dc6fa7ed..1a691b572 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -7,11 +7,10 @@ ARG DOTNET_VERSION=3.1 FROM node:alpine as web-builder ARG JELLYFIN_WEB_VERSION=master -RUN apk add curl git \ +RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ && cd jellyfin-web-* \ && yarn install \ - && yarn build \ && mv dist /dist @@ -35,7 +34,7 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility" COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin -RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -y \ +RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -y \ ffmpeg \ libssl-dev \ ca-certificates \ @@ -43,15 +42,21 @@ RUN apt-get update && apt-get install --no-install-recommends --no-install-sugge libfreetype6 \ libomxil-bellagio0 \ libomxil-bellagio-bin \ + locales \ && apt-get clean autoclean -y \ && apt-get autoremove -y \ && rm -rf /var/lib/apt/lists/* \ && mkdir -p /cache /config /media \ - && chmod 777 /cache /config /media + && chmod 777 /cache /config /media \ + && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen + COPY --from=builder /jellyfin /jellyfin COPY --from=web-builder /dist /jellyfin/jellyfin-web ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 +ENV LC_ALL en_US.UTF-8 +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US:en EXPOSE 8096 VOLUME /cache /config /media diff --git a/DvdLib/BigEndianBinaryReader.cs b/DvdLib/BigEndianBinaryReader.cs index b3b2eabd5..473005b55 100644 --- a/DvdLib/BigEndianBinaryReader.cs +++ b/DvdLib/BigEndianBinaryReader.cs @@ -1,4 +1,4 @@ -using System; +using System.Buffers.Binary; using System.IO; namespace DvdLib @@ -12,19 +12,12 @@ namespace DvdLib public override ushort ReadUInt16() { - return BitConverter.ToUInt16(ReadAndReverseBytes(2), 0); + return BinaryPrimitives.ReadUInt16BigEndian(base.ReadBytes(2)); } public override uint ReadUInt32() { - return BitConverter.ToUInt32(ReadAndReverseBytes(4), 0); - } - - private byte[] ReadAndReverseBytes(int count) - { - byte[] val = base.ReadBytes(count); - Array.Reverse(val, 0, count); - return val; + return BinaryPrimitives.ReadUInt32BigEndian(base.ReadBytes(4)); } } } diff --git a/DvdLib/DvdLib.csproj b/DvdLib/DvdLib.csproj index f4df6a9f5..36f949616 100644 --- a/DvdLib/DvdLib.csproj +++ b/DvdLib/DvdLib.csproj @@ -4,10 +4,6 @@ <Compile Include="..\SharedVersion.cs" /> </ItemGroup> - <ItemGroup> - <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> - </ItemGroup> - <PropertyGroup> <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> diff --git a/DvdLib/Ifo/Dvd.cs b/DvdLib/Ifo/Dvd.cs index 157b2e197..5af58a2dc 100644 --- a/DvdLib/Ifo/Dvd.cs +++ b/DvdLib/Ifo/Dvd.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using MediaBrowser.Model.IO; namespace DvdLib.Ifo { @@ -13,13 +12,10 @@ namespace DvdLib.Ifo private ushort _titleCount; public readonly Dictionary<ushort, string> VTSPaths = new Dictionary<ushort, string>(); - private readonly IFileSystem _fileSystem; - - public Dvd(string path, IFileSystem fileSystem) + public Dvd(string path) { - _fileSystem = fileSystem; Titles = new List<Title>(); - var allFiles = _fileSystem.GetFiles(path, true).ToList(); + var allFiles = new DirectoryInfo(path).GetFiles(path, SearchOption.AllDirectories); var vmgPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.IFO", StringComparison.OrdinalIgnoreCase)) ?? allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.BUP", StringComparison.OrdinalIgnoreCase)); @@ -33,7 +29,7 @@ namespace DvdLib.Ifo continue; } - var nums = ifo.Name.Split(new [] { '_' }, StringSplitOptions.RemoveEmptyEntries); + var nums = ifo.Name.Split(new[] { '_' }, StringSplitOptions.RemoveEmptyEntries); if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber)) { ReadVTS(ifoNumber, ifo.FullName); @@ -76,7 +72,7 @@ namespace DvdLib.Ifo } } - private void ReadVTS(ushort vtsNum, IEnumerable<FileSystemMetadata> allFiles) + private void ReadVTS(ushort vtsNum, IReadOnlyList<FileInfo> allFiles) { var filename = string.Format("VTS_{0:00}_0.IFO", vtsNum); diff --git a/Emby.Dlna/Api/DlnaServerService.cs b/Emby.Dlna/Api/DlnaServerService.cs index b7d018921..7fba2184a 100644 --- a/Emby.Dlna/Api/DlnaServerService.cs +++ b/Emby.Dlna/Api/DlnaServerService.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text; using System.Threading.Tasks; @@ -151,6 +152,7 @@ namespace Emby.Dlna.Api return _resultFactory.GetStaticResult(Request, cacheKey, null, cacheLength, XMLContentType, () => Task.FromResult<Stream>(new MemoryStream(bytes))); } + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] public object Get(GetContentDirectory request) { var xml = ContentDirectory.GetServiceXml(); @@ -158,6 +160,7 @@ namespace Emby.Dlna.Api return _resultFactory.GetResult(Request, xml, XMLContentType); } + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] public object Get(GetMediaReceiverRegistrar request) { var xml = MediaReceiverRegistrar.GetServiceXml(); @@ -165,6 +168,7 @@ namespace Emby.Dlna.Api return _resultFactory.GetResult(Request, xml, XMLContentType); } + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] public object Get(GetConnnectionManager request) { var xml = ConnectionManager.GetServiceXml(); @@ -313,31 +317,37 @@ namespace Emby.Dlna.Api return _resultFactory.GetStaticResult(Request, cacheKey, null, cacheLength, contentType, () => Task.FromResult(_dlnaManager.GetIcon(request.Filename).Stream)); } + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] public object Subscribe(ProcessContentDirectoryEventRequest request) { return ProcessEventRequest(ContentDirectory); } + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] public object Subscribe(ProcessConnectionManagerEventRequest request) { return ProcessEventRequest(ConnectionManager); } + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] public object Subscribe(ProcessMediaReceiverRegistrarEventRequest request) { return ProcessEventRequest(MediaReceiverRegistrar); } + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] public object Unsubscribe(ProcessContentDirectoryEventRequest request) { return ProcessEventRequest(ContentDirectory); } + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] public object Unsubscribe(ProcessConnectionManagerEventRequest request) { return ProcessEventRequest(ConnectionManager); } + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] public object Unsubscribe(ProcessMediaReceiverRegistrarEventRequest request) { return ProcessEventRequest(MediaReceiverRegistrar); diff --git a/Emby.Dlna/Api/DlnaService.cs b/Emby.Dlna/Api/DlnaService.cs index 7d6b8f78e..5f984bb33 100644 --- a/Emby.Dlna/Api/DlnaService.cs +++ b/Emby.Dlna/Api/DlnaService.cs @@ -1,5 +1,6 @@ #pragma warning disable CS1591 +using System.Diagnostics.CodeAnalysis; using System.Linq; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Net; @@ -52,6 +53,7 @@ namespace Emby.Dlna.Api _dlnaManager = dlnaManager; } + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] public object Get(GetProfileInfos request) { return _dlnaManager.GetProfileInfos().ToArray(); @@ -62,6 +64,7 @@ namespace Emby.Dlna.Api return _dlnaManager.GetProfile(request.Id); } + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] public object Get(GetDefaultProfile request) { return _dlnaManager.GetDefaultProfile(); diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index 41f4fbbd3..28888f031 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -78,7 +78,18 @@ namespace Emby.Dlna.ContentDirectory _profile = profile; _config = config; - _didlBuilder = new DidlBuilder(profile, user, imageProcessor, serverAddress, accessToken, userDataManager, localization, mediaSourceManager, Logger, mediaEncoder); + _didlBuilder = new DidlBuilder( + profile, + user, + imageProcessor, + serverAddress, + accessToken, + userDataManager, + localization, + mediaSourceManager, + Logger, + mediaEncoder, + libraryManager); } /// <inheritdoc /> @@ -153,7 +164,7 @@ namespace Emby.Dlna.ContentDirectory { var id = sparams["ObjectID"]; - var serverItem = GetItemFromObjectId(id, _user); + var serverItem = GetItemFromObjectId(id); var item = serverItem.Item; @@ -276,7 +287,7 @@ namespace Emby.Dlna.ContentDirectory DidlBuilder.WriteXmlRootAttributes(_profile, writer); - var serverItem = GetItemFromObjectId(id, _user); + var serverItem = GetItemFromObjectId(id); var item = serverItem.Item; @@ -293,7 +304,7 @@ namespace Emby.Dlna.ContentDirectory else { var dlnaOptions = _config.GetDlnaConfiguration(); - _didlBuilder.WriteItemElement(dlnaOptions, writer, item, _user, null, null, deviceId, filter); + _didlBuilder.WriteItemElement(writer, item, _user, null, null, deviceId, filter); } provided++; @@ -320,7 +331,7 @@ namespace Emby.Dlna.ContentDirectory } else { - _didlBuilder.WriteItemElement(dlnaOptions, writer, childItem, _user, item, serverItem.StubType, deviceId, filter); + _didlBuilder.WriteItemElement(writer, childItem, _user, item, serverItem.StubType, deviceId, filter); } } } @@ -387,7 +398,7 @@ namespace Emby.Dlna.ContentDirectory DidlBuilder.WriteXmlRootAttributes(_profile, writer); - var serverItem = GetItemFromObjectId(sparams["ContainerID"], _user); + var serverItem = GetItemFromObjectId(sparams["ContainerID"]); var item = serverItem.Item; @@ -406,7 +417,7 @@ namespace Emby.Dlna.ContentDirectory } else { - _didlBuilder.WriteItemElement(dlnaOptions, writer, i, _user, item, serverItem.StubType, deviceId, filter); + _didlBuilder.WriteItemElement(writer, i, _user, item, serverItem.StubType, deviceId, filter); } } @@ -512,11 +523,11 @@ namespace Emby.Dlna.ContentDirectory } else if (string.Equals(CollectionType.Folders, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase)) { - return GetFolders(item, user, stubType, sort, startIndex, limit); + return GetFolders(user, startIndex, limit); } else if (string.Equals(CollectionType.LiveTv, collectionFolder.CollectionType, StringComparison.OrdinalIgnoreCase)) { - return GetLiveTvChannels(item, user, stubType, sort, startIndex, limit); + return GetLiveTvChannels(user, sort, startIndex, limit); } } @@ -547,7 +558,7 @@ namespace Emby.Dlna.ContentDirectory return ToResult(queryResult); } - private QueryResult<ServerItem> GetLiveTvChannels(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit) + private QueryResult<ServerItem> GetLiveTvChannels(User user, SortCriteria sort, int? startIndex, int? limit) { var query = new InternalItemsQuery(user) { @@ -579,7 +590,7 @@ namespace Emby.Dlna.ContentDirectory if (stubType.HasValue && stubType.Value == StubType.Playlists) { - return GetMusicPlaylists(item, user, query); + return GetMusicPlaylists(user, query); } if (stubType.HasValue && stubType.Value == StubType.Albums) @@ -707,7 +718,7 @@ namespace Emby.Dlna.ContentDirectory if (stubType.HasValue && stubType.Value == StubType.Collections) { - return GetMovieCollections(item, user, query); + return GetMovieCollections(user, query); } if (stubType.HasValue && stubType.Value == StubType.Favorites) @@ -720,46 +731,42 @@ namespace Emby.Dlna.ContentDirectory return GetGenres(item, user, query); } - var list = new List<ServerItem>(); - - list.Add(new ServerItem(item) - { - StubType = StubType.ContinueWatching - }); - - list.Add(new ServerItem(item) + var array = new ServerItem[] { - StubType = StubType.Latest - }); - - list.Add(new ServerItem(item) - { - StubType = StubType.Movies - }); - - list.Add(new ServerItem(item) - { - StubType = StubType.Collections - }); - - list.Add(new ServerItem(item) - { - StubType = StubType.Favorites - }); - - list.Add(new ServerItem(item) - { - StubType = StubType.Genres - }); + new ServerItem(item) + { + StubType = StubType.ContinueWatching + }, + new ServerItem(item) + { + StubType = StubType.Latest + }, + new ServerItem(item) + { + StubType = StubType.Movies + }, + new ServerItem(item) + { + StubType = StubType.Collections + }, + new ServerItem(item) + { + StubType = StubType.Favorites + }, + new ServerItem(item) + { + StubType = StubType.Genres + } + }; return new QueryResult<ServerItem> { - Items = list, - TotalRecordCount = list.Count + Items = array, + TotalRecordCount = array.Length }; } - private QueryResult<ServerItem> GetFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit) + private QueryResult<ServerItem> GetFolders(User user, int? startIndex, int? limit) { var folders = _libraryManager.GetUserRootFolder().GetChildren(user, true) .OrderBy(i => i.SortName) @@ -792,7 +799,7 @@ namespace Emby.Dlna.ContentDirectory if (stubType.HasValue && stubType.Value == StubType.NextUp) { - return GetNextUp(item, user, query); + return GetNextUp(item, query); } if (stubType.HasValue && stubType.Value == StubType.Latest) @@ -910,7 +917,7 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } - private QueryResult<ServerItem> GetMovieCollections(BaseItem parent, User user, InternalItemsQuery query) + private QueryResult<ServerItem> GetMovieCollections(User user, InternalItemsQuery query) { query.Recursive = true; //query.Parent = parent; @@ -1105,7 +1112,7 @@ namespace Emby.Dlna.ContentDirectory return ToResult(result); } - private QueryResult<ServerItem> GetMusicPlaylists(BaseItem parent, User user, InternalItemsQuery query) + private QueryResult<ServerItem> GetMusicPlaylists(User user, InternalItemsQuery query) { query.Parent = null; query.IncludeItemTypes = new[] { typeof(Playlist).Name }; @@ -1134,7 +1141,7 @@ namespace Emby.Dlna.ContentDirectory return ToResult(items); } - private QueryResult<ServerItem> GetNextUp(BaseItem parent, User user, InternalItemsQuery query) + private QueryResult<ServerItem> GetNextUp(BaseItem parent, InternalItemsQuery query) { query.OrderBy = Array.Empty<(string, SortOrder)>(); @@ -1289,15 +1296,15 @@ namespace Emby.Dlna.ContentDirectory return result; } - private ServerItem GetItemFromObjectId(string id, User user) + private ServerItem GetItemFromObjectId(string id) { return DidlBuilder.IsIdRoot(id) ? new ServerItem(_libraryManager.GetUserRootFolder()) - : ParseItemId(id, user); + : ParseItemId(id); } - private ServerItem ParseItemId(string id, User user) + private ServerItem ParseItemId(string id) { StubType? stubType = null; diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index 45335f90d..59951f6d9 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -45,6 +45,7 @@ namespace Emby.Dlna.Didl private readonly IMediaSourceManager _mediaSourceManager; private readonly ILogger _logger; private readonly IMediaEncoder _mediaEncoder; + private readonly ILibraryManager _libraryManager; public DidlBuilder( DeviceProfile profile, @@ -56,7 +57,8 @@ namespace Emby.Dlna.Didl ILocalizationManager localization, IMediaSourceManager mediaSourceManager, ILogger logger, - IMediaEncoder mediaEncoder) + IMediaEncoder mediaEncoder, + ILibraryManager libraryManager) { _profile = profile; _user = user; @@ -68,6 +70,7 @@ namespace Emby.Dlna.Didl _mediaSourceManager = mediaSourceManager; _logger = logger; _mediaEncoder = mediaEncoder; + _libraryManager = libraryManager; } public static string NormalizeDlnaMediaUrl(string url) @@ -75,7 +78,7 @@ namespace Emby.Dlna.Didl return url + "&dlnaheaders=true"; } - public string GetItemDidl(DlnaOptions options, BaseItem item, User user, BaseItem context, string deviceId, Filter filter, StreamInfo streamInfo) + public string GetItemDidl(BaseItem item, User user, BaseItem context, string deviceId, Filter filter, StreamInfo streamInfo) { var settings = new XmlWriterSettings { @@ -100,7 +103,7 @@ namespace Emby.Dlna.Didl WriteXmlRootAttributes(_profile, writer); - WriteItemElement(options, writer, item, user, context, null, deviceId, filter, streamInfo); + WriteItemElement(writer, item, user, context, null, deviceId, filter, streamInfo); writer.WriteFullEndElement(); //writer.WriteEndDocument(); @@ -127,7 +130,6 @@ namespace Emby.Dlna.Didl } public void WriteItemElement( - DlnaOptions options, XmlWriter writer, BaseItem item, User user, @@ -164,25 +166,23 @@ namespace Emby.Dlna.Didl // refID? // storeAttribute(itemNode, object, ClassProperties.REF_ID, false); - var hasMediaSources = item as IHasMediaSources; - - if (hasMediaSources != null) + if (item is IHasMediaSources) { if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) { - AddAudioResource(options, writer, item, deviceId, filter, streamInfo); + AddAudioResource(writer, item, deviceId, filter, streamInfo); } else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) { - AddVideoResource(options, writer, item, deviceId, filter, streamInfo); + AddVideoResource(writer, item, deviceId, filter, streamInfo); } } - AddCover(item, context, null, writer); + AddCover(item, null, writer); writer.WriteFullEndElement(); } - private void AddVideoResource(DlnaOptions options, XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo streamInfo = null) + private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo streamInfo = null) { if (streamInfo == null) { @@ -226,7 +226,7 @@ namespace Emby.Dlna.Didl foreach (var contentFeature in contentFeatureList) { - AddVideoResource(writer, video, deviceId, filter, contentFeature, streamInfo); + AddVideoResource(writer, filter, contentFeature, streamInfo); } var subtitleProfiles = streamInfo.GetSubtitleProfiles(_mediaEncoder, false, _serverAddress, _accessToken); @@ -283,7 +283,10 @@ namespace Emby.Dlna.Didl else { writer.WriteStartElement(string.Empty, "res", NS_DIDL); - var protocolInfo = string.Format("http-get:*:text/{0}:*", info.Format.ToLowerInvariant()); + var protocolInfo = string.Format( + CultureInfo.InvariantCulture, + "http-get:*:text/{0}:*", + info.Format.ToLowerInvariant()); writer.WriteAttributeString("protocolInfo", protocolInfo); writer.WriteString(info.Url); @@ -293,7 +296,7 @@ namespace Emby.Dlna.Didl return true; } - private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, string contentFeatures, StreamInfo streamInfo) + private void AddVideoResource(XmlWriter writer, Filter filter, string contentFeatures, StreamInfo streamInfo) { writer.WriteStartElement(string.Empty, "res", NS_DIDL); @@ -335,7 +338,13 @@ namespace Emby.Dlna.Didl { if (targetWidth.HasValue && targetHeight.HasValue) { - writer.WriteAttributeString("resolution", string.Format("{0}x{1}", targetWidth.Value, targetHeight.Value)); + writer.WriteAttributeString( + "resolution", + string.Format( + CultureInfo.InvariantCulture, + "{0}x{1}", + targetWidth.Value, + targetHeight.Value)); } } @@ -369,17 +378,19 @@ namespace Emby.Dlna.Didl streamInfo.TargetVideoCodecTag, streamInfo.IsTargetAVC); - var filename = url.Substring(0, url.IndexOf('?')); + var filename = url.Substring(0, url.IndexOf('?', StringComparison.Ordinal)); var mimeType = mediaProfile == null || string.IsNullOrEmpty(mediaProfile.MimeType) ? MimeTypes.GetMimeType(filename) : mediaProfile.MimeType; - writer.WriteAttributeString("protocolInfo", string.Format( - "http-get:*:{0}:{1}", - mimeType, - contentFeatures - )); + writer.WriteAttributeString( + "protocolInfo", + string.Format( + CultureInfo.InvariantCulture, + "http-get:*:{0}:{1}", + mimeType, + contentFeatures)); writer.WriteString(url); @@ -392,24 +403,24 @@ namespace Emby.Dlna.Didl { switch (itemStubType.Value) { - case StubType.Latest: return _localization.GetLocalizedString("Latest"); - case StubType.Playlists: return _localization.GetLocalizedString("Playlists"); - case StubType.AlbumArtists: return _localization.GetLocalizedString("HeaderAlbumArtists"); - case StubType.Albums: return _localization.GetLocalizedString("Albums"); - case StubType.Artists: return _localization.GetLocalizedString("Artists"); - case StubType.Songs: return _localization.GetLocalizedString("Songs"); - case StubType.Genres: return _localization.GetLocalizedString("Genres"); - case StubType.FavoriteAlbums: return _localization.GetLocalizedString("HeaderFavoriteAlbums"); - case StubType.FavoriteArtists: return _localization.GetLocalizedString("HeaderFavoriteArtists"); - case StubType.FavoriteSongs: return _localization.GetLocalizedString("HeaderFavoriteSongs"); + case StubType.Latest: return _localization.GetLocalizedString("Latest"); + case StubType.Playlists: return _localization.GetLocalizedString("Playlists"); + case StubType.AlbumArtists: return _localization.GetLocalizedString("HeaderAlbumArtists"); + case StubType.Albums: return _localization.GetLocalizedString("Albums"); + case StubType.Artists: return _localization.GetLocalizedString("Artists"); + case StubType.Songs: return _localization.GetLocalizedString("Songs"); + case StubType.Genres: return _localization.GetLocalizedString("Genres"); + case StubType.FavoriteAlbums: return _localization.GetLocalizedString("HeaderFavoriteAlbums"); + case StubType.FavoriteArtists: return _localization.GetLocalizedString("HeaderFavoriteArtists"); + case StubType.FavoriteSongs: return _localization.GetLocalizedString("HeaderFavoriteSongs"); case StubType.ContinueWatching: return _localization.GetLocalizedString("HeaderContinueWatching"); - case StubType.Movies: return _localization.GetLocalizedString("Movies"); - case StubType.Collections: return _localization.GetLocalizedString("Collections"); - case StubType.Favorites: return _localization.GetLocalizedString("Favorites"); - case StubType.NextUp: return _localization.GetLocalizedString("HeaderNextUp"); - case StubType.FavoriteSeries: return _localization.GetLocalizedString("HeaderFavoriteShows"); + case StubType.Movies: return _localization.GetLocalizedString("Movies"); + case StubType.Collections: return _localization.GetLocalizedString("Collections"); + case StubType.Favorites: return _localization.GetLocalizedString("Favorites"); + case StubType.NextUp: return _localization.GetLocalizedString("HeaderNextUp"); + case StubType.FavoriteSeries: return _localization.GetLocalizedString("HeaderFavoriteShows"); case StubType.FavoriteEpisodes: return _localization.GetLocalizedString("HeaderFavoriteEpisodes"); - case StubType.Series: return _localization.GetLocalizedString("Shows"); + case StubType.Series: return _localization.GetLocalizedString("Shows"); default: break; } } @@ -420,7 +431,10 @@ namespace Emby.Dlna.Didl if (item.ParentIndexNumber.HasValue && item.ParentIndexNumber.Value == 0 && season.IndexNumber.HasValue && season.IndexNumber.Value != 0) { - return string.Format(_localization.GetLocalizedString("ValueSpecialEpisodeName"), item.Name); + return string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("ValueSpecialEpisodeName"), + item.Name); } if (item.IndexNumber.HasValue) @@ -435,11 +449,34 @@ namespace Emby.Dlna.Didl return number + " - " + item.Name; } } + else if (item is Episode ep) + { + var parent = ep.GetParent(); + var name = parent.Name + " - "; + + if (ep.ParentIndexNumber.HasValue) + { + name += "S" + ep.ParentIndexNumber.Value.ToString("00", CultureInfo.InvariantCulture); + } + else if (!item.IndexNumber.HasValue) + { + return name + " - " + item.Name; + } + + name += "E" + ep.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture); + if (ep.IndexNumberEnd.HasValue) + { + name += "-" + ep.IndexNumberEnd.Value.ToString("00", CultureInfo.InvariantCulture); + } + + name += " - " + item.Name; + return name; + } return item.Name; } - private void AddAudioResource(DlnaOptions options, XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null) + private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null) { writer.WriteStartElement(string.Empty, "res", NS_DIDL); @@ -505,7 +542,7 @@ namespace Emby.Dlna.Didl targetSampleRate, targetAudioBitDepth); - var filename = url.Substring(0, url.IndexOf('?')); + var filename = url.Substring(0, url.IndexOf('?', StringComparison.Ordinal)); var mimeType = mediaProfile == null || string.IsNullOrEmpty(mediaProfile.MimeType) ? MimeTypes.GetMimeType(filename) @@ -521,11 +558,13 @@ namespace Emby.Dlna.Didl streamInfo.RunTimeTicks ?? 0, streamInfo.TranscodeSeekInfo); - writer.WriteAttributeString("protocolInfo", string.Format( - "http-get:*:{0}:{1}", - mimeType, - contentFeatures - )); + writer.WriteAttributeString( + "protocolInfo", + string.Format( + CultureInfo.InvariantCulture, + "http-get:*:{0}:{1}", + mimeType, + contentFeatures)); writer.WriteString(url); @@ -548,7 +587,7 @@ namespace Emby.Dlna.Didl var clientId = GetClientId(folder, stubType); - if (string.Equals(requestedId, "0")) + if (string.Equals(requestedId, "0", StringComparison.Ordinal)) { writer.WriteAttributeString("id", "0"); writer.WriteAttributeString("parentID", "-1"); @@ -577,7 +616,7 @@ namespace Emby.Dlna.Didl AddGeneralProperties(folder, stubType, context, writer, filter); - AddCover(folder, context, stubType, writer); + AddCover(folder, stubType, writer); writer.WriteFullEndElement(); } @@ -610,7 +649,10 @@ namespace Emby.Dlna.Didl if (playbackPositionTicks > 0) { - var elementValue = string.Format("BM={0}", Convert.ToInt32(TimeSpan.FromTicks(playbackPositionTicks).TotalSeconds).ToString(_usCulture)); + var elementValue = string.Format( + CultureInfo.InvariantCulture, + "BM={0}", + Convert.ToInt32(TimeSpan.FromTicks(playbackPositionTicks).TotalSeconds)); AddValue(writer, "sec", "dcmInfo", elementValue, secAttribute.Value); } } @@ -763,37 +805,36 @@ namespace Emby.Dlna.Didl private void AddPeople(BaseItem item, XmlWriter writer) { - //var types = new[] - //{ - // PersonType.Director, - // PersonType.Writer, - // PersonType.Producer, - // PersonType.Composer, - // "Creator" - //}; - - //var people = _libraryManager.GetPeople(item); - - //var index = 0; - - //// Seeing some LG models locking up due content with large lists of people - //// The actual issue might just be due to processing a more metadata than it can handle - //var limit = 6; + if (!item.SupportsPeople) + { + return; + } - //foreach (var actor in people) - //{ - // var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase)) - // ?? PersonType.Actor; + var types = new[] + { + PersonType.Director, + PersonType.Writer, + PersonType.Producer, + PersonType.Composer, + "creator" + }; - // AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NS_UPNP); + // Seeing some LG models locking up due content with large lists of people + // The actual issue might just be due to processing a more metadata than it can handle + var people = _libraryManager.GetPeople( + new InternalPeopleQuery + { + ItemId = item.Id, + Limit = 6 + }); - // index++; + foreach (var actor in people) + { + var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase)) + ?? PersonType.Actor; - // if (index >= limit) - // { - // break; - // } - //} + AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NS_UPNP); + } } private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter) @@ -870,7 +911,7 @@ namespace Emby.Dlna.Didl } } - private void AddCover(BaseItem item, BaseItem context, StubType? stubType, XmlWriter writer) + private void AddCover(BaseItem item, StubType? stubType, XmlWriter writer) { ImageDownloadInfo imageInfo = GetImageInfo(item); @@ -915,17 +956,8 @@ namespace Emby.Dlna.Didl } - private void AddEmbeddedImageAsCover(string name, XmlWriter writer) - { - writer.WriteStartElement("upnp", "albumArtURI", NS_UPNP); - writer.WriteAttributeString("dlna", "profileID", NS_DLNA, _profile.AlbumArtPn); - writer.WriteString(_serverAddress + "/Dlna/icons/people480.jpg"); - writer.WriteFullEndElement(); - - writer.WriteElementString("upnp", "icon", NS_UPNP, _serverAddress + "/Dlna/icons/people48.jpg"); - } - - private void AddImageResElement(BaseItem item, + private void AddImageResElement( + BaseItem item, XmlWriter writer, int maxWidth, int maxHeight, @@ -951,13 +983,17 @@ namespace Emby.Dlna.Didl var contentFeatures = new ContentFeatureBuilder(_profile) .BuildImageHeader(format, width, height, imageInfo.IsDirectStream, org_Pn); - writer.WriteAttributeString("protocolInfo", string.Format( - "http-get:*:{0}:{1}", - MimeTypes.GetMimeType("file." + format), - contentFeatures - )); + writer.WriteAttributeString( + "protocolInfo", + string.Format( + CultureInfo.InvariantCulture, + "http-get:*:{0}:{1}", + MimeTypes.GetMimeType("file." + format), + contentFeatures)); - writer.WriteAttributeString("resolution", string.Format("{0}x{1}", width, height)); + writer.WriteAttributeString( + "resolution", + string.Format(CultureInfo.InvariantCulture, "{0}x{1}", width, height)); writer.WriteString(albumartUrlInfo.Url); @@ -982,19 +1018,58 @@ namespace Emby.Dlna.Didl } } - item = item.GetParents().FirstOrDefault(i => i.HasImage(ImageType.Primary)); + // For audio tracks without art use album art if available. + if (item is Audio audioItem) + { + var album = audioItem.AlbumEntity; + return album != null && album.HasImage(ImageType.Primary) + ? GetImageInfo(album, ImageType.Primary) + : null; + } - if (item != null) + // Don't look beyond album/playlist level. Metadata service may assign an image from a different album/show to the parent folder. + if (item is MusicAlbum || item is Playlist) { - if (item.HasImage(ImageType.Primary)) - { - return GetImageInfo(item, ImageType.Primary); - } + return null; + } + + // For other item types check parents, but be aware that image retrieved from a parent may be not suitable for this media item. + var parentWithImage = GetFirstParentWithImageBelowUserRoot(item); + if (parentWithImage != null) + { + return GetImageInfo(parentWithImage, ImageType.Primary); } return null; } + private BaseItem GetFirstParentWithImageBelowUserRoot(BaseItem item) + { + if (item == null) + { + return null; + } + + if (item.HasImage(ImageType.Primary)) + { + return item; + } + + var parent = item.GetParent(); + if (parent is UserRootFolder) + { + return null; + } + + // terminate in case we went past user root folder (unlikely?) + if (parent is Folder folder && folder.IsRoot) + { + return null; + } + + return GetFirstParentWithImageBelowUserRoot(parent); + } + private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type) { var imageInfo = item.GetImageInfo(type, 0); @@ -1096,7 +1171,9 @@ namespace Emby.Dlna.Didl private ImageUrlInfo GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format) { - var url = string.Format("{0}/Items/{1}/Images/{2}/0/{3}/{4}/{5}/{6}/0/0", + var url = string.Format( + CultureInfo.InvariantCulture, + "{0}/Items/{1}/Images/{2}/0/{3}/{4}/{5}/{6}/0/0", _serverAddress, info.ItemId.ToString("N", CultureInfo.InvariantCulture), info.Type, diff --git a/Emby.Dlna/Didl/Filter.cs b/Emby.Dlna/Didl/Filter.cs index f6217d91e..412259e90 100644 --- a/Emby.Dlna/Didl/Filter.cs +++ b/Emby.Dlna/Didl/Filter.cs @@ -1,7 +1,6 @@ #pragma warning disable CS1591 using System; -using MediaBrowser.Model.Extensions; namespace Emby.Dlna.Didl { diff --git a/Emby.Dlna/Didl/StringWriterWithEncoding.cs b/Emby.Dlna/Didl/StringWriterWithEncoding.cs index 67fc56ec0..896fe992b 100644 --- a/Emby.Dlna/Didl/StringWriterWithEncoding.cs +++ b/Emby.Dlna/Didl/StringWriterWithEncoding.cs @@ -53,6 +53,6 @@ namespace Emby.Dlna.Didl _encoding = encoding; } - public override Encoding Encoding => (null == _encoding) ? base.Encoding : _encoding; + public override Encoding Encoding => _encoding ?? base.Encoding; } } diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 770d48168..c5d60b2a0 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -262,8 +262,8 @@ namespace Emby.Dlna.Main { if (address.AddressFamily == AddressFamily.InterNetworkV6) { - // Not support IPv6 right now - continue; + // Not supporting IPv6 right now + continue; } var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs index b77a2bbac..6abc3a82c 100644 --- a/Emby.Dlna/PlayTo/Device.cs +++ b/Emby.Dlna/PlayTo/Device.cs @@ -346,7 +346,12 @@ namespace Emby.Dlna.PlayTo throw new InvalidOperationException("Unable to find service"); } - return new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1)); + return new SsdpHttpClient(_httpClient).SendCommandAsync( + Properties.BaseUrl, + service, + command.Name, + avCommands.BuildPost(command, service.ServiceType, 1), + cancellationToken: cancellationToken); } public async Task SetPlay(CancellationToken cancellationToken) @@ -515,8 +520,12 @@ namespace Emby.Dlna.PlayTo return; } - var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), true) - .ConfigureAwait(false); + var result = await new SsdpHttpClient(_httpClient).SendCommandAsync( + Properties.BaseUrl, + service, + command.Name, + rendererCommands.BuildPost(command, service.ServiceType), + cancellationToken: cancellationToken).ConfigureAwait(false); if (result == null || result.Document == null) { @@ -561,8 +570,12 @@ namespace Emby.Dlna.PlayTo return; } - var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), true) - .ConfigureAwait(false); + var result = await new SsdpHttpClient(_httpClient).SendCommandAsync( + Properties.BaseUrl, + service, + command.Name, + rendererCommands.BuildPost(command, service.ServiceType), + cancellationToken: cancellationToken).ConfigureAwait(false); if (result == null || result.Document == null) return; @@ -588,8 +601,12 @@ namespace Emby.Dlna.PlayTo return null; } - var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType), false) - .ConfigureAwait(false); + var result = await new SsdpHttpClient(_httpClient).SendCommandAsync( + Properties.BaseUrl, + service, + command.Name, + avCommands.BuildPost(command, service.ServiceType), + cancellationToken: cancellationToken).ConfigureAwait(false); if (result == null || result.Document == null) { @@ -599,7 +616,7 @@ namespace Emby.Dlna.PlayTo var transportState = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i != null); - var transportStateValue = transportState == null ? null : transportState.Value; + var transportStateValue = transportState?.Value; if (transportStateValue != null && Enum.TryParse(transportStateValue, true, out TRANSPORTSTATE state)) @@ -626,8 +643,12 @@ namespace Emby.Dlna.PlayTo var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), false) - .ConfigureAwait(false); + var result = await new SsdpHttpClient(_httpClient).SendCommandAsync( + Properties.BaseUrl, + service, + command.Name, + rendererCommands.BuildPost(command, service.ServiceType), + cancellationToken: cancellationToken).ConfigureAwait(false); if (result == null || result.Document == null) { @@ -689,8 +710,12 @@ namespace Emby.Dlna.PlayTo var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false); - var result = await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, rendererCommands.BuildPost(command, service.ServiceType), false) - .ConfigureAwait(false); + var result = await new SsdpHttpClient(_httpClient).SendCommandAsync( + Properties.BaseUrl, + service, + command.Name, + rendererCommands.BuildPost(command, service.ServiceType), + cancellationToken: cancellationToken).ConfigureAwait(false); if (result == null || result.Document == null) { diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs index cf978d742..43e983054 100644 --- a/Emby.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -27,6 +27,8 @@ namespace Emby.Dlna.PlayTo { public class PlayToController : ISessionController, IDisposable { + private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US")); + private Device _device; private readonly SessionInfo _session; private readonly ISessionManager _sessionManager; @@ -45,9 +47,10 @@ namespace Emby.Dlna.PlayTo private readonly string _serverAddress; private readonly string _accessToken; - public bool IsSessionActive => !_disposed && _device != null; + private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>(); + private int _currentPlaylistIndex; - public bool SupportsMediaControl => IsSessionActive; + private bool _disposed; public PlayToController( SessionInfo session, @@ -83,18 +86,22 @@ namespace Emby.Dlna.PlayTo _mediaEncoder = mediaEncoder; } + public bool IsSessionActive => !_disposed && _device != null; + + public bool SupportsMediaControl => IsSessionActive; + public void Init(Device device) { _device = device; _device.OnDeviceUnavailable = OnDeviceUnavailable; - _device.PlaybackStart += _device_PlaybackStart; - _device.PlaybackProgress += _device_PlaybackProgress; - _device.PlaybackStopped += _device_PlaybackStopped; - _device.MediaChanged += _device_MediaChanged; + _device.PlaybackStart += OnDevicePlaybackStart; + _device.PlaybackProgress += OnDevicePlaybackProgress; + _device.PlaybackStopped += OnDevicePlaybackStopped; + _device.MediaChanged += OnDeviceMediaChanged; _device.Start(); - _deviceDiscovery.DeviceLeft += _deviceDiscovery_DeviceLeft; + _deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft; } private void OnDeviceUnavailable() @@ -110,7 +117,7 @@ namespace Emby.Dlna.PlayTo } } - void _deviceDiscovery_DeviceLeft(object sender, GenericEventArgs<UpnpDeviceInfo> e) + private void OnDeviceDiscoveryDeviceLeft(object sender, GenericEventArgs<UpnpDeviceInfo> e) { var info = e.Argument; @@ -125,7 +132,7 @@ namespace Emby.Dlna.PlayTo } } - async void _device_MediaChanged(object sender, MediaChangedEventArgs e) + private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e) { if (_disposed) { @@ -137,15 +144,15 @@ namespace Emby.Dlna.PlayTo var streamInfo = StreamParams.ParseFromUrl(e.OldMediaInfo.Url, _libraryManager, _mediaSourceManager); if (streamInfo.Item != null) { - var positionTicks = GetProgressPositionTicks(e.OldMediaInfo, streamInfo); + var positionTicks = GetProgressPositionTicks(streamInfo); - ReportPlaybackStopped(e.OldMediaInfo, streamInfo, positionTicks); + ReportPlaybackStopped(streamInfo, positionTicks); } streamInfo = StreamParams.ParseFromUrl(e.NewMediaInfo.Url, _libraryManager, _mediaSourceManager); if (streamInfo.Item == null) return; - var newItemProgress = GetProgressInfo(e.NewMediaInfo, streamInfo); + var newItemProgress = GetProgressInfo(streamInfo); await _sessionManager.OnPlaybackStart(newItemProgress).ConfigureAwait(false); } @@ -155,7 +162,7 @@ namespace Emby.Dlna.PlayTo } } - async void _device_PlaybackStopped(object sender, PlaybackStoppedEventArgs e) + private async void OnDevicePlaybackStopped(object sender, PlaybackStoppedEventArgs e) { if (_disposed) { @@ -168,9 +175,9 @@ namespace Emby.Dlna.PlayTo if (streamInfo.Item == null) return; - var positionTicks = GetProgressPositionTicks(e.MediaInfo, streamInfo); + var positionTicks = GetProgressPositionTicks(streamInfo); - ReportPlaybackStopped(e.MediaInfo, streamInfo, positionTicks); + ReportPlaybackStopped(streamInfo, positionTicks); var mediaSource = await streamInfo.GetMediaSource(CancellationToken.None).ConfigureAwait(false); @@ -194,7 +201,7 @@ namespace Emby.Dlna.PlayTo } else { - Playlist.Clear(); + _playlist.Clear(); } } catch (Exception ex) @@ -203,7 +210,7 @@ namespace Emby.Dlna.PlayTo } } - private async void ReportPlaybackStopped(uBaseObject mediaInfo, StreamParams streamInfo, long? positionTicks) + private async void ReportPlaybackStopped(StreamParams streamInfo, long? positionTicks) { try { @@ -222,7 +229,7 @@ namespace Emby.Dlna.PlayTo } } - async void _device_PlaybackStart(object sender, PlaybackStartEventArgs e) + private async void OnDevicePlaybackStart(object sender, PlaybackStartEventArgs e) { if (_disposed) { @@ -235,7 +242,7 @@ namespace Emby.Dlna.PlayTo if (info.Item != null) { - var progress = GetProgressInfo(e.MediaInfo, info); + var progress = GetProgressInfo(info); await _sessionManager.OnPlaybackStart(progress).ConfigureAwait(false); } @@ -246,7 +253,7 @@ namespace Emby.Dlna.PlayTo } } - async void _device_PlaybackProgress(object sender, PlaybackProgressEventArgs e) + private async void OnDevicePlaybackProgress(object sender, PlaybackProgressEventArgs e) { if (_disposed) { @@ -266,7 +273,7 @@ namespace Emby.Dlna.PlayTo if (info.Item != null) { - var progress = GetProgressInfo(e.MediaInfo, info); + var progress = GetProgressInfo(info); await _sessionManager.OnPlaybackProgress(progress).ConfigureAwait(false); } @@ -277,7 +284,7 @@ namespace Emby.Dlna.PlayTo } } - private long? GetProgressPositionTicks(uBaseObject mediaInfo, StreamParams info) + private long? GetProgressPositionTicks(StreamParams info) { var ticks = _device.Position.Ticks; @@ -289,13 +296,13 @@ namespace Emby.Dlna.PlayTo return ticks; } - private PlaybackStartInfo GetProgressInfo(uBaseObject mediaInfo, StreamParams info) + private PlaybackStartInfo GetProgressInfo(StreamParams info) { return new PlaybackStartInfo { ItemId = info.ItemId, SessionId = _session.Id, - PositionTicks = GetProgressPositionTicks(mediaInfo, info), + PositionTicks = GetProgressPositionTicks(info), IsMuted = _device.IsMuted, IsPaused = _device.IsPaused, MediaSourceId = info.MediaSourceId, @@ -310,9 +317,7 @@ namespace Emby.Dlna.PlayTo }; } - #region SendCommands - - public async Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken) + public Task SendPlayCommand(PlayRequest command, CancellationToken cancellationToken) { _logger.LogDebug("{0} - Received PlayRequest: {1}", this._session.DeviceName, command.PlayCommand); @@ -350,11 +355,12 @@ namespace Emby.Dlna.PlayTo if (command.PlayCommand == PlayCommand.PlayLast) { - Playlist.AddRange(playlist); + _playlist.AddRange(playlist); } + if (command.PlayCommand == PlayCommand.PlayNext) { - Playlist.AddRange(playlist); + _playlist.AddRange(playlist); } if (!command.ControllingUserId.Equals(Guid.Empty)) @@ -363,7 +369,7 @@ namespace Emby.Dlna.PlayTo _session.DeviceName, _session.RemoteEndPoint, user); } - await PlayItems(playlist).ConfigureAwait(false); + return PlayItems(playlist, cancellationToken); } private Task SendPlaystateCommand(PlaystateRequest command, CancellationToken cancellationToken) @@ -371,7 +377,7 @@ namespace Emby.Dlna.PlayTo switch (command.Command) { case PlaystateCommand.Stop: - Playlist.Clear(); + _playlist.Clear(); return _device.SetStop(CancellationToken.None); case PlaystateCommand.Pause: @@ -387,10 +393,10 @@ namespace Emby.Dlna.PlayTo return Seek(command.SeekPositionTicks ?? 0); case PlaystateCommand.NextTrack: - return SetPlaylistIndex(_currentPlaylistIndex + 1); + return SetPlaylistIndex(_currentPlaylistIndex + 1, cancellationToken); case PlaystateCommand.PreviousTrack: - return SetPlaylistIndex(_currentPlaylistIndex - 1); + return SetPlaylistIndex(_currentPlaylistIndex - 1, cancellationToken); } return Task.CompletedTask; @@ -426,14 +432,6 @@ namespace Emby.Dlna.PlayTo return info.IsDirectStream; } - #endregion - - #region Playlist - - private int _currentPlaylistIndex; - private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>(); - private List<PlaylistItem> Playlist => _playlist; - private void AddItemFromId(Guid id, List<BaseItem> list) { var item = _libraryManager.GetItemById(id); @@ -451,7 +449,7 @@ namespace Emby.Dlna.PlayTo _dlnaManager.GetDefaultProfile(); var mediaSources = item is IHasMediaSources - ? (_mediaSourceManager.GetStaticMediaSources(item, true, user)) + ? _mediaSourceManager.GetStaticMediaSources(item, true, user) : new List<MediaSourceInfo>(); var playlistItem = GetPlaylistItem(item, mediaSources, profile, _session.DeviceId, mediaSourceId, audioStreamIndex, subtitleStreamIndex); @@ -459,8 +457,19 @@ namespace Emby.Dlna.PlayTo playlistItem.StreamUrl = DidlBuilder.NormalizeDlnaMediaUrl(playlistItem.StreamInfo.ToUrl(_serverAddress, _accessToken)); - var itemXml = new DidlBuilder(profile, user, _imageProcessor, _serverAddress, _accessToken, _userDataManager, _localization, _mediaSourceManager, _logger, _mediaEncoder) - .GetItemDidl(_config.GetDlnaConfiguration(), item, user, null, _session.DeviceId, new Filter(), playlistItem.StreamInfo); + var itemXml = new DidlBuilder( + profile, + user, + _imageProcessor, + _serverAddress, + _accessToken, + _userDataManager, + _localization, + _mediaSourceManager, + _logger, + _mediaEncoder, + _libraryManager) + .GetItemDidl(item, user, null, _session.DeviceId, new Filter(), playlistItem.StreamInfo); playlistItem.Didl = itemXml; @@ -570,30 +579,31 @@ namespace Emby.Dlna.PlayTo /// Plays the items. /// </summary> /// <param name="items">The items.</param> - /// <returns></returns> - private async Task<bool> PlayItems(IEnumerable<PlaylistItem> items) + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns><c>true</c> on success.</returns> + private async Task<bool> PlayItems(IEnumerable<PlaylistItem> items, CancellationToken cancellationToken = default) { - Playlist.Clear(); - Playlist.AddRange(items); - _logger.LogDebug("{0} - Playing {1} items", _session.DeviceName, Playlist.Count); + _playlist.Clear(); + _playlist.AddRange(items); + _logger.LogDebug("{0} - Playing {1} items", _session.DeviceName, _playlist.Count); - await SetPlaylistIndex(0).ConfigureAwait(false); + await SetPlaylistIndex(0, cancellationToken).ConfigureAwait(false); return true; } - private async Task SetPlaylistIndex(int index) + private async Task SetPlaylistIndex(int index, CancellationToken cancellationToken = default) { - if (index < 0 || index >= Playlist.Count) + if (index < 0 || index >= _playlist.Count) { - Playlist.Clear(); - await _device.SetStop(CancellationToken.None); + _playlist.Clear(); + await _device.SetStop(cancellationToken).ConfigureAwait(false); return; } _currentPlaylistIndex = index; - var currentitem = Playlist[index]; + var currentitem = _playlist[index]; - await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, CancellationToken.None); + await _device.SetAvTransport(currentitem.StreamUrl, GetDlnaHeaders(currentitem), currentitem.Didl, cancellationToken).ConfigureAwait(false); var streamInfo = currentitem.StreamInfo; if (streamInfo.StartPositionTicks > 0 && EnableClientSideSeek(streamInfo)) @@ -602,10 +612,7 @@ namespace Emby.Dlna.PlayTo } } - #endregion - - private bool _disposed; - + /// <inheritdoc /> public void Dispose() { Dispose(true); @@ -624,19 +631,17 @@ namespace Emby.Dlna.PlayTo _device.Dispose(); } - _device.PlaybackStart -= _device_PlaybackStart; - _device.PlaybackProgress -= _device_PlaybackProgress; - _device.PlaybackStopped -= _device_PlaybackStopped; - _device.MediaChanged -= _device_MediaChanged; - _deviceDiscovery.DeviceLeft -= _deviceDiscovery_DeviceLeft; + _device.PlaybackStart -= OnDevicePlaybackStart; + _device.PlaybackProgress -= OnDevicePlaybackProgress; + _device.PlaybackStopped -= OnDevicePlaybackStopped; + _device.MediaChanged -= OnDeviceMediaChanged; + _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft; _device.OnDeviceUnavailable = null; _device = null; _disposed = true; } - private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US")); - private Task SendGeneralCommand(GeneralCommand command, CancellationToken cancellationToken) { if (Enum.TryParse(command.Name, true, out GeneralCommandType commandType)) @@ -713,7 +718,7 @@ namespace Emby.Dlna.PlayTo if (info.Item != null) { - var newPosition = GetProgressPositionTicks(media, info) ?? 0; + var newPosition = GetProgressPositionTicks(info) ?? 0; var user = !_session.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(_session.UserId) : null; var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, newIndex, info.SubtitleStreamIndex); @@ -738,7 +743,7 @@ namespace Emby.Dlna.PlayTo if (info.Item != null) { - var newPosition = GetProgressPositionTicks(media, info) ?? 0; + var newPosition = GetProgressPositionTicks(info) ?? 0; var user = !_session.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(_session.UserId) : null; var newItem = CreatePlaylistItem(info.Item, user, newPosition, info.MediaSourceId, info.AudioStreamIndex, newIndex); @@ -852,8 +857,11 @@ namespace Emby.Dlna.PlayTo return request; } - var index = url.IndexOf('?'); - if (index == -1) return request; + var index = url.IndexOf('?', StringComparison.Ordinal); + if (index == -1) + { + return request; + } var query = url.Substring(index + 1); Dictionary<string, string> values = QueryHelpers.ParseQuery(query).ToDictionary(kv => kv.Key, kv => kv.Value.ToString()); diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs index b8a47c44c..bbedd1485 100644 --- a/Emby.Dlna/PlayTo/PlayToManager.cs +++ b/Emby.Dlna/PlayTo/PlayToManager.cs @@ -23,7 +23,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Dlna.PlayTo { - public class PlayToManager : IDisposable + public sealed class PlayToManager : IDisposable { private readonly ILogger _logger; private readonly ISessionManager _sessionManager; @@ -231,6 +231,7 @@ namespace Emby.Dlna.PlayTo } } + /// <inheritdoc /> public void Dispose() { _deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered; @@ -244,6 +245,9 @@ namespace Emby.Dlna.PlayTo } + _sessionLock.Dispose(); + _disposeCancellationTokenSource.Dispose(); + _disposed = true; } } diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs index dab5f29bd..8c1362007 100644 --- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs +++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs @@ -32,18 +32,15 @@ namespace Emby.Dlna.PlayTo DeviceService service, string command, string postData, - bool logRequest = true, - string header = null) + string header = null, + CancellationToken cancellationToken = default) { - var cancellationToken = CancellationToken.None; - var url = NormalizeServiceUrl(baseUrl, service.ControlUrl); using (var response = await PostSoapDataAsync( url, $"\"{service.ServiceType}#{command}\"", postData, header, - logRequest, cancellationToken) .ConfigureAwait(false)) using (var stream = response.Content) @@ -63,7 +60,7 @@ namespace Emby.Dlna.PlayTo return serviceUrl; } - if (!serviceUrl.StartsWith("/")) + if (!serviceUrl.StartsWith("/", StringComparison.Ordinal)) { serviceUrl = "/" + serviceUrl; } @@ -127,7 +124,6 @@ namespace Emby.Dlna.PlayTo string soapAction, string postData, string header, - bool logRequest, CancellationToken cancellationToken) { if (soapAction[0] != '\"') diff --git a/Emby.Naming/AudioBook/AudioBookFileInfo.cs b/Emby.Naming/AudioBook/AudioBookFileInfo.cs index 0bc6ec7e4..c4863b50a 100644 --- a/Emby.Naming/AudioBook/AudioBookFileInfo.cs +++ b/Emby.Naming/AudioBook/AudioBookFileInfo.cs @@ -37,7 +37,7 @@ namespace Emby.Naming.AudioBook /// <value>The type.</value> public bool IsDirectory { get; set; } - /// <inheritdoc/> + /// <inheritdoc /> public int CompareTo(AudioBookFileInfo other) { if (ReferenceEquals(this, other)) diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs index 081510f95..f4ba11a0d 100644 --- a/Emby.Naming/AudioBook/AudioBookListResolver.cs +++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs @@ -29,11 +29,7 @@ namespace Emby.Naming.AudioBook // Filter out all extras, otherwise they could cause stacks to not be resolved // See the unit test TestStackedWithTrailer var metadata = audiobookFileInfos - .Select(i => new FileSystemMetadata - { - FullName = i.Path, - IsDirectory = i.IsDirectory - }); + .Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory }); var stackResult = new StackResolver(_options) .ResolveAudioBooks(metadata); @@ -42,11 +38,7 @@ namespace Emby.Naming.AudioBook { var stackFiles = stack.Files.Select(i => audioBookResolver.Resolve(i, stack.IsDirectoryStack)).ToList(); stackFiles.Sort(); - var info = new AudioBookInfo - { - Files = stackFiles, - Name = stack.Name - }; + var info = new AudioBookInfo { Files = stackFiles, Name = stack.Name }; yield return info; } diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 793847f84..a2d75d0b8 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -136,7 +136,8 @@ namespace Emby.Naming.Common CleanDateTimes = new[] { - @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*" + @"(.+[^_\,\.\(\)\[\]\-])[_\.\(\)\[\]\-](19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*", + @"(.+[^_\,\.\(\)\[\]\-])[ _\.\(\)\[\]\-]+(19\d{2}|20\d{2})([ _\,\.\(\)\[\]\-][^0-9]|).*(19\d{2}|20\d{2})*" }; CleanStrings = new[] @@ -505,7 +506,63 @@ namespace Emby.Naming.Common RuleType = ExtraRuleType.Suffix, Token = "-short", MediaType = MediaType.Video - } + }, + new ExtraRule + { + ExtraType = ExtraType.BehindTheScenes, + RuleType = ExtraRuleType.DirectoryName, + Token = "behind the scenes", + MediaType = MediaType.Video, + }, + new ExtraRule + { + ExtraType = ExtraType.DeletedScene, + RuleType = ExtraRuleType.DirectoryName, + Token = "deleted scenes", + MediaType = MediaType.Video, + }, + new ExtraRule + { + ExtraType = ExtraType.Interview, + RuleType = ExtraRuleType.DirectoryName, + Token = "interviews", + MediaType = MediaType.Video, + }, + new ExtraRule + { + ExtraType = ExtraType.Scene, + RuleType = ExtraRuleType.DirectoryName, + Token = "scenes", + MediaType = MediaType.Video, + }, + new ExtraRule + { + ExtraType = ExtraType.Sample, + RuleType = ExtraRuleType.DirectoryName, + Token = "samples", + MediaType = MediaType.Video, + }, + new ExtraRule + { + ExtraType = ExtraType.Clip, + RuleType = ExtraRuleType.DirectoryName, + Token = "shorts", + MediaType = MediaType.Video, + }, + new ExtraRule + { + ExtraType = ExtraType.Clip, + RuleType = ExtraRuleType.DirectoryName, + Token = "featurettes", + MediaType = MediaType.Video, + }, + new ExtraRule + { + ExtraType = ExtraType.Unknown, + RuleType = ExtraRuleType.DirectoryName, + Token = "extras", + MediaType = MediaType.Video, + }, }; Format3DRules = new[] diff --git a/Emby.Naming/Subtitles/SubtitleParser.cs b/Emby.Naming/Subtitles/SubtitleParser.cs index 082696da4..88ec3e2d6 100644 --- a/Emby.Naming/Subtitles/SubtitleParser.cs +++ b/Emby.Naming/Subtitles/SubtitleParser.cs @@ -37,7 +37,8 @@ namespace Emby.Naming.Subtitles IsForced = _options.SubtitleForcedFlags.Any(i => flags.Contains(i, StringComparer.OrdinalIgnoreCase)) }; - var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparer.OrdinalIgnoreCase) && !_options.SubtitleForcedFlags.Contains(i, StringComparer.OrdinalIgnoreCase)) + var parts = flags.Where(i => !_options.SubtitleDefaultFlags.Contains(i, StringComparer.OrdinalIgnoreCase) + && !_options.SubtitleForcedFlags.Contains(i, StringComparer.OrdinalIgnoreCase)) .ToList(); // Should have a name, language and file extension diff --git a/Emby.Naming/TV/EpisodePathParser.cs b/Emby.Naming/TV/EpisodePathParser.cs index d3a822b17..a6af689c7 100644 --- a/Emby.Naming/TV/EpisodePathParser.cs +++ b/Emby.Naming/TV/EpisodePathParser.cs @@ -18,7 +18,13 @@ namespace Emby.Naming.TV _options = options; } - public EpisodePathParserResult Parse(string path, bool isDirectory, bool? isNamed = null, bool? isOptimistic = null, bool? supportsAbsoluteNumbers = null, bool fillExtendedInfo = true) + public EpisodePathParserResult Parse( + string path, + bool isDirectory, + bool? isNamed = null, + bool? isOptimistic = null, + bool? supportsAbsoluteNumbers = null, + bool fillExtendedInfo = true) { // Added to be able to use regex patterns which require a file extension. // There were no failed tests without this block, but to be safe, we can keep it until @@ -64,7 +70,7 @@ namespace Emby.Naming.TV { result.SeriesName = result.SeriesName .Trim() - .Trim(new[] { '_', '.', '-' }) + .Trim('_', '.', '-') .Trim(); } } diff --git a/Emby.Naming/TV/SeasonPathParserResult.cs b/Emby.Naming/TV/SeasonPathParserResult.cs index 44090c059..a142fafea 100644 --- a/Emby.Naming/TV/SeasonPathParserResult.cs +++ b/Emby.Naming/TV/SeasonPathParserResult.cs @@ -11,7 +11,7 @@ namespace Emby.Naming.TV public int? SeasonNumber { get; set; } /// <summary> - /// Gets or sets a value indicating whether this <see cref="SeasonPathParserResult"/> is success. + /// Gets or sets a value indicating whether this <see cref="SeasonPathParserResult" /> is success. /// </summary> /// <value><c>true</c> if success; otherwise, <c>false</c>.</value> public bool Success { get; set; } diff --git a/Emby.Naming/Video/ExtraResolver.cs b/Emby.Naming/Video/ExtraResolver.cs index 42a5c88b3..fc0424faa 100644 --- a/Emby.Naming/Video/ExtraResolver.cs +++ b/Emby.Naming/Video/ExtraResolver.cs @@ -80,6 +80,15 @@ namespace Emby.Naming.Video result.Rule = rule; } } + else if (rule.RuleType == ExtraRuleType.DirectoryName) + { + var directoryName = Path.GetFileName(Path.GetDirectoryName(path)); + if (string.Equals(directoryName, rule.Token, StringComparison.OrdinalIgnoreCase)) + { + result.ExtraType = rule.ExtraType; + result.Rule = rule; + } + } return result; } diff --git a/Emby.Naming/Video/ExtraRule.cs b/Emby.Naming/Video/ExtraRule.cs index cb58a3934..7c9702e24 100644 --- a/Emby.Naming/Video/ExtraRule.cs +++ b/Emby.Naming/Video/ExtraRule.cs @@ -5,30 +5,29 @@ using MediaType = Emby.Naming.Common.MediaType; namespace Emby.Naming.Video { + /// <summary> + /// A rule used to match a file path with an <see cref="MediaBrowser.Model.Entities.ExtraType"/>. + /// </summary> public class ExtraRule { /// <summary> - /// Gets or sets the token. + /// Gets or sets the token to use for matching against the file path. /// </summary> - /// <value>The token.</value> public string Token { get; set; } /// <summary> - /// Gets or sets the type of the extra. + /// Gets or sets the type of the extra to return when matched. /// </summary> - /// <value>The type of the extra.</value> public ExtraType ExtraType { get; set; } /// <summary> /// Gets or sets the type of the rule. /// </summary> - /// <value>The type of the rule.</value> public ExtraRuleType RuleType { get; set; } /// <summary> - /// Gets or sets the type of the media. + /// Gets or sets the type of the media to return when matched. /// </summary> - /// <value>The type of the media.</value> public MediaType MediaType { get; set; } } } diff --git a/Emby.Naming/Video/ExtraRuleType.cs b/Emby.Naming/Video/ExtraRuleType.cs index b021a04a3..e89876f4a 100644 --- a/Emby.Naming/Video/ExtraRuleType.cs +++ b/Emby.Naming/Video/ExtraRuleType.cs @@ -5,18 +5,23 @@ namespace Emby.Naming.Video public enum ExtraRuleType { /// <summary> - /// The suffix + /// Match <see cref="ExtraRule.Token"/> against a suffix in the file name. /// </summary> Suffix = 0, /// <summary> - /// The filename + /// Match <see cref="ExtraRule.Token"/> against the file name, excluding the file extension. /// </summary> Filename = 1, /// <summary> - /// The regex + /// Match <see cref="ExtraRule.Token"/> against the file name, including the file extension. /// </summary> - Regex = 2 + Regex = 2, + + /// <summary> + /// Match <see cref="ExtraRule.Token"/> against the name of the directory containing the file. + /// </summary> + DirectoryName = 3, } } diff --git a/Emby.Naming/Video/StackResolver.cs b/Emby.Naming/Video/StackResolver.cs index ee05904c7..f733cd262 100644 --- a/Emby.Naming/Video/StackResolver.cs +++ b/Emby.Naming/Video/StackResolver.cs @@ -21,31 +21,24 @@ namespace Emby.Naming.Video public IEnumerable<FileStack> ResolveDirectories(IEnumerable<string> files) { - return Resolve(files.Select(i => new FileSystemMetadata - { - FullName = i, - IsDirectory = true - })); + return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = true })); } public IEnumerable<FileStack> ResolveFiles(IEnumerable<string> files) { - return Resolve(files.Select(i => new FileSystemMetadata - { - FullName = i, - IsDirectory = false - })); + return Resolve(files.Select(i => new FileSystemMetadata { FullName = i, IsDirectory = false })); } public IEnumerable<FileStack> ResolveAudioBooks(IEnumerable<FileSystemMetadata> files) { - foreach (var directory in files.GroupBy(file => file.IsDirectory ? file.FullName : Path.GetDirectoryName(file.FullName))) + var groupedDirectoryFiles = files.GroupBy(file => + file.IsDirectory + ? file.FullName + : Path.GetDirectoryName(file.FullName)); + + foreach (var directory in groupedDirectoryFiles) { - var stack = new FileStack() - { - Name = Path.GetFileName(directory.Key), - IsDirectoryStack = false - }; + var stack = new FileStack { Name = Path.GetFileName(directory.Key), IsDirectoryStack = false }; foreach (var file in directory) { if (file.IsDirectory) diff --git a/Emby.Naming/Video/VideoFileInfo.cs b/Emby.Naming/Video/VideoFileInfo.cs index aa4f3a35c..11e789b66 100644 --- a/Emby.Naming/Video/VideoFileInfo.cs +++ b/Emby.Naming/Video/VideoFileInfo.cs @@ -77,7 +77,9 @@ namespace Emby.Naming.Video /// Gets the file name without extension. /// </summary> /// <value>The file name without extension.</value> - public string FileNameWithoutExtension => !IsDirectory ? System.IO.Path.GetFileNameWithoutExtension(Path) : System.IO.Path.GetFileName(Path); + public string FileNameWithoutExtension => !IsDirectory + ? System.IO.Path.GetFileNameWithoutExtension(Path) + : System.IO.Path.GetFileName(Path); /// <inheritdoc /> public override string ToString() diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs index d4b02cf2a..7f755fd25 100644 --- a/Emby.Naming/Video/VideoListResolver.cs +++ b/Emby.Naming/Video/VideoListResolver.cs @@ -33,11 +33,7 @@ namespace Emby.Naming.Video // See the unit test TestStackedWithTrailer var nonExtras = videoInfos .Where(i => i.ExtraType == null) - .Select(i => new FileSystemMetadata - { - FullName = i.Path, - IsDirectory = i.IsDirectory - }); + .Select(i => new FileSystemMetadata { FullName = i.Path, IsDirectory = i.IsDirectory }); var stackResult = new StackResolver(_options) .Resolve(nonExtras).ToList(); @@ -57,11 +53,7 @@ namespace Emby.Naming.Video info.Year = info.Files[0].Year; - var extraBaseNames = new List<string> - { - stack.Name, - Path.GetFileNameWithoutExtension(stack.Files[0]) - }; + var extraBaseNames = new List<string> { stack.Name, Path.GetFileNameWithoutExtension(stack.Files[0]) }; var extras = GetExtras(remainingFiles, extraBaseNames); @@ -83,10 +75,7 @@ namespace Emby.Naming.Video foreach (var media in standaloneMedia) { - var info = new VideoInfo(media.Name) - { - Files = new List<VideoFileInfo> { media } - }; + var info = new VideoInfo(media.Name) { Files = new List<VideoFileInfo> { media } }; info.Year = info.Files[0].Year; @@ -222,8 +211,8 @@ namespace Emby.Naming.Video { testFilename = testFilename.Substring(folderName.Length).Trim(); return string.IsNullOrEmpty(testFilename) - || testFilename[0] == '-' - || string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty)); + || testFilename[0] == '-' + || string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty)); } return false; @@ -239,7 +228,8 @@ namespace Emby.Naming.Video return remainingFiles .Where(i => i.ExtraType == null) - .Where(i => baseNames.Any(b => i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase))) + .Where(i => baseNames.Any(b => + i.FileNameWithoutExtension.StartsWith(b, StringComparison.OrdinalIgnoreCase))) .ToList(); } } diff --git a/Emby.Notifications/Api/NotificationsService.cs b/Emby.Notifications/Api/NotificationsService.cs index f2f381838..788750796 100644 --- a/Emby.Notifications/Api/NotificationsService.cs +++ b/Emby.Notifications/Api/NotificationsService.cs @@ -1,6 +1,5 @@ #pragma warning disable CS1591 #pragma warning disable SA1402 -#pragma warning disable SA1600 #pragma warning disable SA1649 using System; @@ -135,19 +134,19 @@ namespace Emby.Notifications.Api _userManager = userManager; } - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] public object Get(GetNotificationTypes request) { return _notificationManager.GetNotificationTypes(); } - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] public object Get(GetNotificationServices request) { return _notificationManager.GetNotificationServices().ToList(); } - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] public object Get(GetNotificationsSummary request) { return new NotificationsSummary @@ -171,17 +170,17 @@ namespace Emby.Notifications.Api return _notificationManager.SendNotification(notification, CancellationToken.None); } - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] public void Post(MarkRead request) { } - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] public void Post(MarkUnread request) { } - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request")] + [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] public object Get(GetNotifications request) { return new NotificationResult(); diff --git a/Emby.Notifications/CoreNotificationTypes.cs b/Emby.Notifications/CoreNotificationTypes.cs index 73e0b0256..a602b7221 100644 --- a/Emby.Notifications/CoreNotificationTypes.cs +++ b/Emby.Notifications/CoreNotificationTypes.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; diff --git a/Emby.Notifications/NotificationConfigurationFactory.cs b/Emby.Notifications/NotificationConfigurationFactory.cs index b168ed221..3fb3553d0 100644 --- a/Emby.Notifications/NotificationConfigurationFactory.cs +++ b/Emby.Notifications/NotificationConfigurationFactory.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System.Collections.Generic; using MediaBrowser.Common.Configuration; diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs index 332dfa95c..9f1d5096d 100644 --- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs +++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Globalization; @@ -8,7 +6,6 @@ using System.Text; using System.Threading.Tasks; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; -using MediaBrowser.Controller; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Entities; @@ -28,6 +25,9 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Activity { + /// <summary> + /// Entry point for the activity logger. + /// </summary> public sealed class ActivityLogEntryPoint : IServerEntryPoint { private readonly ILogger _logger; @@ -43,16 +43,15 @@ namespace Emby.Server.Implementations.Activity /// <summary> /// Initializes a new instance of the <see cref="ActivityLogEntryPoint"/> class. /// </summary> - /// <param name="logger"></param> - /// <param name="sessionManager"></param> - /// <param name="deviceManager"></param> - /// <param name="taskManager"></param> - /// <param name="activityManager"></param> - /// <param name="localization"></param> - /// <param name="installationManager"></param> - /// <param name="subManager"></param> - /// <param name="userManager"></param> - /// <param name="appHost"></param> + /// <param name="logger">The logger.</param> + /// <param name="sessionManager">The session manager.</param> + /// <param name="deviceManager">The device manager.</param> + /// <param name="taskManager">The task manager.</param> + /// <param name="activityManager">The activity manager.</param> + /// <param name="localization">The localization manager.</param> + /// <param name="installationManager">The installation manager.</param> + /// <param name="subManager">The subtitle manager.</param> + /// <param name="userManager">The user manager.</param> public ActivityLogEntryPoint( ILogger<ActivityLogEntryPoint> logger, ISessionManager sessionManager, @@ -75,6 +74,7 @@ namespace Emby.Server.Implementations.Activity _userManager = userManager; } + /// <inheritdoc /> public Task RunAsync() { _taskManager.TaskCompleted += OnTaskCompleted; @@ -169,7 +169,12 @@ namespace Emby.Server.Implementations.Activity CreateLogEntry(new ActivityLogEntry { - Name = string.Format(_localization.GetLocalizedString("UserStoppedPlayingItemWithValues"), user.Name, GetItemName(item), e.DeviceName), + Name = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("UserStoppedPlayingItemWithValues"), + user.Name, + GetItemName(item), + e.DeviceName), Type = GetPlaybackStoppedNotificationType(item.MediaType), UserId = user.Id }); @@ -486,8 +491,8 @@ namespace Emby.Server.Implementations.Activity var result = e.Result; var task = e.Task; - var activityTask = task.ScheduledTask as IConfigurableScheduledTask; - if (activityTask != null && !activityTask.IsLogged) + if (task.ScheduledTask is IConfigurableScheduledTask activityTask + && !activityTask.IsLogged) { return; } @@ -561,7 +566,7 @@ namespace Emby.Server.Implementations.Activity /// <summary> /// Constructs a user-friendly string for this TimeSpan instance. /// </summary> - public static string ToUserFriendlyString(TimeSpan span) + private static string ToUserFriendlyString(TimeSpan span) { const int DaysInYear = 365; const int DaysInMonth = 30; diff --git a/Emby.Server.Implementations/Activity/ActivityManager.cs b/Emby.Server.Implementations/Activity/ActivityManager.cs index ee10845cf..c1d8dd8da 100644 --- a/Emby.Server.Implementations/Activity/ActivityManager.cs +++ b/Emby.Server.Implementations/Activity/ActivityManager.cs @@ -11,22 +11,17 @@ namespace Emby.Server.Implementations.Activity { public class ActivityManager : IActivityManager { - public event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated; - private readonly IActivityRepository _repo; - private readonly ILogger _logger; private readonly IUserManager _userManager; - public ActivityManager( - ILoggerFactory loggerFactory, - IActivityRepository repo, - IUserManager userManager) + public ActivityManager(IActivityRepository repo, IUserManager userManager) { - _logger = loggerFactory.CreateLogger(nameof(ActivityManager)); _repo = repo; _userManager = userManager; } + public event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated; + public void Create(ActivityLogEntry entry) { entry.Date = DateTime.UtcNow; diff --git a/Emby.Server.Implementations/Activity/ActivityRepository.cs b/Emby.Server.Implementations/Activity/ActivityRepository.cs index 7be72319e..26fc229f7 100644 --- a/Emby.Server.Implementations/Activity/ActivityRepository.cs +++ b/Emby.Server.Implementations/Activity/ActivityRepository.cs @@ -17,11 +17,12 @@ namespace Emby.Server.Implementations.Activity { public class ActivityRepository : BaseSqliteRepository, IActivityRepository { - private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US")); + private const string BaseActivitySelectText = "select Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity from ActivityLog"; + private readonly IFileSystem _fileSystem; - public ActivityRepository(ILoggerFactory loggerFactory, IServerApplicationPaths appPaths, IFileSystem fileSystem) - : base(loggerFactory.CreateLogger(nameof(ActivityRepository))) + public ActivityRepository(ILogger<ActivityRepository> logger, IServerApplicationPaths appPaths, IFileSystem fileSystem) + : base(logger) { DbFilePath = Path.Combine(appPaths.DataPath, "activitylog.db"); _fileSystem = fileSystem; @@ -76,8 +77,6 @@ namespace Emby.Server.Implementations.Activity } } - private const string BaseActivitySelectText = "select Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity from ActivityLog"; - public void Create(ActivityLogEntry entry) { if (entry == null) @@ -87,32 +86,34 @@ namespace Emby.Server.Implementations.Activity using (var connection = GetConnection()) { - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("insert into ActivityLog (Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) values (@Name, @Overview, @ShortOverview, @Type, @ItemId, @UserId, @DateCreated, @LogSeverity)")) + connection.RunInTransaction( + db => { - statement.TryBind("@Name", entry.Name); + using (var statement = db.PrepareStatement("insert into ActivityLog (Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) values (@Name, @Overview, @ShortOverview, @Type, @ItemId, @UserId, @DateCreated, @LogSeverity)")) + { + statement.TryBind("@Name", entry.Name); - statement.TryBind("@Overview", entry.Overview); - statement.TryBind("@ShortOverview", entry.ShortOverview); - statement.TryBind("@Type", entry.Type); - statement.TryBind("@ItemId", entry.ItemId); + statement.TryBind("@Overview", entry.Overview); + statement.TryBind("@ShortOverview", entry.ShortOverview); + statement.TryBind("@Type", entry.Type); + statement.TryBind("@ItemId", entry.ItemId); - if (entry.UserId.Equals(Guid.Empty)) - { - statement.TryBindNull("@UserId"); - } - else - { - statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture)); - } + if (entry.UserId.Equals(Guid.Empty)) + { + statement.TryBindNull("@UserId"); + } + else + { + statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture)); + } - statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue()); - statement.TryBind("@LogSeverity", entry.Severity.ToString()); + statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue()); + statement.TryBind("@LogSeverity", entry.Severity.ToString()); - statement.MoveNext(); - } - }, TransactionMode); + statement.MoveNext(); + } + }, + TransactionMode); } } @@ -125,33 +126,35 @@ namespace Emby.Server.Implementations.Activity using (var connection = GetConnection()) { - connection.RunInTransaction(db => - { - using (var statement = db.PrepareStatement("Update ActivityLog set Name=@Name,Overview=@Overview,ShortOverview=@ShortOverview,Type=@Type,ItemId=@ItemId,UserId=@UserId,DateCreated=@DateCreated,LogSeverity=@LogSeverity where Id=@Id")) + connection.RunInTransaction( + db => { - statement.TryBind("@Id", entry.Id); + using (var statement = db.PrepareStatement("Update ActivityLog set Name=@Name,Overview=@Overview,ShortOverview=@ShortOverview,Type=@Type,ItemId=@ItemId,UserId=@UserId,DateCreated=@DateCreated,LogSeverity=@LogSeverity where Id=@Id")) + { + statement.TryBind("@Id", entry.Id); - statement.TryBind("@Name", entry.Name); - statement.TryBind("@Overview", entry.Overview); - statement.TryBind("@ShortOverview", entry.ShortOverview); - statement.TryBind("@Type", entry.Type); - statement.TryBind("@ItemId", entry.ItemId); + statement.TryBind("@Name", entry.Name); + statement.TryBind("@Overview", entry.Overview); + statement.TryBind("@ShortOverview", entry.ShortOverview); + statement.TryBind("@Type", entry.Type); + statement.TryBind("@ItemId", entry.ItemId); - if (entry.UserId.Equals(Guid.Empty)) - { - statement.TryBindNull("@UserId"); - } - else - { - statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture)); - } + if (entry.UserId.Equals(Guid.Empty)) + { + statement.TryBindNull("@UserId"); + } + else + { + statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture)); + } - statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue()); - statement.TryBind("@LogSeverity", entry.Severity.ToString()); + statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue()); + statement.TryBind("@LogSeverity", entry.Severity.ToString()); - statement.MoveNext(); - } - }, TransactionMode); + statement.MoveNext(); + } + }, + TransactionMode); } } @@ -164,6 +167,7 @@ namespace Emby.Server.Implementations.Activity { whereClauses.Add("DateCreated>=@DateCreated"); } + if (hasUserId.HasValue) { if (hasUserId.Value) @@ -204,7 +208,7 @@ namespace Emby.Server.Implementations.Activity if (limit.HasValue) { - commandText += " LIMIT " + limit.Value.ToString(_usCulture); + commandText += " LIMIT " + limit.Value.ToString(CultureInfo.InvariantCulture); } var statementTexts = new[] @@ -304,7 +308,7 @@ namespace Emby.Server.Implementations.Activity index++; if (reader[index].SQLiteType != SQLiteType.Null) { - info.Severity = (LogLevel)Enum.Parse(typeof(LogLevel), reader[index].ToString(), true); + info.Severity = Enum.Parse<LogLevel>(reader[index].ToString(), true); } return info; diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index c3cdcc222..bc4781743 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -5,7 +5,7 @@ using MediaBrowser.Common.Configuration; namespace Emby.Server.Implementations.AppBase { /// <summary> - /// Provides a base class to hold common application paths used by both the Ui and Server. + /// Provides a base class to hold common application paths used by both the UI and Server. /// This can be subclassed to add application-specific paths. /// </summary> public abstract class BaseApplicationPaths : IApplicationPaths @@ -37,10 +37,7 @@ namespace Emby.Server.Implementations.AppBase /// <value>The program data path.</value> public string ProgramDataPath { get; } - /// <summary> - /// Gets the path to the web UI resources folder. - /// </summary> - /// <value>The web UI resources path.</value> + /// <inheritdoc/> public string WebPath { get; } /// <summary> diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index acae23052..3f5ae5536 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -30,7 +30,6 @@ using Emby.Server.Implementations.Configuration; using Emby.Server.Implementations.Cryptography; using Emby.Server.Implementations.Data; using Emby.Server.Implementations.Devices; -using Emby.Server.Implementations.Diagnostics; using Emby.Server.Implementations.Dto; using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.HttpServer.Security; @@ -43,6 +42,7 @@ using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.ScheduledTasks; using Emby.Server.Implementations.Security; using Emby.Server.Implementations.Serialization; +using Emby.Server.Implementations.Services; using Emby.Server.Implementations.Session; using Emby.Server.Implementations.SocketSharp; using Emby.Server.Implementations.TV; @@ -85,7 +85,6 @@ using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Cryptography; -using MediaBrowser.Model.Diagnostics; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Events; using MediaBrowser.Model.Globalization; @@ -99,8 +98,8 @@ using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Updates; using MediaBrowser.Providers.Chapters; using MediaBrowser.Providers.Manager; +using MediaBrowser.Providers.Plugins.TheTvdb; using MediaBrowser.Providers.Subtitles; -using MediaBrowser.Providers.TV.TheTVDB; using MediaBrowser.WebDashboard.Api; using MediaBrowser.XbmcMetadata.Providers; using Microsoft.AspNetCore.Http; @@ -117,6 +116,11 @@ namespace Emby.Server.Implementations /// </summary> public abstract class ApplicationHost : IServerApplicationHost, IDisposable { + /// <summary> + /// The environment variable prefixes to log at server startup. + /// </summary> + private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" }; + private SqliteUserRepository _userRepository; private SqliteDisplayPreferencesRepository _displayPreferencesRepository; @@ -236,11 +240,6 @@ namespace Emby.Server.Implementations public int HttpsPort { get; private set; } /// <summary> - /// Gets the content root for the webhost. - /// </summary> - public string ContentRoot { get; private set; } - - /// <summary> /// Gets the server configuration manager. /// </summary> /// <value>The server configuration manager.</value> @@ -336,8 +335,6 @@ namespace Emby.Server.Implementations internal IImageEncoder ImageEncoder { get; private set; } - protected IProcessFactory ProcessFactory { get; private set; } - protected readonly IXmlSerializer XmlSerializer; protected ISocketFactory SocketFactory { get; private set; } @@ -612,13 +609,7 @@ namespace Emby.Server.Implementations DiscoverTypes(); - await RegisterResources(serviceCollection, startupConfig).ConfigureAwait(false); - - ContentRoot = ServerConfigurationManager.Configuration.DashboardSourcePath; - if (string.IsNullOrEmpty(ContentRoot)) - { - ContentRoot = ServerConfigurationManager.ApplicationPaths.WebPath; - } + await RegisterServices(serviceCollection, startupConfig).ConfigureAwait(false); } public async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next) @@ -649,9 +640,9 @@ namespace Emby.Server.Implementations } /// <summary> - /// Registers resources that classes will depend on + /// Registers services/resources with the service collection that will be available via DI. /// </summary> - protected async Task RegisterResources(IServiceCollection serviceCollection, IConfiguration startupConfig) + protected async Task RegisterServices(IServiceCollection serviceCollection, IConfiguration startupConfig) { serviceCollection.AddMemoryCache(); @@ -666,7 +657,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<ILogger>(Logger); serviceCollection.AddSingleton(FileSystemManager); - serviceCollection.AddSingleton<TvDbClientManager>(); + serviceCollection.AddSingleton<TvdbClientManager>(); HttpClient = new HttpClientManager.HttpClientManager( ApplicationPaths, @@ -685,9 +676,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(XmlSerializer); - ProcessFactory = new ProcessFactory(); - serviceCollection.AddSingleton(ProcessFactory); - serviceCollection.AddSingleton(typeof(IStreamHelper), typeof(StreamHelper)); var cryptoProvider = new CryptographyProvider(); @@ -748,7 +736,6 @@ namespace Emby.Server.Implementations LoggerFactory.CreateLogger<MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(), ServerConfigurationManager, FileSystemManager, - ProcessFactory, LocalizationManager, () => SubtitleEncoder, startupConfig, @@ -769,20 +756,9 @@ namespace Emby.Server.Implementations CertificateInfo = GetCertificateInfo(true); Certificate = GetCertificate(CertificateInfo); - HttpServer = new HttpListenerHost( - this, - LoggerFactory.CreateLogger<HttpListenerHost>(), - ServerConfigurationManager, - startupConfig, - NetworkManager, - JsonSerializer, - XmlSerializer, - CreateHttpListener()) - { - GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading") - }; - - serviceCollection.AddSingleton(HttpServer); + serviceCollection.AddSingleton<ServiceController>(); + serviceCollection.AddSingleton<IHttpListener, WebSocketSharpListener>(); + serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>(); ImageProcessor = new ImageProcessor(LoggerFactory.CreateLogger<ImageProcessor>(), ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder); serviceCollection.AddSingleton(ImageProcessor); @@ -805,7 +781,16 @@ namespace Emby.Server.Implementations DtoService = new DtoService(LoggerFactory, LibraryManager, UserDataManager, ItemRepository, ImageProcessor, ProviderManager, this, () => MediaSourceManager, () => LiveTvManager); serviceCollection.AddSingleton(DtoService); - ChannelManager = new ChannelManager(UserManager, DtoService, LibraryManager, LoggerFactory, ServerConfigurationManager, FileSystemManager, UserDataManager, JsonSerializer, ProviderManager); + ChannelManager = new ChannelManager( + UserManager, + DtoService, + LibraryManager, + LoggerFactory.CreateLogger<ChannelManager>(), + ServerConfigurationManager, + FileSystemManager, + UserDataManager, + JsonSerializer, + ProviderManager); serviceCollection.AddSingleton(ChannelManager); SessionManager = new SessionManager( @@ -844,15 +829,20 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IDeviceDiscovery>(new DeviceDiscovery(ServerConfigurationManager)); - ChapterManager = new ChapterManager(LibraryManager, LoggerFactory, ServerConfigurationManager, ItemRepository); + ChapterManager = new ChapterManager(ItemRepository); serviceCollection.AddSingleton(ChapterManager); - EncodingManager = new MediaEncoder.EncodingManager(FileSystemManager, LoggerFactory, MediaEncoder, ChapterManager, LibraryManager); + EncodingManager = new MediaEncoder.EncodingManager( + LoggerFactory.CreateLogger<MediaEncoder.EncodingManager>(), + FileSystemManager, + MediaEncoder, + ChapterManager, + LibraryManager); serviceCollection.AddSingleton(EncodingManager); var activityLogRepo = GetActivityLogRepository(); serviceCollection.AddSingleton(activityLogRepo); - serviceCollection.AddSingleton<IActivityManager>(new ActivityManager(LoggerFactory, activityLogRepo, UserManager)); + serviceCollection.AddSingleton<IActivityManager>(new ActivityManager(activityLogRepo, UserManager)); var authContext = new AuthorizationContext(AuthenticationRepository, UserManager); serviceCollection.AddSingleton<IAuthorizationContext>(authContext); @@ -868,8 +858,7 @@ namespace Emby.Server.Implementations FileSystemManager, MediaEncoder, HttpClient, - MediaSourceManager, - ProcessFactory); + MediaSourceManager); serviceCollection.AddSingleton(SubtitleEncoder); serviceCollection.AddSingleton(typeof(IResourceFileManager), typeof(ResourceFileManager)); @@ -890,6 +879,14 @@ namespace Emby.Server.Implementations ((LibraryManager)LibraryManager).ItemRepository = ItemRepository; } + /// <summary> + /// Create services registered with the service container that need to be initialized at application startup. + /// </summary> + public void InitializeServices() + { + HttpServer = Resolve<IHttpServer>(); + } + public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths) { // Distinct these to prevent users from reporting problems that aren't actually problems @@ -897,6 +894,18 @@ namespace Emby.Server.Implementations .GetCommandLineArgs() .Distinct(); + // Get all relevant environment variables + var allEnvVars = Environment.GetEnvironmentVariables(); + var relevantEnvVars = new Dictionary<object, object>(); + foreach (var key in allEnvVars.Keys) + { + if (_relevantEnvVarPrefixes.Any(prefix => key.ToString().StartsWith(prefix, StringComparison.OrdinalIgnoreCase))) + { + relevantEnvVars.Add(key, allEnvVars[key]); + } + } + + logger.LogInformation("Environment Variables: {EnvVars}", relevantEnvVars); logger.LogInformation("Arguments: {Args}", commandLineArgs); logger.LogInformation("Operating system: {OS}", OperatingSystem.Name); logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture); @@ -970,7 +979,7 @@ namespace Emby.Server.Implementations private IActivityRepository GetActivityLogRepository() { - var repo = new ActivityRepository(LoggerFactory, ServerConfigurationManager.ApplicationPaths, FileSystemManager); + var repo = new ActivityRepository(LoggerFactory.CreateLogger<ActivityRepository>(), ServerConfigurationManager.ApplicationPaths, FileSystemManager); repo.Initialize(); @@ -1006,48 +1015,12 @@ namespace Emby.Server.Implementations AuthenticatedAttribute.AuthService = AuthService; } - private async void PluginInstalled(object sender, GenericEventArgs<PackageVersionInfo> args) - { - string dir = Path.Combine(ApplicationPaths.PluginsPath, args.Argument.name); - var types = Directory.EnumerateFiles(dir, "*.dll", SearchOption.AllDirectories) - .Select(Assembly.LoadFrom) - .SelectMany(x => x.ExportedTypes) - .Where(x => x.IsClass && !x.IsAbstract && !x.IsInterface && !x.IsGenericType) - .ToArray(); - - int oldLen = _allConcreteTypes.Length; - Array.Resize(ref _allConcreteTypes, oldLen + types.Length); - types.CopyTo(_allConcreteTypes, oldLen); - - var plugins = types.Where(x => x.IsAssignableFrom(typeof(IPlugin))) - .Select(CreateInstanceSafe) - .Where(x => x != null) - .Cast<IPlugin>() - .Select(LoadPlugin) - .Where(x => x != null) - .ToArray(); - - oldLen = _plugins.Length; - Array.Resize(ref _plugins, oldLen + plugins.Length); - plugins.CopyTo(_plugins, oldLen); - - var entries = types.Where(x => x.IsAssignableFrom(typeof(IServerEntryPoint))) - .Select(CreateInstanceSafe) - .Where(x => x != null) - .Cast<IServerEntryPoint>() - .ToList(); - - await Task.WhenAll(StartEntryPoints(entries, true)).ConfigureAwait(false); - await Task.WhenAll(StartEntryPoints(entries, false)).ConfigureAwait(false); - } - /// <summary> /// Finds the parts. /// </summary> public void FindParts() { InstallationManager = ServiceProvider.GetService<IInstallationManager>(); - InstallationManager.PluginInstalled += PluginInstalled; if (!ServerConfigurationManager.Configuration.IsPortAuthorized) { @@ -1061,7 +1034,7 @@ namespace Emby.Server.Implementations .Where(i => i != null) .ToArray(); - HttpServer.Init(GetExports<IService>(false), GetExports<IWebSocketListener>(), GetUrlPrefixes()); + HttpServer.Init(GetExportTypes<IService>(), GetExports<IWebSocketListener>(), GetUrlPrefixes()); LibraryManager.AddParts( GetExports<IResolverIgnoreRule>(), @@ -1155,7 +1128,7 @@ namespace Emby.Server.Implementations { exportedTypes = ass.GetExportedTypes(); } - catch (TypeLoadException ex) + catch (FileNotFoundException ex) { Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName); continue; @@ -1195,8 +1168,6 @@ namespace Emby.Server.Implementations }); } - protected IHttpListener CreateHttpListener() => new WebSocketSharpListener(LoggerFactory.CreateLogger<WebSocketSharpListener>()); - private CertificateInfo GetCertificateInfo(bool generateCertificate) { // Custom cert @@ -1507,18 +1478,10 @@ namespace Emby.Server.Implementations public string GetLocalApiUrl(ReadOnlySpan<char> host) { var url = new StringBuilder(64); - if (EnableHttps) - { - url.Append("https://"); - } - else - { - url.Append("http://"); - } - - url.Append(host) + url.Append(EnableHttps ? "https://" : "http://") + .Append(host) .Append(':') - .Append(HttpPort); + .Append(EnableHttps ? HttpsPort : HttpPort); string baseUrl = ServerConfigurationManager.Configuration.BaseUrl; if (baseUrl.Length != 0) @@ -1715,15 +1678,17 @@ namespace Emby.Server.Implementations throw new NotSupportedException(); } - var process = ProcessFactory.Create(new ProcessOptions + var process = new Process { - FileName = url, - EnableRaisingEvents = true, - UseShellExecute = true, - ErrorDialog = false - }); - - process.Exited += ProcessExited; + StartInfo = new ProcessStartInfo + { + FileName = url, + UseShellExecute = true, + ErrorDialog = false + }, + EnableRaisingEvents = true + }; + process.Exited += (sender, args) => ((Process)sender).Dispose(); try { @@ -1736,11 +1701,6 @@ namespace Emby.Server.Implementations } } - private static void ProcessExited(object sender, EventArgs e) - { - ((IProcess)sender).Dispose(); - } - public virtual void EnableLoopback(string appName) { } @@ -1791,7 +1751,7 @@ namespace Emby.Server.Implementations } _userRepository?.Dispose(); - _displayPreferencesRepository.Dispose(); + _displayPreferencesRepository?.Dispose(); } _userRepository = null; diff --git a/Emby.Server.Implementations/Archiving/ZipClient.cs b/Emby.Server.Implementations/Archiving/ZipClient.cs index 4a6e5cfd7..e01495e19 100644 --- a/Emby.Server.Implementations/Archiving/ZipClient.cs +++ b/Emby.Server.Implementations/Archiving/ZipClient.cs @@ -50,6 +50,7 @@ namespace Emby.Server.Implementations.Archiving } } + /// <inheritdoc /> public void ExtractAllFromZip(Stream source, string targetPath, bool overwriteExistingFiles) { using (var reader = ZipReader.Open(source)) @@ -66,6 +67,7 @@ namespace Emby.Server.Implementations.Archiving } } + /// <inheritdoc /> public void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles) { using (var reader = GZipReader.Open(source)) @@ -82,6 +84,7 @@ namespace Emby.Server.Implementations.Archiving } } + /// <inheritdoc /> public void ExtractFirstFileFromGz(Stream source, string targetPath, string defaultFileName) { using (var reader = GZipReader.Open(source)) diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs index f5da0d018..96096e142 100644 --- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs +++ b/Emby.Server.Implementations/Browser/BrowserLauncher.cs @@ -1,51 +1,48 @@ using System; using MediaBrowser.Controller; +using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Browser { /// <summary> - /// Class BrowserLauncher. + /// Assists in opening application URLs in an external browser. /// </summary> public static class BrowserLauncher { /// <summary> - /// Opens the dashboard page. + /// Opens the home page of the web client. /// </summary> - /// <param name="page">The page.</param> /// <param name="appHost">The app host.</param> - private static void OpenDashboardPage(string page, IServerApplicationHost appHost) + public static void OpenWebApp(IServerApplicationHost appHost) { - var url = appHost.GetLocalApiUrl("localhost") + "/web/" + page; - - OpenUrl(appHost, url); + TryOpenUrl(appHost, "/web/index.html"); } /// <summary> - /// Opens the web client. + /// Opens the swagger API page. /// </summary> /// <param name="appHost">The app host.</param> - public static void OpenWebApp(IServerApplicationHost appHost) + public static void OpenSwaggerPage(IServerApplicationHost appHost) { - OpenDashboardPage("index.html", appHost); + TryOpenUrl(appHost, "/swagger/index.html"); } /// <summary> - /// Opens the URL. + /// Opens the specified URL in an external browser window. Any exceptions will be logged, but ignored. /// </summary> - /// <param name="appHost">The application host instance.</param> + /// <param name="appHost">The application host.</param> /// <param name="url">The URL.</param> - private static void OpenUrl(IServerApplicationHost appHost, string url) + private static void TryOpenUrl(IServerApplicationHost appHost, string url) { try { - appHost.LaunchUrl(url); - } - catch (NotSupportedException) - { - + string baseUrl = appHost.GetLocalApiUrl("localhost"); + appHost.LaunchUrl(baseUrl + url); } - catch (Exception) + catch (Exception ex) { + var logger = appHost.Resolve<ILogger>(); + logger?.LogError(ex, "Failed to open browser window with URL {URL}", url); } } } diff --git a/Emby.Server.Implementations/Channels/ChannelImageProvider.cs b/Emby.Server.Implementations/Channels/ChannelImageProvider.cs index 62aeb9bcb..cf56a5fae 100644 --- a/Emby.Server.Implementations/Channels/ChannelImageProvider.cs +++ b/Emby.Server.Implementations/Channels/ChannelImageProvider.cs @@ -20,6 +20,8 @@ namespace Emby.Server.Implementations.Channels _channelManager = channelManager; } + public string Name => "Channel Image Provider"; + public IEnumerable<ImageType> GetSupportedImages(BaseItem item) { return GetChannel(item).GetSupportedChannelImages(); @@ -32,8 +34,6 @@ namespace Emby.Server.Implementations.Channels return channel.GetChannelImage(type, cancellationToken); } - public string Name => "Channel Image Provider"; - public bool Supports(BaseItem item) { return item is Channel; diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 6e1baddfe..8502af97a 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -31,8 +31,6 @@ namespace Emby.Server.Implementations.Channels { public class ChannelManager : IChannelManager { - internal IChannel[] Channels { get; private set; } - private readonly IUserManager _userManager; private readonly IUserDataManager _userDataManager; private readonly IDtoService _dtoService; @@ -43,11 +41,16 @@ namespace Emby.Server.Implementations.Channels private readonly IJsonSerializer _jsonSerializer; private readonly IProviderManager _providerManager; + private readonly ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>> _channelItemMediaInfo = + new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>(); + + private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1); + public ChannelManager( IUserManager userManager, IDtoService dtoService, ILibraryManager libraryManager, - ILoggerFactory loggerFactory, + ILogger<ChannelManager> logger, IServerConfigurationManager config, IFileSystem fileSystem, IUserDataManager userDataManager, @@ -57,7 +60,7 @@ namespace Emby.Server.Implementations.Channels _userManager = userManager; _dtoService = dtoService; _libraryManager = libraryManager; - _logger = loggerFactory.CreateLogger(nameof(ChannelManager)); + _logger = logger; _config = config; _fileSystem = fileSystem; _userDataManager = userDataManager; @@ -65,6 +68,8 @@ namespace Emby.Server.Implementations.Channels _providerManager = providerManager; } + internal IChannel[] Channels { get; private set; } + private static TimeSpan CacheLength => TimeSpan.FromHours(3); public void AddParts(IEnumerable<IChannel> channels) @@ -85,8 +90,7 @@ namespace Emby.Server.Implementations.Channels var internalChannel = _libraryManager.GetItemById(item.ChannelId); var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id)); - var supportsDelete = channel as ISupportsDelete; - return supportsDelete != null && supportsDelete.CanDelete(item); + return channel is ISupportsDelete supportsDelete && supportsDelete.CanDelete(item); } public bool EnableMediaProbe(BaseItem item) @@ -146,15 +150,13 @@ namespace Emby.Server.Implementations.Channels { try { - var hasAttributes = GetChannelProvider(i) as IHasFolderAttributes; - - return (hasAttributes != null && hasAttributes.Attributes.Contains("Recordings", StringComparer.OrdinalIgnoreCase)) == val; + return (GetChannelProvider(i) is IHasFolderAttributes hasAttributes + && hasAttributes.Attributes.Contains("Recordings", StringComparer.OrdinalIgnoreCase)) == val; } catch { return false; } - }).ToList(); } @@ -171,7 +173,6 @@ namespace Emby.Server.Implementations.Channels { return false; } - }).ToList(); } @@ -188,9 +189,9 @@ namespace Emby.Server.Implementations.Channels { return false; } - }).ToList(); } + if (query.IsFavorite.HasValue) { var val = query.IsFavorite.Value; @@ -215,7 +216,6 @@ namespace Emby.Server.Implementations.Channels { return false; } - }).ToList(); } @@ -226,6 +226,7 @@ namespace Emby.Server.Implementations.Channels { all = all.Skip(query.StartIndex.Value).ToList(); } + if (query.Limit.HasValue) { all = all.Take(query.Limit.Value).ToList(); @@ -256,11 +257,9 @@ namespace Emby.Server.Implementations.Channels var internalResult = GetChannelsInternal(query); - var dtoOptions = new DtoOptions() - { - }; + var dtoOptions = new DtoOptions(); - //TODO Fix The co-variant conversion (internalResult.Items) between Folder[] and BaseItem[], this can generate runtime issues. + // TODO Fix The co-variant conversion (internalResult.Items) between Folder[] and BaseItem[], this can generate runtime issues. var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user); var result = new QueryResult<BaseItemDto> @@ -341,8 +340,8 @@ namespace Emby.Server.Implementations.Channels } catch { - } + return; } @@ -365,11 +364,9 @@ namespace Emby.Server.Implementations.Channels var channel = GetChannel(item.ChannelId); var channelPlugin = GetChannelProvider(channel); - var requiresCallback = channelPlugin as IRequiresMediaInfoCallback; - IEnumerable<MediaSourceInfo> results; - if (requiresCallback != null) + if (channelPlugin is IRequiresMediaInfoCallback requiresCallback) { results = await GetChannelItemMediaSourcesInternal(requiresCallback, item.ExternalId, cancellationToken) .ConfigureAwait(false); @@ -384,9 +381,6 @@ namespace Emby.Server.Implementations.Channels .ToList(); } - private readonly ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>> _channelItemMediaInfo = - new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>(); - private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken) { if (_channelItemMediaInfo.TryGetValue(id, out Tuple<DateTime, List<MediaSourceInfo>> cachedInfo)) @@ -444,18 +438,21 @@ namespace Emby.Server.Implementations.Channels { isNew = true; } + item.Path = path; if (!item.ChannelId.Equals(id)) { forceUpdate = true; } + item.ChannelId = id; if (item.ParentId != parentFolderId) { forceUpdate = true; } + item.ParentId = parentFolderId; item.OfficialRating = GetOfficialRating(channelInfo.ParentalRating); @@ -472,10 +469,12 @@ namespace Emby.Server.Implementations.Channels _libraryManager.CreateItem(item, null); } - await item.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) - { - ForceSave = !isNew && forceUpdate - }, cancellationToken).ConfigureAwait(false); + await item.RefreshMetadata( + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + ForceSave = !isNew && forceUpdate + }, + cancellationToken).ConfigureAwait(false); return item; } @@ -509,12 +508,12 @@ namespace Emby.Server.Implementations.Channels public ChannelFeatures[] GetAllChannelFeatures() { - return _libraryManager.GetItemIds(new InternalItemsQuery - { - IncludeItemTypes = new[] { typeof(Channel).Name }, - OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) } - - }).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray(); + return _libraryManager.GetItemIds( + new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Channel).Name }, + OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) } + }).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray(); } public ChannelFeatures GetChannelFeatures(string id) @@ -532,13 +531,13 @@ namespace Emby.Server.Implementations.Channels public bool SupportsExternalTransfer(Guid channelId) { - //var channel = GetChannel(channelId); var channelProvider = GetChannelProvider(channelId); return channelProvider.GetChannelFeatures().SupportsContentDownloading; } - public ChannelFeatures GetChannelFeaturesDto(Channel channel, + public ChannelFeatures GetChannelFeaturesDto( + Channel channel, IChannel provider, InternalChannelFeatures features) { @@ -567,6 +566,7 @@ namespace Emby.Server.Implementations.Channels { throw new ArgumentNullException(nameof(name)); } + return _libraryManager.GetNewItemId("Channel " + name, typeof(Channel)); } @@ -614,7 +614,7 @@ namespace Emby.Server.Implementations.Channels query.IsFolder = false; // hack for trailers, figure out a better way later - var sortByPremiereDate = channels.Length == 1 && channels[0].GetType().Name.IndexOf("Trailer") != -1; + var sortByPremiereDate = channels.Length == 1 && channels[0].GetType().Name.Contains("Trailer", StringComparison.Ordinal); if (sortByPremiereDate) { @@ -640,10 +640,12 @@ namespace Emby.Server.Implementations.Channels { var internalChannel = await GetChannel(channel, cancellationToken).ConfigureAwait(false); - var query = new InternalItemsQuery(); - query.Parent = internalChannel; - query.EnableTotalRecordCount = false; - query.ChannelIds = new Guid[] { internalChannel.Id }; + var query = new InternalItemsQuery + { + Parent = internalChannel, + EnableTotalRecordCount = false, + ChannelIds = new Guid[] { internalChannel.Id } + }; var result = await GetChannelItemsInternal(query, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false); @@ -651,13 +653,15 @@ namespace Emby.Server.Implementations.Channels { if (item is Folder folder) { - await GetChannelItemsInternal(new InternalItemsQuery - { - Parent = folder, - EnableTotalRecordCount = false, - ChannelIds = new Guid[] { internalChannel.Id } - - }, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false); + await GetChannelItemsInternal( + new InternalItemsQuery + { + Parent = folder, + EnableTotalRecordCount = false, + ChannelIds = new Guid[] { internalChannel.Id } + }, + new SimpleProgress<double>(), + cancellationToken).ConfigureAwait(false); } } } @@ -672,7 +676,8 @@ namespace Emby.Server.Implementations.Channels var parentItem = query.ParentId == Guid.Empty ? channel : _libraryManager.GetItemById(query.ParentId); - var itemsResult = await GetChannelItems(channelProvider, + var itemsResult = await GetChannelItems( + channelProvider, query.User, parentItem is Channel ? null : parentItem.ExternalId, null, @@ -684,13 +689,12 @@ namespace Emby.Server.Implementations.Channels { query.Parent = channel; } + query.ChannelIds = Array.Empty<Guid>(); // Not yet sure why this is causing a problem query.GroupByPresentationUniqueKey = false; - //_logger.LogDebug("GetChannelItemsInternal"); - // null if came from cache if (itemsResult != null) { @@ -707,12 +711,15 @@ namespace Emby.Server.Implementations.Channels var deadItem = _libraryManager.GetItemById(deadId); if (deadItem != null) { - _libraryManager.DeleteItem(deadItem, new DeleteOptions - { - DeleteFileLocation = false, - DeleteFromExternalProvider = false - - }, parentItem, false); + _libraryManager.DeleteItem( + deadItem, + new DeleteOptions + { + DeleteFileLocation = false, + DeleteFromExternalProvider = false + }, + parentItem, + false); } } } @@ -735,7 +742,6 @@ namespace Emby.Server.Implementations.Channels return result; } - private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1); private async Task<ChannelItemResult> GetChannelItems(IChannel channel, User user, string externalFolderId, @@ -743,7 +749,7 @@ namespace Emby.Server.Implementations.Channels bool sortDescending, CancellationToken cancellationToken) { - var userId = user == null ? null : user.Id.ToString("N", CultureInfo.InvariantCulture); + var userId = user?.Id.ToString("N", CultureInfo.InvariantCulture); var cacheLength = CacheLength; var cachePath = GetChannelDataCachePath(channel, userId, externalFolderId, sortField, sortDescending); @@ -761,11 +767,9 @@ namespace Emby.Server.Implementations.Channels } catch (FileNotFoundException) { - } catch (IOException) { - } await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); @@ -785,11 +789,9 @@ namespace Emby.Server.Implementations.Channels } catch (FileNotFoundException) { - } catch (IOException) { - } var query = new InternalChannelItemQuery @@ -833,7 +835,8 @@ namespace Emby.Server.Implementations.Channels } } - private string GetChannelDataCachePath(IChannel channel, + private string GetChannelDataCachePath( + IChannel channel, string userId, string externalFolderId, ChannelItemSortField? sortField, @@ -843,8 +846,7 @@ namespace Emby.Server.Implementations.Channels var userCacheKey = string.Empty; - var hasCacheKey = channel as IHasCacheKey; - if (hasCacheKey != null) + if (channel is IHasCacheKey hasCacheKey) { userCacheKey = hasCacheKey.GetCacheKey(userId) ?? string.Empty; } @@ -858,6 +860,7 @@ namespace Emby.Server.Implementations.Channels { filename += "-sortField-" + sortField.Value; } + if (sortDescending) { filename += "-sortDescending"; @@ -865,7 +868,8 @@ namespace Emby.Server.Implementations.Channels filename = filename.GetMD5().ToString("N", CultureInfo.InvariantCulture); - return Path.Combine(_config.ApplicationPaths.CachePath, + return Path.Combine( + _config.ApplicationPaths.CachePath, "channels", channelId, version, @@ -981,7 +985,6 @@ namespace Emby.Server.Implementations.Channels { item.RunTimeTicks = null; } - else if (isNew || !enableMediaProbe) { item.RunTimeTicks = info.RunTimeTicks; @@ -1014,26 +1017,24 @@ namespace Emby.Server.Implementations.Channels } } - var hasArtists = item as IHasArtist; - if (hasArtists != null) + if (item is IHasArtist hasArtists) { hasArtists.Artists = info.Artists.ToArray(); } - var hasAlbumArtists = item as IHasAlbumArtist; - if (hasAlbumArtists != null) + if (item is IHasAlbumArtist hasAlbumArtists) { hasAlbumArtists.AlbumArtists = info.AlbumArtists.ToArray(); } - var trailer = item as Trailer; - if (trailer != null) + if (item is Trailer trailer) { if (!info.TrailerTypes.SequenceEqual(trailer.TrailerTypes)) { _logger.LogDebug("Forcing update due to TrailerTypes {0}", item.Name); forceUpdate = true; } + trailer.TrailerTypes = info.TrailerTypes.ToArray(); } @@ -1057,6 +1058,7 @@ namespace Emby.Server.Implementations.Channels forceUpdate = true; _logger.LogDebug("Forcing update due to ChannelId {0}", item.Name); } + item.ChannelId = internalChannelId; if (!item.ParentId.Equals(parentFolderId)) @@ -1064,16 +1066,17 @@ namespace Emby.Server.Implementations.Channels forceUpdate = true; _logger.LogDebug("Forcing update due to parent folder Id {0}", item.Name); } + item.ParentId = parentFolderId; - var hasSeries = item as IHasSeries; - if (hasSeries != null) + if (item is IHasSeries hasSeries) { if (!string.Equals(hasSeries.SeriesName, info.SeriesName, StringComparison.OrdinalIgnoreCase)) { forceUpdate = true; _logger.LogDebug("Forcing update due to SeriesName {0}", item.Name); } + hasSeries.SeriesName = info.SeriesName; } @@ -1082,24 +1085,23 @@ namespace Emby.Server.Implementations.Channels forceUpdate = true; _logger.LogDebug("Forcing update due to ExternalId {0}", item.Name); } + item.ExternalId = info.Id; - var channelAudioItem = item as Audio; - if (channelAudioItem != null) + if (item is Audio channelAudioItem) { channelAudioItem.ExtraType = info.ExtraType; var mediaSource = info.MediaSources.FirstOrDefault(); - item.Path = mediaSource == null ? null : mediaSource.Path; + item.Path = mediaSource?.Path; } - var channelVideoItem = item as Video; - if (channelVideoItem != null) + if (item is Video channelVideoItem) { channelVideoItem.ExtraType = info.ExtraType; var mediaSource = info.MediaSources.FirstOrDefault(); - item.Path = mediaSource == null ? null : mediaSource.Path; + item.Path = mediaSource?.Path; } if (!string.IsNullOrEmpty(info.ImageUrl) && !item.HasImage(ImageType.Primary)) @@ -1156,7 +1158,7 @@ namespace Emby.Server.Implementations.Channels } } - if (isNew || forceUpdate || item.DateLastRefreshed == default(DateTime)) + if (isNew || forceUpdate || item.DateLastRefreshed == default) { _providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal); } diff --git a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs index 266d539d0..250e9da64 100644 --- a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs +++ b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs @@ -14,14 +14,12 @@ namespace Emby.Server.Implementations.Channels public class ChannelPostScanTask { private readonly IChannelManager _channelManager; - private readonly IUserManager _userManager; private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; - public ChannelPostScanTask(IChannelManager channelManager, IUserManager userManager, ILogger logger, ILibraryManager libraryManager) + public ChannelPostScanTask(IChannelManager channelManager, ILogger logger, ILibraryManager libraryManager) { _channelManager = channelManager; - _userManager = userManager; _logger = logger; _libraryManager = libraryManager; } diff --git a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs index 891b81a36..e1d35f2b0 100644 --- a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs +++ b/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; @@ -15,30 +16,30 @@ namespace Emby.Server.Implementations.Channels public class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask { private readonly IChannelManager _channelManager; - private readonly IUserManager _userManager; private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; + private readonly ILocalizationManager _localization; public RefreshChannelsScheduledTask( IChannelManager channelManager, - IUserManager userManager, ILogger<RefreshChannelsScheduledTask> logger, - ILibraryManager libraryManager) + ILibraryManager libraryManager, + ILocalizationManager localization) { _channelManager = channelManager; - _userManager = userManager; _logger = logger; _libraryManager = libraryManager; + _localization = localization; } /// <inheritdoc /> - public string Name => "Refresh Channels"; + public string Name => _localization.GetLocalizedString("TasksRefreshChannels"); /// <inheritdoc /> - public string Description => "Refreshes internet channel information."; + public string Description => _localization.GetLocalizedString("TasksRefreshChannelsDescription"); /// <inheritdoc /> - public string Category => "Internet Channels"; + public string Category => _localization.GetLocalizedString("TasksChannelsCategory"); /// <inheritdoc /> public bool IsHidden => ((ChannelManager)_channelManager).Channels.Length == 0; @@ -59,7 +60,7 @@ namespace Emby.Server.Implementations.Channels await manager.RefreshChannels(new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false); - await new ChannelPostScanTask(_channelManager, _userManager, _logger, _libraryManager).Run(progress, cancellationToken) + await new ChannelPostScanTask(_channelManager, _logger, _libraryManager).Run(progress, cancellationToken) .ConfigureAwait(false); } @@ -68,7 +69,6 @@ namespace Emby.Server.Implementations.Channels { return new[] { - // Every so often new TaskTriggerInfo { diff --git a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs index 21ba0288e..320552465 100644 --- a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs +++ b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs @@ -46,9 +46,7 @@ namespace Emby.Server.Implementations.Collections { var subItem = i; - var episode = subItem as Episode; - - if (episode != null) + if (subItem is Episode episode) { var series = episode.Series; if (series != null && series.HasImage(ImageType.Primary)) diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index 321952874..68bcc5a4f 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -52,7 +52,9 @@ namespace Emby.Server.Implementations.Collections } public event EventHandler<CollectionCreatedEventArgs> CollectionCreated; + public event EventHandler<CollectionModifiedEventArgs> ItemsAddedToCollection; + public event EventHandler<CollectionModifiedEventArgs> ItemsRemovedFromCollection; private IEnumerable<Folder> FindFolders(string path) @@ -109,9 +111,9 @@ namespace Emby.Server.Implementations.Collections { var folder = GetCollectionsFolder(false).Result; - return folder == null ? - new List<BoxSet>() : - folder.GetChildren(user, true).OfType<BoxSet>(); + return folder == null + ? Enumerable.Empty<BoxSet>() + : folder.GetChildren(user, true).OfType<BoxSet>(); } public BoxSet CreateCollection(CollectionCreationOptions options) @@ -191,7 +193,6 @@ namespace Emby.Server.Implementations.Collections private void AddToCollection(Guid collectionId, IEnumerable<string> ids, bool fireEvent, MetadataRefreshOptions refreshOptions) { var collection = _libraryManager.GetItemById(collectionId) as BoxSet; - if (collection == null) { throw new ArgumentException("No collection exists with the supplied Id"); @@ -289,10 +290,13 @@ namespace Emby.Server.Implementations.Collections } collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); - _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) - { - ForceSave = true - }, RefreshPriority.High); + _providerManager.QueueRefresh( + collection.Id, + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + ForceSave = true + }, + RefreshPriority.High); ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs { diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs index 30b654886..f407317ec 100644 --- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -132,7 +132,7 @@ namespace Emby.Server.Implementations.Configuration var newPath = newConfig.MetadataPath; if (!string.IsNullOrWhiteSpace(newPath) - && !string.Equals(Configuration.MetadataPath, newPath, StringComparison.Ordinal)) + && !string.Equals(Configuration.MetadataPath, newPath, StringComparison.Ordinal)) { // Validate if (!Directory.Exists(newPath)) diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index 31fb5ca58..20bdd18e7 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -1,13 +1,24 @@ using System.Collections.Generic; +using Emby.Server.Implementations.HttpServer; +using Emby.Server.Implementations.Updates; +using MediaBrowser.Providers.Music; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; namespace Emby.Server.Implementations { + /// <summary> + /// Static class containing the default configuration options for the web server. + /// </summary> public static class ConfigurationOptions { - public static Dictionary<string, string> Configuration => new Dictionary<string, string> + /// <summary> + /// Gets a new copy of the default configuration options. + /// </summary> + public static Dictionary<string, string> DefaultConfiguration => new Dictionary<string, string> { - { "HttpListenerHost:DefaultRedirectPath", "web/index.html" }, + { HostWebClientKey, bool.TrueString }, + { HttpListenerHost.DefaultRedirectKey, "web/index.html" }, + { InstallationManager.PluginManifestUrlKey, "https://repo.jellyfin.org/releases/plugin/manifest.json" }, { FfmpegProbeSizeKey, "1G" }, { FfmpegAnalyzeDurationKey, "200M" }, { PlaylistsAllowDuplicatesKey, bool.TrueString } diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs index c87793072..716e5071d 100644 --- a/Emby.Server.Implementations/Data/SqliteExtensions.cs +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -3,8 +3,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; -using MediaBrowser.Model.Serialization; using SQLitePCL.pretty; namespace Emby.Server.Implementations.Data @@ -109,25 +107,6 @@ namespace Emby.Server.Implementations.Data return null; } - /// <summary> - /// Serializes to bytes. - /// </summary> - /// <returns>System.Byte[][].</returns> - /// <exception cref="ArgumentNullException">obj</exception> - public static byte[] SerializeToBytes(this IJsonSerializer json, object obj) - { - if (obj == null) - { - throw new ArgumentNullException(nameof(obj)); - } - - using (var stream = new MemoryStream()) - { - json.SerializeToStream(obj, stream); - return stream.ToArray(); - } - } - public static void Attach(SQLiteDatabaseConnection db, string path, string alias) { var commandText = string.Format( @@ -287,7 +266,7 @@ namespace Emby.Server.Implementations.Data } } - public static void TryBind(this IStatement statement, string name, byte[] value) + public static void TryBind(this IStatement statement, string name, ReadOnlySpan<byte> value) { if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) { @@ -383,11 +362,11 @@ namespace Emby.Server.Implementations.Data } } - public static IEnumerable<IReadOnlyList<IResultSetValue>> ExecuteQuery(this IStatement This) + public static IEnumerable<IReadOnlyList<IResultSetValue>> ExecuteQuery(this IStatement statement) { - while (This.MoveNext()) + while (statement.MoveNext()) { - yield return This.Current; + yield return statement.Current; } } } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 44f38504a..33ff74bb5 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -454,7 +454,7 @@ namespace Emby.Server.Implementations.Data private static string GetSaveItemCommandText() { - var saveColumns = new [] + var saveColumns = new[] { "guid", "type", @@ -560,7 +560,7 @@ namespace Emby.Server.Implementations.Data throw new ArgumentNullException(nameof(item)); } - SaveItems(new [] { item }, cancellationToken); + SaveItems(new[] { item }, cancellationToken); } public void SaveImages(BaseItem item) @@ -1622,7 +1622,7 @@ namespace Emby.Server.Implementations.Data { IEnumerable<MetadataFields> GetLockedFields(string s) { - foreach (var i in s.Split(new [] { '|' }, StringSplitOptions.RemoveEmptyEntries)) + foreach (var i in s.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries)) { if (Enum.TryParse(i, true, out MetadataFields parsedValue)) { @@ -1818,7 +1818,7 @@ namespace Emby.Server.Implementations.Data { if (!reader.IsDBNull(index)) { - item.ProductionLocations = reader.GetString(index).Split(new [] { '|' }, StringSplitOptions.RemoveEmptyEntries).ToArray(); + item.ProductionLocations = reader.GetString(index).Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries).ToArray(); } index++; } @@ -2006,7 +2006,7 @@ namespace Emby.Server.Implementations.Data /// <summary> /// Saves the chapters. /// </summary> - public void SaveChapters(Guid id, List<ChapterInfo> chapters) + public void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> chapters) { CheckDisposed(); @@ -2035,22 +2035,24 @@ namespace Emby.Server.Implementations.Data } } - private void InsertChapters(byte[] idBlob, List<ChapterInfo> chapters, IDatabaseConnection db) + private void InsertChapters(byte[] idBlob, IReadOnlyList<ChapterInfo> chapters, IDatabaseConnection db) { var startIndex = 0; var limit = 100; var chapterIndex = 0; + const string StartInsertText = "insert into " + ChaptersTableName + " (ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath, ImageDateModified) values "; + var insertText = new StringBuilder(StartInsertText, 256); + while (startIndex < chapters.Count) { - var insertText = new StringBuilder("insert into " + ChaptersTableName + " (ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath, ImageDateModified) values "); - var endIndex = Math.Min(chapters.Count, startIndex + limit); for (var i = startIndex; i < endIndex; i++) { insertText.AppendFormat("(@ItemId, @ChapterIndex{0}, @StartPositionTicks{0}, @Name{0}, @ImagePath{0}, @ImageDateModified{0}),", i.ToString(CultureInfo.InvariantCulture)); } + insertText.Length -= 1; // Remove last , using (var statement = PrepareStatement(db, insertText.ToString())) @@ -2077,6 +2079,7 @@ namespace Emby.Server.Implementations.Data } startIndex += limit; + insertText.Length = StartInsertText.Length; } } @@ -2897,8 +2900,8 @@ namespace Emby.Server.Implementations.Data BindSimilarParams(query, statement); BindSearchParams(query, statement); - // Running this again will bind the params - GetWhereClauses(query, statement); + // Running this again will bind the params + GetWhereClauses(query, statement); result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); } @@ -2914,29 +2917,30 @@ namespace Emby.Server.Implementations.Data private string GetOrderByText(InternalItemsQuery query) { var orderBy = query.OrderBy; - if (string.IsNullOrEmpty(query.SearchTerm)) + bool hasSimilar = query.SimilarTo != null; + bool hasSearch = !string.IsNullOrEmpty(query.SearchTerm); + + if (hasSimilar || hasSearch) { - int oldLen = orderBy.Count; - if (oldLen == 0 && query.SimilarTo != null) + List<(string, SortOrder)> prepend = new List<(string, SortOrder)>(4); + if (hasSearch) { - var arr = new (string, SortOrder)[oldLen + 2]; - orderBy.CopyTo(arr, 0); - arr[oldLen] = ("SimilarityScore", SortOrder.Descending); - arr[oldLen + 1] = (ItemSortBy.Random, SortOrder.Ascending); - query.OrderBy = arr; + prepend.Add(("SearchScore", SortOrder.Descending)); + prepend.Add((ItemSortBy.SortName, SortOrder.Ascending)); } - } - else - { - query.OrderBy = new[] - { - ("SearchScore", SortOrder.Descending), - (ItemSortBy.SortName, SortOrder.Ascending) - }; - } + if (hasSimilar) + { + prepend.Add(("SimilarityScore", SortOrder.Descending)); + prepend.Add((ItemSortBy.Random, SortOrder.Ascending)); + } - if (orderBy.Count == 0) + var arr = new (string, SortOrder)[prepend.Count + orderBy.Count]; + prepend.CopyTo(arr, 0); + orderBy.CopyTo(arr, prepend.Count); + orderBy = query.OrderBy = arr; + } + else if (orderBy.Count == 0) { return string.Empty; } @@ -3265,8 +3269,8 @@ namespace Emby.Server.Implementations.Data BindSimilarParams(query, statement); BindSearchParams(query, statement); - // Running this again will bind the params - GetWhereClauses(query, statement); + // Running this again will bind the params + GetWhereClauses(query, statement); foreach (var row in statement.ExecuteQuery()) { @@ -3287,8 +3291,8 @@ namespace Emby.Server.Implementations.Data BindSimilarParams(query, statement); BindSearchParams(query, statement); - // Running this again will bind the params - GetWhereClauses(query, statement); + // Running this again will bind the params + GetWhereClauses(query, statement); result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); } @@ -3311,7 +3315,7 @@ namespace Emby.Server.Implementations.Data for (int i = 0; i < str.Length; i++) { - if (!(char.IsLetter(str[i])) && (!(char.IsNumber(str[i])))) + if (!char.IsLetter(str[i]) && !char.IsNumber(str[i])) { return false; } @@ -3335,7 +3339,7 @@ namespace Emby.Server.Implementations.Data return IsAlphaNumeric(value); } - private List<string> GetWhereClauses(InternalItemsQuery query, IStatement statement, string paramSuffix = "") + private List<string> GetWhereClauses(InternalItemsQuery query, IStatement statement) { if (query.IsResumable ?? false) { @@ -3347,27 +3351,27 @@ namespace Emby.Server.Implementations.Data if (query.IsHD.HasValue) { - var threshold = 1200; + const int Threshold = 1200; if (query.IsHD.Value) { - minWidth = threshold; + minWidth = Threshold; } else { - maxWidth = threshold - 1; + maxWidth = Threshold - 1; } } if (query.Is4K.HasValue) { - var threshold = 3800; + const int Threshold = 3800; if (query.Is4K.Value) { - minWidth = threshold; + minWidth = Threshold; } else { - maxWidth = threshold - 1; + maxWidth = Threshold - 1; } } @@ -3376,93 +3380,61 @@ namespace Emby.Server.Implementations.Data if (minWidth.HasValue) { whereClauses.Add("Width>=@MinWidth"); - if (statement != null) - { - statement.TryBind("@MinWidth", minWidth); - } + statement?.TryBind("@MinWidth", minWidth); } + if (query.MinHeight.HasValue) { whereClauses.Add("Height>=@MinHeight"); - if (statement != null) - { - statement.TryBind("@MinHeight", query.MinHeight); - } + statement?.TryBind("@MinHeight", query.MinHeight); } + if (maxWidth.HasValue) { whereClauses.Add("Width<=@MaxWidth"); - if (statement != null) - { - statement.TryBind("@MaxWidth", maxWidth); - } + statement?.TryBind("@MaxWidth", maxWidth); } + if (query.MaxHeight.HasValue) { whereClauses.Add("Height<=@MaxHeight"); - if (statement != null) - { - statement.TryBind("@MaxHeight", query.MaxHeight); - } + statement?.TryBind("@MaxHeight", query.MaxHeight); } if (query.IsLocked.HasValue) { whereClauses.Add("IsLocked=@IsLocked"); - if (statement != null) - { - statement.TryBind("@IsLocked", query.IsLocked); - } + statement?.TryBind("@IsLocked", query.IsLocked); } var tags = query.Tags.ToList(); var excludeTags = query.ExcludeTags.ToList(); - if (query.IsMovie ?? false) + if (query.IsMovie == true) { - var alternateTypes = new List<string>(); - if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Movie).Name)) - { - alternateTypes.Add(typeof(Movie).FullName); - } - if (query.IncludeItemTypes.Length == 0 || query.IncludeItemTypes.Contains(typeof(Trailer).Name)) + if (query.IncludeItemTypes.Length == 0 + || query.IncludeItemTypes.Contains(nameof(Movie)) + || query.IncludeItemTypes.Contains(nameof(Trailer))) { - alternateTypes.Add(typeof(Trailer).FullName); - } - - var programAttribtues = new List<string>(); - if (alternateTypes.Count == 0) - { - programAttribtues.Add("IsMovie=@IsMovie"); + whereClauses.Add("(IsMovie is null OR IsMovie=@IsMovie)"); } else { - programAttribtues.Add("(IsMovie is null OR IsMovie=@IsMovie)"); - } - - if (statement != null) - { - statement.TryBind("@IsMovie", true); + whereClauses.Add("IsMovie=@IsMovie"); } - whereClauses.Add("(" + string.Join(" OR ", programAttribtues) + ")"); + statement?.TryBind("@IsMovie", true); } else if (query.IsMovie.HasValue) { whereClauses.Add("IsMovie=@IsMovie"); - if (statement != null) - { - statement.TryBind("@IsMovie", query.IsMovie); - } + statement?.TryBind("@IsMovie", query.IsMovie); } if (query.IsSeries.HasValue) { whereClauses.Add("IsSeries=@IsSeries"); - if (statement != null) - { - statement.TryBind("@IsSeries", query.IsSeries); - } + statement?.TryBind("@IsSeries", query.IsSeries); } if (query.IsSports.HasValue) @@ -3514,10 +3486,7 @@ namespace Emby.Server.Implementations.Data if (query.IsFolder.HasValue) { whereClauses.Add("IsFolder=@IsFolder"); - if (statement != null) - { - statement.TryBind("@IsFolder", query.IsFolder); - } + statement?.TryBind("@IsFolder", query.IsFolder); } var includeTypes = query.IncludeItemTypes.SelectMany(MapIncludeItemTypes).ToArray(); @@ -3528,10 +3497,7 @@ namespace Emby.Server.Implementations.Data if (excludeTypes.Length == 1) { whereClauses.Add("type<>@type"); - if (statement != null) - { - statement.TryBind("@type", excludeTypes[0]); - } + statement?.TryBind("@type", excludeTypes[0]); } else if (excludeTypes.Length > 1) { @@ -3542,10 +3508,7 @@ namespace Emby.Server.Implementations.Data else if (includeTypes.Length == 1) { whereClauses.Add("type=@type"); - if (statement != null) - { - statement.TryBind("@type", includeTypes[0]); - } + statement?.TryBind("@type", includeTypes[0]); } else if (includeTypes.Length > 1) { @@ -3556,10 +3519,7 @@ namespace Emby.Server.Implementations.Data if (query.ChannelIds.Length == 1) { whereClauses.Add("ChannelId=@ChannelId"); - if (statement != null) - { - statement.TryBind("@ChannelId", query.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture)); - } + statement?.TryBind("@ChannelId", query.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture)); } else if (query.ChannelIds.Length > 1) { @@ -3570,98 +3530,65 @@ namespace Emby.Server.Implementations.Data if (!query.ParentId.Equals(Guid.Empty)) { whereClauses.Add("ParentId=@ParentId"); - if (statement != null) - { - statement.TryBind("@ParentId", query.ParentId); - } + statement?.TryBind("@ParentId", query.ParentId); } if (!string.IsNullOrWhiteSpace(query.Path)) { whereClauses.Add("Path=@Path"); - if (statement != null) - { - statement.TryBind("@Path", GetPathToSave(query.Path)); - } + statement?.TryBind("@Path", GetPathToSave(query.Path)); } if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey)) { whereClauses.Add("PresentationUniqueKey=@PresentationUniqueKey"); - if (statement != null) - { - statement.TryBind("@PresentationUniqueKey", query.PresentationUniqueKey); - } + statement?.TryBind("@PresentationUniqueKey", query.PresentationUniqueKey); } if (query.MinCommunityRating.HasValue) { whereClauses.Add("CommunityRating>=@MinCommunityRating"); - if (statement != null) - { - statement.TryBind("@MinCommunityRating", query.MinCommunityRating.Value); - } + statement?.TryBind("@MinCommunityRating", query.MinCommunityRating.Value); } if (query.MinIndexNumber.HasValue) { whereClauses.Add("IndexNumber>=@MinIndexNumber"); - if (statement != null) - { - statement.TryBind("@MinIndexNumber", query.MinIndexNumber.Value); - } + statement?.TryBind("@MinIndexNumber", query.MinIndexNumber.Value); } if (query.MinDateCreated.HasValue) { whereClauses.Add("DateCreated>=@MinDateCreated"); - if (statement != null) - { - statement.TryBind("@MinDateCreated", query.MinDateCreated.Value); - } + statement?.TryBind("@MinDateCreated", query.MinDateCreated.Value); } if (query.MinDateLastSaved.HasValue) { whereClauses.Add("(DateLastSaved not null and DateLastSaved>=@MinDateLastSavedForUser)"); - if (statement != null) - { - statement.TryBind("@MinDateLastSaved", query.MinDateLastSaved.Value); - } + statement?.TryBind("@MinDateLastSaved", query.MinDateLastSaved.Value); } if (query.MinDateLastSavedForUser.HasValue) { whereClauses.Add("(DateLastSaved not null and DateLastSaved>=@MinDateLastSavedForUser)"); - if (statement != null) - { - statement.TryBind("@MinDateLastSavedForUser", query.MinDateLastSavedForUser.Value); - } + statement?.TryBind("@MinDateLastSavedForUser", query.MinDateLastSavedForUser.Value); } if (query.IndexNumber.HasValue) { whereClauses.Add("IndexNumber=@IndexNumber"); - if (statement != null) - { - statement.TryBind("@IndexNumber", query.IndexNumber.Value); - } + statement?.TryBind("@IndexNumber", query.IndexNumber.Value); } if (query.ParentIndexNumber.HasValue) { whereClauses.Add("ParentIndexNumber=@ParentIndexNumber"); - if (statement != null) - { - statement.TryBind("@ParentIndexNumber", query.ParentIndexNumber.Value); - } + statement?.TryBind("@ParentIndexNumber", query.ParentIndexNumber.Value); } if (query.ParentIndexNumberNotEquals.HasValue) { whereClauses.Add("(ParentIndexNumber<>@ParentIndexNumberNotEquals or ParentIndexNumber is null)"); - if (statement != null) - { - statement.TryBind("@ParentIndexNumberNotEquals", query.ParentIndexNumberNotEquals.Value); - } + statement?.TryBind("@ParentIndexNumberNotEquals", query.ParentIndexNumberNotEquals.Value); } var minEndDate = query.MinEndDate; @@ -3682,73 +3609,59 @@ namespace Emby.Server.Implementations.Data if (minEndDate.HasValue) { whereClauses.Add("EndDate>=@MinEndDate"); - if (statement != null) - { - statement.TryBind("@MinEndDate", minEndDate.Value); - } + statement?.TryBind("@MinEndDate", minEndDate.Value); } if (maxEndDate.HasValue) { whereClauses.Add("EndDate<=@MaxEndDate"); - if (statement != null) - { - statement.TryBind("@MaxEndDate", maxEndDate.Value); - } + statement?.TryBind("@MaxEndDate", maxEndDate.Value); } if (query.MinStartDate.HasValue) { whereClauses.Add("StartDate>=@MinStartDate"); - if (statement != null) - { - statement.TryBind("@MinStartDate", query.MinStartDate.Value); - } + statement?.TryBind("@MinStartDate", query.MinStartDate.Value); } if (query.MaxStartDate.HasValue) { whereClauses.Add("StartDate<=@MaxStartDate"); - if (statement != null) - { - statement.TryBind("@MaxStartDate", query.MaxStartDate.Value); - } + statement?.TryBind("@MaxStartDate", query.MaxStartDate.Value); } if (query.MinPremiereDate.HasValue) { whereClauses.Add("PremiereDate>=@MinPremiereDate"); - if (statement != null) - { - statement.TryBind("@MinPremiereDate", query.MinPremiereDate.Value); - } + statement?.TryBind("@MinPremiereDate", query.MinPremiereDate.Value); } + if (query.MaxPremiereDate.HasValue) { whereClauses.Add("PremiereDate<=@MaxPremiereDate"); - if (statement != null) - { - statement.TryBind("@MaxPremiereDate", query.MaxPremiereDate.Value); - } + statement?.TryBind("@MaxPremiereDate", query.MaxPremiereDate.Value); } - if (query.TrailerTypes.Length > 0) + var trailerTypes = query.TrailerTypes; + int trailerTypesLen = trailerTypes.Length; + if (trailerTypesLen > 0) { - var clauses = new List<string>(); - var index = 0; - foreach (var type in query.TrailerTypes) + const string Or = " OR "; + StringBuilder clause = new StringBuilder("(", trailerTypesLen * 32); + for (int i = 0; i < trailerTypesLen; i++) { - var paramName = "@TrailerTypes" + index; - - clauses.Add("TrailerTypes like " + paramName); - if (statement != null) - { - statement.TryBind(paramName, "%" + type + "%"); - } - index++; + var paramName = "@TrailerTypes" + i; + clause.Append("TrailerTypes like ") + .Append(paramName) + .Append(Or); + statement?.TryBind(paramName, "%" + trailerTypes[i] + "%"); } - var clause = "(" + string.Join(" OR ", clauses) + ")"; - whereClauses.Add(clause); + + // Remove last " OR " + clause.Length -= Or.Length; + clause.Append(')'); + + whereClauses.Add(clause.ToString()); } if (query.IsAiring.HasValue) @@ -3756,24 +3669,15 @@ namespace Emby.Server.Implementations.Data if (query.IsAiring.Value) { whereClauses.Add("StartDate<=@MaxStartDate"); - if (statement != null) - { - statement.TryBind("@MaxStartDate", DateTime.UtcNow); - } + statement?.TryBind("@MaxStartDate", DateTime.UtcNow); whereClauses.Add("EndDate>=@MinEndDate"); - if (statement != null) - { - statement.TryBind("@MinEndDate", DateTime.UtcNow); - } + statement?.TryBind("@MinEndDate", DateTime.UtcNow); } else { whereClauses.Add("(StartDate>@IsAiringDate OR EndDate < @IsAiringDate)"); - if (statement != null) - { - statement.TryBind("@IsAiringDate", DateTime.UtcNow); - } + statement?.TryBind("@IsAiringDate", DateTime.UtcNow); } } @@ -3788,13 +3692,10 @@ namespace Emby.Server.Implementations.Data var paramName = "@PersonId" + index; clauses.Add("(guid in (select itemid from People where Name = (select Name from TypedBaseItems where guid=" + paramName + ")))"); - - if (statement != null) - { - statement.TryBind(paramName, personId.ToByteArray()); - } + statement?.TryBind(paramName, personId.ToByteArray()); index++; } + var clause = "(" + string.Join(" OR ", clauses) + ")"; whereClauses.Add(clause); } @@ -3802,47 +3703,31 @@ namespace Emby.Server.Implementations.Data if (!string.IsNullOrWhiteSpace(query.Person)) { whereClauses.Add("Guid in (select ItemId from People where Name=@PersonName)"); - if (statement != null) - { - statement.TryBind("@PersonName", query.Person); - } + statement?.TryBind("@PersonName", query.Person); } if (!string.IsNullOrWhiteSpace(query.MinSortName)) { whereClauses.Add("SortName>=@MinSortName"); - if (statement != null) - { - statement.TryBind("@MinSortName", query.MinSortName); - } + statement?.TryBind("@MinSortName", query.MinSortName); } if (!string.IsNullOrWhiteSpace(query.ExternalSeriesId)) { whereClauses.Add("ExternalSeriesId=@ExternalSeriesId"); - if (statement != null) - { - statement.TryBind("@ExternalSeriesId", query.ExternalSeriesId); - } + statement?.TryBind("@ExternalSeriesId", query.ExternalSeriesId); } if (!string.IsNullOrWhiteSpace(query.ExternalId)) { whereClauses.Add("ExternalId=@ExternalId"); - if (statement != null) - { - statement.TryBind("@ExternalId", query.ExternalId); - } + statement?.TryBind("@ExternalId", query.ExternalId); } if (!string.IsNullOrWhiteSpace(query.Name)) { whereClauses.Add("CleanName=@Name"); - - if (statement != null) - { - statement.TryBind("@Name", GetCleanValue(query.Name)); - } + statement?.TryBind("@Name", GetCleanValue(query.Name)); } // These are the same, for now @@ -3861,28 +3746,21 @@ namespace Emby.Server.Implementations.Data if (!string.IsNullOrWhiteSpace(query.NameStartsWith)) { whereClauses.Add("SortName like @NameStartsWith"); - if (statement != null) - { - statement.TryBind("@NameStartsWith", query.NameStartsWith + "%"); - } + statement?.TryBind("@NameStartsWith", query.NameStartsWith + "%"); } + if (!string.IsNullOrWhiteSpace(query.NameStartsWithOrGreater)) { whereClauses.Add("SortName >= @NameStartsWithOrGreater"); // lowercase this because SortName is stored as lowercase - if (statement != null) - { - statement.TryBind("@NameStartsWithOrGreater", query.NameStartsWithOrGreater.ToLowerInvariant()); - } + statement?.TryBind("@NameStartsWithOrGreater", query.NameStartsWithOrGreater.ToLowerInvariant()); } + if (!string.IsNullOrWhiteSpace(query.NameLessThan)) { whereClauses.Add("SortName < @NameLessThan"); // lowercase this because SortName is stored as lowercase - if (statement != null) - { - statement.TryBind("@NameLessThan", query.NameLessThan.ToLowerInvariant()); - } + statement?.TryBind("@NameLessThan", query.NameLessThan.ToLowerInvariant()); } if (query.ImageTypes.Length > 0) @@ -3898,18 +3776,12 @@ namespace Emby.Server.Implementations.Data if (query.IsLiked.Value) { whereClauses.Add("rating>=@UserRating"); - if (statement != null) - { - statement.TryBind("@UserRating", UserItemData.MinLikeValue); - } + statement?.TryBind("@UserRating", UserItemData.MinLikeValue); } else { whereClauses.Add("(rating is null or rating<@UserRating)"); - if (statement != null) - { - statement.TryBind("@UserRating", UserItemData.MinLikeValue); - } + statement?.TryBind("@UserRating", UserItemData.MinLikeValue); } } @@ -3923,10 +3795,8 @@ namespace Emby.Server.Implementations.Data { whereClauses.Add("(IsFavorite is null or IsFavorite=@IsFavoriteOrLiked)"); } - if (statement != null) - { - statement.TryBind("@IsFavoriteOrLiked", query.IsFavoriteOrLiked.Value); - } + + statement?.TryBind("@IsFavoriteOrLiked", query.IsFavoriteOrLiked.Value); } if (query.IsFavorite.HasValue) @@ -3939,10 +3809,8 @@ namespace Emby.Server.Implementations.Data { whereClauses.Add("(IsFavorite is null or IsFavorite=@IsFavorite)"); } - if (statement != null) - { - statement.TryBind("@IsFavorite", query.IsFavorite.Value); - } + + statement?.TryBind("@IsFavorite", query.IsFavorite.Value); } if (EnableJoinUserData(query)) @@ -3971,10 +3839,8 @@ namespace Emby.Server.Implementations.Data { whereClauses.Add("(played is null or played=@IsPlayed)"); } - if (statement != null) - { - statement.TryBind("@IsPlayed", query.IsPlayed.Value); - } + + statement?.TryBind("@IsPlayed", query.IsPlayed.Value); } } } @@ -4006,6 +3872,7 @@ namespace Emby.Server.Implementations.Data } index++; } + var clause = "(" + string.Join(" OR ", clauses) + ")"; whereClauses.Add(clause); } @@ -4025,6 +3892,7 @@ namespace Emby.Server.Implementations.Data } index++; } + var clause = "(" + string.Join(" OR ", clauses) + ")"; whereClauses.Add(clause); } @@ -4758,18 +4626,22 @@ namespace Emby.Server.Implementations.Data { list.Add(typeof(Person).Name); } + if (IsTypeInQuery(typeof(Genre).Name, query)) { list.Add(typeof(Genre).Name); } + if (IsTypeInQuery(typeof(MusicGenre).Name, query)) { list.Add(typeof(MusicGenre).Name); } + if (IsTypeInQuery(typeof(MusicArtist).Name, query)) { list.Add(typeof(MusicArtist).Name); } + if (IsTypeInQuery(typeof(Studio).Name, query)) { list.Add(typeof(Studio).Name); @@ -4843,7 +4715,7 @@ namespace Emby.Server.Implementations.Data return false; } - private static readonly Type[] KnownTypes = + private static readonly Type[] _knownTypes = { typeof(LiveTvProgram), typeof(LiveTvChannel), @@ -4912,7 +4784,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type { var dict = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase); - foreach (var t in KnownTypes) + foreach (var t in _knownTypes) { dict[t.Name] = new[] { t.FullName }; } @@ -4924,7 +4796,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type } // Not crazy about having this all the way down here, but at least it's in one place - readonly Dictionary<string, string[]> _types = GetTypeMapDictionary(); + private readonly Dictionary<string, string[]> _types = GetTypeMapDictionary(); private string[] MapIncludeItemTypes(string value) { @@ -4941,7 +4813,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type return Array.Empty<string>(); } - public void DeleteItem(Guid id, CancellationToken cancellationToken) + public void DeleteItem(Guid id) { if (id == Guid.Empty) { @@ -4977,7 +4849,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type } } - private void ExecuteWithSingleParam(IDatabaseConnection db, string query, byte[] value) + private void ExecuteWithSingleParam(IDatabaseConnection db, string query, ReadOnlySpan<byte> value) { using (var statement = PrepareStatement(db, query)) { @@ -5007,6 +4879,11 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type commandText += " order by ListOrder"; + if (query.Limit > 0) + { + commandText += " LIMIT " + query.Limit; + } + using (var connection = GetConnection(true)) { var list = new List<string>(); @@ -5045,6 +4922,11 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type commandText += " order by ListOrder"; + if (query.Limit > 0) + { + commandText += " LIMIT " + query.Limit; + } + using (var connection = GetConnection(true)) { var list = new List<PersonInfo>(); @@ -5527,6 +5409,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type { GetWhereClauses(typeSubQuery, null); } + BindSimilarParams(query, statement); BindSearchParams(query, statement); GetWhereClauses(innerQuery, statement); @@ -5568,7 +5451,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type } var allTypes = typeString.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries) - .ToLookup(i => i); + .ToLookup(x => x); foreach (var type in allTypes) { @@ -5659,30 +5542,26 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type private void InsertItemValues(byte[] idBlob, List<(int, string)> values, IDatabaseConnection db) { + const int Limit = 100; var startIndex = 0; - var limit = 100; while (startIndex < values.Count) { var insertText = new StringBuilder("insert into ItemValues (ItemId, Type, Value, CleanValue) values "); - var endIndex = Math.Min(values.Count, startIndex + limit); - var isSubsequentRow = false; + var endIndex = Math.Min(values.Count, startIndex + Limit); for (var i = startIndex; i < endIndex; i++) { - if (isSubsequentRow) - { - insertText.Append(','); - } - insertText.AppendFormat( CultureInfo.InvariantCulture, - "(@ItemId, @Type{0}, @Value{0}, @CleanValue{0})", + "(@ItemId, @Type{0}, @Value{0}, @CleanValue{0}),", i); - isSubsequentRow = true; } + // Remove last comma + insertText.Length--; + using (var statement = PrepareStatement(db, insertText.ToString())) { statement.TryBind("@ItemId", idBlob); @@ -5710,7 +5589,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type statement.MoveNext(); } - startIndex += limit; + startIndex += Limit; } } @@ -5745,28 +5624,23 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type private void InsertPeople(byte[] idBlob, List<PersonInfo> people, IDatabaseConnection db) { + const int Limit = 100; var startIndex = 0; - var limit = 100; var listIndex = 0; while (startIndex < people.Count) { var insertText = new StringBuilder("insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values "); - var endIndex = Math.Min(people.Count, startIndex + limit); - var isSubsequentRow = false; - + var endIndex = Math.Min(people.Count, startIndex + Limit); for (var i = startIndex; i < endIndex; i++) { - if (isSubsequentRow) - { - insertText.Append(','); - } - - insertText.AppendFormat("(@ItemId, @Name{0}, @Role{0}, @PersonType{0}, @SortOrder{0}, @ListOrder{0})", i.ToString(CultureInfo.InvariantCulture)); - isSubsequentRow = true; + insertText.AppendFormat("(@ItemId, @Name{0}, @Role{0}, @PersonType{0}, @SortOrder{0}, @ListOrder{0}),", i.ToString(CultureInfo.InvariantCulture)); } + // Remove last comma + insertText.Length--; + using (var statement = PrepareStatement(db, insertText.ToString())) { statement.TryBind("@ItemId", idBlob); @@ -5790,16 +5664,17 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type statement.MoveNext(); } - startIndex += limit; + startIndex += Limit; } } private PersonInfo GetPerson(IReadOnlyList<IResultSetValue> reader) { - var item = new PersonInfo(); - - item.ItemId = reader.GetGuid(0); - item.Name = reader.GetString(1); + var item = new PersonInfo + { + ItemId = reader.GetGuid(0), + Name = reader.GetString(1) + }; if (!reader.IsDBNull(2)) { @@ -5906,20 +5781,28 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type private void InsertMediaStreams(byte[] idBlob, List<MediaStream> streams, IDatabaseConnection db) { + const int Limit = 10; var startIndex = 0; - var limit = 10; while (startIndex < streams.Count) { - var insertText = new StringBuilder(string.Format("insert into mediastreams ({0}) values ", string.Join(",", _mediaStreamSaveColumns))); + var insertText = new StringBuilder("insert into mediastreams ("); + foreach (var column in _mediaStreamSaveColumns) + { + insertText.Append(column).Append(','); + } - var endIndex = Math.Min(streams.Count, startIndex + limit); + // Remove last comma + insertText.Length--; + insertText.Append(") values "); + + var endIndex = Math.Min(streams.Count, startIndex + Limit); for (var i = startIndex; i < endIndex; i++) { if (i != startIndex) { - insertText.Append(","); + insertText.Append(','); } var index = i.ToString(CultureInfo.InvariantCulture); @@ -5927,11 +5810,12 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type foreach (var column in _mediaStreamSaveColumns.Skip(1)) { - insertText.Append("@" + column + index + ","); + insertText.Append('@').Append(column).Append(index).Append(','); } + insertText.Length -= 1; // Remove the last comma - insertText.Append(")"); + insertText.Append(')'); } using (var statement = PrepareStatement(db, insertText.ToString())) @@ -5993,7 +5877,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type statement.MoveNext(); } - startIndex += limit; + startIndex += Limit; } } @@ -6010,7 +5894,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type Index = reader[1].ToInt() }; - item.Type = (MediaStreamType)Enum.Parse(typeof(MediaStreamType), reader[2].ToString(), true); + item.Type = Enum.Parse<MediaStreamType>(reader[2].ToString(), true); if (reader[3].SQLiteType != SQLiteType.Null) { @@ -6158,7 +6042,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type item.ColorTransfer = reader[34].ToString(); } - if (item.Type == MediaStreamType.Subtitle){ + if (item.Type == MediaStreamType.Subtitle) + { item.localizedUndefined = _localization.GetLocalizedString("Undefined"); item.localizedDefault = _localization.GetLocalizedString("Default"); item.localizedForced = _localization.GetLocalizedString("Forced"); @@ -6287,8 +6172,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type statement.TryBind("@Codec" + index, attachment.Codec); statement.TryBind("@CodecTag" + index, attachment.CodecTag); statement.TryBind("@Comment" + index, attachment.Comment); - statement.TryBind("@FileName" + index, attachment.FileName); - statement.TryBind("@MimeType" + index, attachment.MimeType); + statement.TryBind("@Filename" + index, attachment.FileName); + statement.TryBind("@MIMEType" + index, attachment.MimeType); } statement.Reset(); diff --git a/Emby.Server.Implementations/Data/SqliteUserRepository.cs b/Emby.Server.Implementations/Data/SqliteUserRepository.cs index a042320c9..0c3f26974 100644 --- a/Emby.Server.Implementations/Data/SqliteUserRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserRepository.cs @@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Data IServerApplicationPaths appPaths) : base(logger) { - _jsonOptions = JsonDefaults.GetOptions();; + _jsonOptions = JsonDefaults.GetOptions(); DbFilePath = Path.Combine(appPaths.DataPath, "users.db"); } diff --git a/Emby.Server.Implementations/Diagnostics/CommonProcess.cs b/Emby.Server.Implementations/Diagnostics/CommonProcess.cs deleted file mode 100644 index bfa49ac5f..000000000 --- a/Emby.Server.Implementations/Diagnostics/CommonProcess.cs +++ /dev/null @@ -1,152 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Diagnostics; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Diagnostics; - -namespace Emby.Server.Implementations.Diagnostics -{ - public class CommonProcess : IProcess - { - private readonly Process _process; - - private bool _disposed = false; - private bool _hasExited; - - public CommonProcess(ProcessOptions options) - { - StartInfo = options; - - var startInfo = new ProcessStartInfo - { - Arguments = options.Arguments, - FileName = options.FileName, - WorkingDirectory = options.WorkingDirectory, - UseShellExecute = options.UseShellExecute, - CreateNoWindow = options.CreateNoWindow, - RedirectStandardError = options.RedirectStandardError, - RedirectStandardInput = options.RedirectStandardInput, - RedirectStandardOutput = options.RedirectStandardOutput, - ErrorDialog = options.ErrorDialog - }; - - - if (options.IsHidden) - { - startInfo.WindowStyle = ProcessWindowStyle.Hidden; - } - - _process = new Process - { - StartInfo = startInfo - }; - - if (options.EnableRaisingEvents) - { - _process.EnableRaisingEvents = true; - _process.Exited += OnProcessExited; - } - } - - public event EventHandler Exited; - - public ProcessOptions StartInfo { get; } - - public StreamWriter StandardInput => _process.StandardInput; - - public StreamReader StandardError => _process.StandardError; - - public StreamReader StandardOutput => _process.StandardOutput; - - public int ExitCode => _process.ExitCode; - - private bool HasExited - { - get - { - if (_hasExited) - { - return true; - } - - try - { - _hasExited = _process.HasExited; - } - catch (InvalidOperationException) - { - _hasExited = true; - } - - return _hasExited; - } - } - - public void Start() - { - _process.Start(); - } - - public void Kill() - { - _process.Kill(); - } - - public bool WaitForExit(int timeMs) - { - return _process.WaitForExit(timeMs); - } - - public Task<bool> WaitForExitAsync(int timeMs) - { - // Note: For this function to work correctly, the option EnableRisingEvents needs to be set to true. - - if (HasExited) - { - return Task.FromResult(true); - } - - timeMs = Math.Max(0, timeMs); - - var tcs = new TaskCompletionSource<bool>(); - - var cancellationToken = new CancellationTokenSource(timeMs).Token; - - _process.Exited += (sender, args) => tcs.TrySetResult(true); - - cancellationToken.Register(() => tcs.TrySetResult(HasExited)); - - return tcs.Task; - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (_disposed) - { - return; - } - - if (disposing) - { - _process?.Dispose(); - } - - _disposed = true; - } - - private void OnProcessExited(object sender, EventArgs e) - { - _hasExited = true; - Exited?.Invoke(this, e); - } - } -} diff --git a/Emby.Server.Implementations/Diagnostics/ProcessFactory.cs b/Emby.Server.Implementations/Diagnostics/ProcessFactory.cs deleted file mode 100644 index 02ad3c1a8..000000000 --- a/Emby.Server.Implementations/Diagnostics/ProcessFactory.cs +++ /dev/null @@ -1,14 +0,0 @@ -#pragma warning disable CS1591 - -using MediaBrowser.Model.Diagnostics; - -namespace Emby.Server.Implementations.Diagnostics -{ - public class ProcessFactory : IProcessFactory - { - public IProcess Create(ProcessOptions options) - { - return new CommonProcess(options); - } - } -} diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 65711e89d..34a342cf7 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1056,30 +1056,19 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.SpecialFeatureCount)) { - if (allExtras == null) - { - allExtras = item.GetExtras().ToArray(); - } - + allExtras = item.GetExtras().ToArray(); dto.SpecialFeatureCount = allExtras.Count(i => i.ExtraType.HasValue && BaseItem.DisplayExtraTypes.Contains(i.ExtraType.Value)); } if (options.ContainsField(ItemFields.LocalTrailerCount)) { - int trailerCount = 0; - if (allExtras == null) - { - allExtras = item.GetExtras().ToArray(); - } - - trailerCount += allExtras.Count(i => i.ExtraType.HasValue && i.ExtraType.Value == ExtraType.Trailer); + allExtras ??= item.GetExtras().ToArray(); + dto.LocalTrailerCount = allExtras.Count(i => i.ExtraType == ExtraType.Trailer); if (item is IHasTrailers hasTrailers) { - trailerCount += hasTrailers.GetTrailerCount(); + dto.LocalTrailerCount += hasTrailers.GetTrailerCount(); } - - dto.LocalTrailerCount = trailerCount; } // Add EpisodeInfo diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index f8560ca85..3d065f70a 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -29,14 +29,14 @@ <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.1" /> - <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.1" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.1" /> - <PackageReference Include="Mono.Nat" Version="2.0.0" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.3" /> + <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.3" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.3" /> + <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.3" /> + <PackageReference Include="Mono.Nat" Version="2.0.1" /> <PackageReference Include="ServiceStack.Text.Core" Version="5.8.0" /> - <PackageReference Include="sharpcompress" Version="0.24.0" /> + <PackageReference Include="sharpcompress" Version="0.25.0" /> <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" /> - <PackageReference Include="System.Interactive.Async" Version="4.0.0" /> </ItemGroup> <ItemGroup> diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs index e290c62e1..37d7fd479 100644 --- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Net; using System.Text; @@ -26,10 +27,10 @@ namespace Emby.Server.Implementations.EntryPoints private readonly IServerConfigurationManager _config; private readonly IDeviceDiscovery _deviceDiscovery; - private readonly object _createdRulesLock = new object(); - private List<IPEndPoint> _createdRules = new List<IPEndPoint>(); + private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>(); + private Timer _timer; - private string _lastConfigIdentifier; + private string _configIdentifier; private bool _disposed = false; @@ -60,6 +61,7 @@ namespace Emby.Server.Implementations.EntryPoints return new StringBuilder(32) .Append(config.EnableUPnP).Append(Separator) .Append(config.PublicPort).Append(Separator) + .Append(config.PublicHttpsPort).Append(Separator) .Append(_appHost.HttpPort).Append(Separator) .Append(_appHost.HttpsPort).Append(Separator) .Append(_appHost.EnableHttps).Append(Separator) @@ -69,7 +71,10 @@ namespace Emby.Server.Implementations.EntryPoints private void OnConfigurationUpdated(object sender, EventArgs e) { - if (!string.Equals(_lastConfigIdentifier, GetConfigIdentifier(), StringComparison.OrdinalIgnoreCase)) + var oldConfigIdentifier = _configIdentifier; + _configIdentifier = GetConfigIdentifier(); + + if (!string.Equals(_configIdentifier, oldConfigIdentifier, StringComparison.OrdinalIgnoreCase)) { Stop(); Start(); @@ -93,21 +98,19 @@ namespace Emby.Server.Implementations.EntryPoints return; } - _logger.LogDebug("Starting NAT discovery"); + _logger.LogInformation("Starting NAT discovery"); NatUtility.DeviceFound += OnNatUtilityDeviceFound; NatUtility.StartDiscovery(); - _timer = new Timer(ClearCreatedRules, null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10)); + _timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10)); _deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered; - - _lastConfigIdentifier = GetConfigIdentifier(); } private void Stop() { - _logger.LogDebug("Stopping NAT discovery"); + _logger.LogInformation("Stopping NAT discovery"); NatUtility.StopDiscovery(); NatUtility.DeviceFound -= OnNatUtilityDeviceFound; @@ -117,26 +120,16 @@ namespace Emby.Server.Implementations.EntryPoints _deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered; } - private void ClearCreatedRules(object state) - { - lock (_createdRulesLock) - { - _createdRules.Clear(); - } - } - private void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e) { NatUtility.Search(e.Argument.LocalIpAddress, NatProtocol.Upnp); } - private void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e) + private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e) { try { - var device = e.Device; - - CreateRules(device); + await CreateRules(e.Device).ConfigureAwait(false); } catch (Exception ex) { @@ -144,7 +137,7 @@ namespace Emby.Server.Implementations.EntryPoints } } - private async void CreateRules(INatDevice device) + private Task CreateRules(INatDevice device) { if (_disposed) { @@ -153,50 +146,46 @@ namespace Emby.Server.Implementations.EntryPoints // On some systems the device discovered event seems to fire repeatedly // This check will help ensure we're not trying to port map the same device over and over - var address = device.DeviceEndpoint; - - lock (_createdRulesLock) + if (!_createdRules.TryAdd(device.DeviceEndpoint, 0)) { - if (!_createdRules.Contains(address)) - { - _createdRules.Add(address); - } - else - { - return; - } + return Task.CompletedTask; } - try - { - await CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating http port map"); - return; - } + return Task.WhenAll(CreatePortMaps(device)); + } - try - { - await CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort).ConfigureAwait(false); - } - catch (Exception ex) + private IEnumerable<Task> CreatePortMaps(INatDevice device) + { + yield return CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort); + + if (_appHost.EnableHttps) { - _logger.LogError(ex, "Error creating https port map"); + yield return CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort); } } - private Task<Mapping> CreatePortMap(INatDevice device, int privatePort, int publicPort) + private async Task CreatePortMap(INatDevice device, int privatePort, int publicPort) { _logger.LogDebug( - "Creating port map on local port {0} to public port {1} with device {2}", + "Creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}", privatePort, publicPort, device.DeviceEndpoint); - return device.CreatePortMapAsync( - new Mapping(Protocol.Tcp, privatePort, publicPort, 0, _appHost.Name)); + try + { + var mapping = new Mapping(Protocol.Tcp, privatePort, publicPort, 0, _appHost.Name); + await device.CreatePortMapAsync(mapping).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Error creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}.", + privatePort, + publicPort, + device.DeviceEndpoint); + } } /// <inheritdoc /> diff --git a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs index 5f2d629fe..8e9771931 100644 --- a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs +++ b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs @@ -2,7 +2,9 @@ using System.Threading.Tasks; using Emby.Server.Implementations.Browser; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Plugins; +using Microsoft.Extensions.Configuration; namespace Emby.Server.Implementations.EntryPoints { @@ -11,10 +13,8 @@ namespace Emby.Server.Implementations.EntryPoints /// </summary> public sealed class StartupWizard : IServerEntryPoint { - /// <summary> - /// The app host. - /// </summary> private readonly IServerApplicationHost _appHost; + private readonly IConfiguration _appConfig; private readonly IServerConfigurationManager _config; /// <summary> @@ -22,9 +22,10 @@ namespace Emby.Server.Implementations.EntryPoints /// </summary> /// <param name="appHost">The application host.</param> /// <param name="config">The configuration manager.</param> - public StartupWizard(IServerApplicationHost appHost, IServerConfigurationManager config) + public StartupWizard(IServerApplicationHost appHost, IConfiguration appConfig, IServerConfigurationManager config) { _appHost = appHost; + _appConfig = appConfig; _config = config; } @@ -36,7 +37,11 @@ namespace Emby.Server.Implementations.EntryPoints return Task.CompletedTask; } - if (!_config.Configuration.IsStartupWizardCompleted) + if (!_appConfig.HostWebClient()) + { + BrowserLauncher.OpenSwaggerPage(_appHost); + } + else if (!_config.Configuration.IsStartupWizardCompleted) { BrowserLauncher.OpenWebApp(_appHost); } diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs index 45fa03cdd..882bfe2f6 100644 --- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs +++ b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs @@ -96,13 +96,13 @@ namespace Emby.Server.Implementations.HttpClientManager switch (options.DecompressionMethod) { - case CompressionMethod.Deflate | CompressionMethod.Gzip: + case CompressionMethods.Deflate | CompressionMethods.Gzip: request.Headers.Add(HeaderNames.AcceptEncoding, new[] { "gzip", "deflate" }); break; - case CompressionMethod.Deflate: + case CompressionMethods.Deflate: request.Headers.Add(HeaderNames.AcceptEncoding, "deflate"); break; - case CompressionMethod.Gzip: + case CompressionMethods.Gzip: request.Headers.Add(HeaderNames.AcceptEncoding, "gzip"); break; default: @@ -239,15 +239,10 @@ namespace Emby.Server.Implementations.HttpClientManager var httpWebRequest = GetRequestMessage(options, httpMethod); - if (options.RequestContentBytes != null - || !string.IsNullOrEmpty(options.RequestContent) + if (!string.IsNullOrEmpty(options.RequestContent) || httpMethod == HttpMethod.Post) { - if (options.RequestContentBytes != null) - { - httpWebRequest.Content = new ByteArrayContent(options.RequestContentBytes); - } - else if (options.RequestContent != null) + if (options.RequestContent != null) { httpWebRequest.Content = new StringContent( options.RequestContent, diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs index 82f1e5b52..0b61e40b0 100644 --- a/Emby.Server.Implementations/HttpServer/FileWriter.cs +++ b/Emby.Server.Implementations/HttpServer/FileWriter.cs @@ -11,8 +11,8 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.IO; using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.HttpServer diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index 93572b8bf..5ae65a4e3 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -17,11 +17,13 @@ using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Events; +using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using ServiceStack.Text.Jsv; @@ -29,6 +31,12 @@ namespace Emby.Server.Implementations.HttpServer { public class HttpListenerHost : IHttpServer, IDisposable { + /// <summary> + /// The key for a setting that specifies the default redirect path + /// to use for requests where the URL base prefix is invalid or missing. + /// </summary> + public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath"; + private readonly ILogger _logger; private readonly IServerConfigurationManager _config; private readonly INetworkManager _networkManager; @@ -41,6 +49,8 @@ namespace Emby.Server.Implementations.HttpServer private readonly string _baseUrlPrefix; private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>(); private readonly List<IWebSocketConnection> _webSocketConnections = new List<IWebSocketConnection>(); + private readonly IHostEnvironment _hostEnvironment; + private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>(); private bool _disposed = false; @@ -52,23 +62,30 @@ namespace Emby.Server.Implementations.HttpServer INetworkManager networkManager, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, - IHttpListener socketListener) + IHttpListener socketListener, + ILocalizationManager localizationManager, + ServiceController serviceController, + IHostEnvironment hostEnvironment) { _appHost = applicationHost; _logger = logger; _config = config; - _defaultRedirectPath = configuration["HttpListenerHost:DefaultRedirectPath"]; + _defaultRedirectPath = configuration[DefaultRedirectKey]; _baseUrlPrefix = _config.Configuration.BaseUrl; _networkManager = networkManager; _jsonSerializer = jsonSerializer; _xmlSerializer = xmlSerializer; _socketListener = socketListener; + ServiceController = serviceController; + _socketListener.WebSocketConnected = OnWebSocketConnected; + _hostEnvironment = hostEnvironment; _funcParseFn = t => s => JsvReader.GetParseFn(t)(s); Instance = this; ResponseFilters = Array.Empty<Action<IRequest, HttpResponse, object>>(); + GlobalResponse = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); } public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected; @@ -81,7 +98,7 @@ namespace Emby.Server.Implementations.HttpServer public string GlobalResponse { get; set; } - public ServiceController ServiceController { get; private set; } + public ServiceController ServiceController { get; } public object CreateInstance(Type type) { @@ -222,7 +239,7 @@ namespace Emby.Server.Implementations.HttpServer } } - private async Task ErrorHandler(Exception ex, IRequest httpReq, bool logExceptionStackTrace) + private async Task ErrorHandler(Exception ex, IRequest httpReq, bool logExceptionStackTrace, string urlToLog) { try { @@ -230,11 +247,11 @@ namespace Emby.Server.Implementations.HttpServer if (logExceptionStackTrace) { - _logger.LogError(ex, "Error processing request"); + _logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog); } else { - _logger.LogError("Error processing request: {Message}", ex.Message); + _logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog); } var httpRes = httpReq.Response; @@ -254,7 +271,7 @@ namespace Emby.Server.Implementations.HttpServer } catch (Exception errorEx) { - _logger.LogError(errorEx, "Error this.ProcessRequest(context)(Exception while writing error to the response)"); + _logger.LogError(errorEx, "Error this.ProcessRequest(context)(Exception while writing error to the response). URL: {Url}", urlToLog); } } @@ -439,7 +456,7 @@ namespace Emby.Server.Implementations.HttpServer var stopWatch = new Stopwatch(); stopWatch.Start(); var httpRes = httpReq.Response; - string urlToLog = null; + string urlToLog = GetUrlToLog(urlString); string remoteIp = httpReq.RemoteIp; try @@ -485,8 +502,6 @@ namespace Emby.Server.Implementations.HttpServer return; } - urlToLog = GetUrlToLog(urlString); - if (string.Equals(localPath, _baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase) || string.Equals(localPath, _baseUrlPrefix, StringComparison.OrdinalIgnoreCase) || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase) @@ -518,22 +533,25 @@ namespace Emby.Server.Implementations.HttpServer } else { - await ErrorHandler(new FileNotFoundException(), httpReq, false).ConfigureAwait(false); + throw new FileNotFoundException(); } } - catch (Exception ex) when (ex is SocketException || ex is IOException || ex is OperationCanceledException) - { - await ErrorHandler(ex, httpReq, false).ConfigureAwait(false); - } - catch (SecurityException ex) - { - await ErrorHandler(ex, httpReq, false).ConfigureAwait(false); - } catch (Exception ex) { - var logException = !string.Equals(ex.GetType().Name, "SocketException", StringComparison.OrdinalIgnoreCase); + // Do not handle exceptions manually when in development mode + // The framework-defined development exception page will be returned instead + if (_hostEnvironment.IsDevelopment()) + { + throw; + } - await ErrorHandler(ex, httpReq, logException).ConfigureAwait(false); + bool ignoreStackTrace = + ex is SocketException + || ex is IOException + || ex is OperationCanceledException + || ex is SecurityException + || ex is FileNotFoundException; + await ErrorHandler(ex, httpReq, !ignoreStackTrace, urlToLog).ConfigureAwait(false); } finally { @@ -585,17 +603,15 @@ namespace Emby.Server.Implementations.HttpServer /// <summary> /// Adds the rest handlers. /// </summary> - /// <param name="services">The services.</param> - /// <param name="listeners"></param> - /// <param name="urlPrefixes"></param> - public void Init(IEnumerable<IService> services, IEnumerable<IWebSocketListener> listeners, IEnumerable<string> urlPrefixes) + /// <param name="serviceTypes">The service types to register with the <see cref="ServiceController"/>.</param> + /// <param name="listeners">The web socket listeners.</param> + /// <param name="urlPrefixes">The URL prefixes. See <see cref="UrlPrefixes"/>.</param> + public void Init(IEnumerable<Type> serviceTypes, IEnumerable<IWebSocketListener> listeners, IEnumerable<string> urlPrefixes) { _webSocketListeners = listeners.ToArray(); UrlPrefixes = urlPrefixes.ToArray(); - ServiceController = new ServiceController(); - var types = services.Select(r => r.GetType()); - ServiceController.Init(this, types); + ServiceController.Init(this, serviceTypes); ResponseFilters = new Action<IRequest, HttpResponse, object>[] { diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index b42662420..464ca3a0b 100644 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -28,6 +28,12 @@ namespace Emby.Server.Implementations.HttpServer /// </summary> public class HttpResultFactory : IHttpResultFactory { + // Last-Modified and If-Modified-Since must follow strict date format, + // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since + private const string HttpDateFormat = "ddd, dd MMM yyyy HH:mm:ss \"GMT\""; + // We specifically use en-US culture because both day of week and month names require it + private static readonly CultureInfo _enUSculture = new CultureInfo("en-US", false); + /// <summary> /// The logger. /// </summary> @@ -420,7 +426,11 @@ namespace Emby.Server.Implementations.HttpServer if (!noCache) { - DateTime.TryParse(requestContext.Headers[HeaderNames.IfModifiedSince], out var ifModifiedSinceHeader); + if (!DateTime.TryParseExact(requestContext.Headers[HeaderNames.IfModifiedSince], HttpDateFormat, _enUSculture, DateTimeStyles.AssumeUniversal, out var ifModifiedSinceHeader)) + { + _logger.LogDebug("Failed to parse If-Modified-Since header date: {0}", requestContext.Headers[HeaderNames.IfModifiedSince]); + return null; + } if (IsNotModified(ifModifiedSinceHeader, options.CacheDuration, options.DateLastModified)) { @@ -629,7 +639,7 @@ namespace Emby.Server.Implementations.HttpServer if (lastModifiedDate.HasValue) { - responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToString(CultureInfo.InvariantCulture); + responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToUniversalTime().ToString(HttpDateFormat, _enUSculture); } } diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 48599beb7..7461ec4f1 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -587,11 +587,11 @@ namespace Emby.Server.Implementations.IO // some drives on linux have no actual size or are used for other purposes return DriveInfo.GetDrives().Where(d => d.IsReady && d.TotalSize != 0 && d.DriveType != DriveType.Ram) .Select(d => new FileSystemMetadata - { - Name = d.Name, - FullName = d.RootDirectory.FullName, - IsDirectory = true - }).ToList(); + { + Name = d.Name, + FullName = d.RootDirectory.FullName, + IsDirectory = true + }).ToList(); } public virtual IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false) diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs index 6e915de3d..16b68170b 100644 --- a/Emby.Server.Implementations/IStartupOptions.cs +++ b/Emby.Server.Implementations/IStartupOptions.cs @@ -3,33 +3,38 @@ namespace Emby.Server.Implementations public interface IStartupOptions { /// <summary> - /// --ffmpeg + /// Gets the value of the --ffmpeg command line option. /// </summary> string FFmpegPath { get; } /// <summary> - /// --service + /// Gets the value of the --service command line option. /// </summary> bool IsService { get; } /// <summary> - /// --noautorunwebapp + /// Gets the value of the --noautorunwebapp command line option. /// </summary> bool NoAutoRunWebApp { get; } /// <summary> - /// --package-name + /// Gets the value of the --package-name command line option. /// </summary> string PackageName { get; } /// <summary> - /// --restartpath + /// Gets the value of the --restartpath command line option. /// </summary> string RestartPath { get; } /// <summary> - /// --restartargs + /// Gets the value of the --restartargs command line option. /// </summary> string RestartArgs { get; } + + /// <summary> + /// Gets the value of the --plugin-manifest-url command line option. + /// </summary> + string PluginManifestUrl { get; } } } diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 8ec4d08be..50a5135d4 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -437,10 +437,10 @@ namespace Emby.Server.Implementations.Library item.SetParent(null); - ItemRepository.DeleteItem(item.Id, CancellationToken.None); + ItemRepository.DeleteItem(item.Id); foreach (var child in children) { - ItemRepository.DeleteItem(child.Id, CancellationToken.None); + ItemRepository.DeleteItem(child.Id); } _libraryItemsCache.TryRemove(item.Id, out BaseItem removed); @@ -2364,7 +2364,7 @@ namespace Emby.Server.Implementations.Library string videoPath, string[] files) { - new SubtitleResolver(BaseItem.LocalizationManager, _fileSystem).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files); + new SubtitleResolver(BaseItem.LocalizationManager).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files); } /// <inheritdoc /> @@ -2609,14 +2609,12 @@ namespace Emby.Server.Implementations.Library }).OrderBy(i => i.Path); } - private static readonly string[] ExtrasSubfolderNames = new[] { "extras", "specials", "shorts", "scenes", "featurettes", "behind the scenes", "deleted scenes", "interviews" }; - public IEnumerable<Video> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) { var namingOptions = GetNamingOptions(); var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory) - .Where(i => ExtrasSubfolderNames.Contains(i.Name ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + .Where(i => BaseItem.AllExtrasTypesFolderNames.Contains(i.Name ?? string.Empty, StringComparer.OrdinalIgnoreCase)) .SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false)) .ToList(); diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs index 4fdf73b77..1d61ed57e 100644 --- a/Emby.Server.Implementations/Library/PathExtensions.cs +++ b/Emby.Server.Implementations/Library/PathExtensions.cs @@ -39,7 +39,7 @@ namespace Emby.Server.Implementations.Library // for imdbid we also accept pattern matching if (string.Equals(attrib, "imdbid", StringComparison.OrdinalIgnoreCase)) { - var m = Regex.Match(str, "tt\\d{7}", RegexOptions.IgnoreCase); + var m = Regex.Match(str, "tt([0-9]{7,8})", RegexOptions.IgnoreCase); return m.Success ? m.Value : null; } diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index c759e7115..dd6bd8ee8 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -8,7 +8,6 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 25d733a65..15076a194 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -264,6 +264,7 @@ namespace Emby.Server.Implementations.Library { if (string.IsNullOrWhiteSpace(username)) { + _logger.LogInformation("Authentication request without username has been denied (IP: {IP}).", remoteEndPoint); throw new ArgumentNullException(nameof(username)); } @@ -319,11 +320,13 @@ namespace Emby.Server.Implementations.Library if (user == null) { + _logger.LogInformation("Authentication request for {UserName} has been denied (IP: {IP}).", username, remoteEndPoint); throw new AuthenticationException("Invalid username or password entered."); } if (user.Policy.IsDisabled) { + _logger.LogInformation("Authentication request for {UserName} has been denied because this account is currently disabled (IP: {IP}).", username, remoteEndPoint); throw new AuthenticationException( string.Format( CultureInfo.InvariantCulture, @@ -333,11 +336,13 @@ namespace Emby.Server.Implementations.Library if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint)) { + _logger.LogInformation("Authentication request for {UserName} forbidden: remote access disabled and user not in local network (IP: {IP}).", username, remoteEndPoint); throw new AuthenticationException("Forbidden."); } if (!user.IsParentalScheduleAllowed()) { + _logger.LogInformation("Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {IP}).", username, remoteEndPoint); throw new AuthenticationException("User is not allowed access at this time."); } @@ -351,14 +356,14 @@ namespace Emby.Server.Implementations.Library } ResetInvalidLoginAttemptCount(user); + _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Name); } else { IncrementInvalidLoginAttemptCount(user); + _logger.LogInformation("Authentication request for {UserName} has been denied (IP: {IP}).", user.Name, remoteEndPoint); } - _logger.LogInformation("Authentication request for {0} {1}.", user.Name, success ? "has succeeded" : "has been denied"); - return success ? user : null; } @@ -805,17 +810,17 @@ namespace Emby.Server.Implementations.Library // Delete user config dir lock (_configSyncLock) - lock (_policySyncLock) - { - try - { - Directory.Delete(user.ConfigurationDirectoryPath, true); - } - catch (IOException ex) + lock (_policySyncLock) { - _logger.LogError(ex, "Error deleting user config dir: {Path}", user.ConfigurationDirectoryPath); + try + { + Directory.Delete(user.ConfigurationDirectoryPath, true); + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting user config dir: {Path}", user.ConfigurationDirectoryPath); + } } - } _users.TryRemove(user.Id, out _); diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs index 9c4f5fe3d..2e13a3bb3 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.IO; @@ -73,7 +72,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV UserAgent = "Emby/3.0", // Shouldn't matter but may cause issues - DecompressionMethod = CompressionMethod.None + DecompressionMethod = CompressionMethods.None }; using (var response = await _httpClient.SendAsync(httpRequestOptions, HttpMethod.Get).ConfigureAwait(false)) diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 139aa19a4..900f12062 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -25,7 +26,6 @@ using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Diagnostics; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Events; @@ -61,7 +61,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private readonly ILibraryManager _libraryManager; private readonly IProviderManager _providerManager; private readonly IMediaEncoder _mediaEncoder; - private readonly IProcessFactory _processFactory; private readonly IMediaSourceManager _mediaSourceManager; private readonly IStreamHelper _streamHelper; @@ -88,8 +87,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, IProviderManager providerManager, - IMediaEncoder mediaEncoder, - IProcessFactory processFactory) + IMediaEncoder mediaEncoder) { Current = this; @@ -102,7 +100,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _libraryMonitor = libraryMonitor; _providerManager = providerManager; _mediaEncoder = mediaEncoder; - _processFactory = processFactory; _liveTvManager = (LiveTvManager)liveTvManager; _jsonSerializer = jsonSerializer; _mediaSourceManager = mediaSourceManager; @@ -1662,7 +1659,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http)) { - return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _processFactory, _config); + return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _config); } return new DirectRecorder(_logger, _httpClient, _streamHelper); @@ -1683,16 +1680,19 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV try { - var process = _processFactory.Create(new ProcessOptions + var process = new Process { - Arguments = GetPostProcessArguments(path, options.RecordingPostProcessorArguments), - CreateNoWindow = true, - EnableRaisingEvents = true, - ErrorDialog = false, - FileName = options.RecordingPostProcessor, - IsHidden = true, - UseShellExecute = false - }); + StartInfo = new ProcessStartInfo + { + Arguments = GetPostProcessArguments(path, options.RecordingPostProcessorArguments), + CreateNoWindow = true, + ErrorDialog = false, + FileName = options.RecordingPostProcessor, + WindowStyle = ProcessWindowStyle.Hidden, + UseShellExecute = false + }, + EnableRaisingEvents = true + }; _logger.LogInformation("Running recording post processor {0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); @@ -1712,11 +1712,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private void Process_Exited(object sender, EventArgs e) { - using (var process = (IProcess)sender) + using (var process = (Process)sender) { _logger.LogInformation("Recording post-processing script completed with exit code {ExitCode}", process.ExitCode); - - process.Dispose(); } } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 8590c56df..bc86cc59a 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -1,8 +1,8 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; @@ -14,7 +14,6 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Diagnostics; using MediaBrowser.Model.Dto; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; @@ -30,8 +29,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private bool _hasExited; private Stream _logFileStream; private string _targetPath; - private IProcess _process; - private readonly IProcessFactory _processFactory; + private Process _process; private readonly IJsonSerializer _json; private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>(); private readonly IServerConfigurationManager _config; @@ -41,14 +39,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV IMediaEncoder mediaEncoder, IServerApplicationPaths appPaths, IJsonSerializer json, - IProcessFactory processFactory, IServerConfigurationManager config) { _logger = logger; _mediaEncoder = mediaEncoder; _appPaths = appPaths; _json = json; - _processFactory = processFactory; _config = config; } @@ -80,7 +76,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _targetPath = targetFile; Directory.CreateDirectory(Path.GetDirectoryName(targetFile)); - var process = _processFactory.Create(new ProcessOptions + var processStartInfo = new ProcessStartInfo { CreateNoWindow = true, UseShellExecute = false, @@ -91,14 +87,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV FileName = _mediaEncoder.EncoderPath, Arguments = GetCommandLineArgs(mediaSource, inputFile, targetFile, duration), - IsHidden = true, - ErrorDialog = false, - EnableRaisingEvents = true - }); - - _process = process; + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + }; - var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments; + var commandLineLogMessage = processStartInfo.FileName + " " + processStartInfo.Arguments; _logger.LogInformation(commandLineLogMessage); var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "record-transcode-" + Guid.NewGuid() + ".txt"); @@ -110,16 +103,21 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine); _logFileStream.Write(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length); - process.Exited += (sender, args) => OnFfMpegProcessExited(process, inputFile); + _process = new Process + { + StartInfo = processStartInfo, + EnableRaisingEvents = true + }; + _process.Exited += (sender, args) => OnFfMpegProcessExited(_process, inputFile); - process.Start(); + _process.Start(); cancellationToken.Register(Stop); onStarted(); // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback - StartStreamingLog(process.StandardError.BaseStream, _logFileStream); + StartStreamingLog(_process.StandardError.BaseStream, _logFileStream); _logger.LogInformation("ffmpeg recording process started for {0}", _targetPath); @@ -293,30 +291,33 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV /// <summary> /// Processes the exited. /// </summary> - private void OnFfMpegProcessExited(IProcess process, string inputFile) + private void OnFfMpegProcessExited(Process process, string inputFile) { - _hasExited = true; + using (process) + { + _hasExited = true; - _logFileStream?.Dispose(); - _logFileStream = null; + _logFileStream?.Dispose(); + _logFileStream = null; - var exitCode = process.ExitCode; + var exitCode = process.ExitCode; - _logger.LogInformation("FFMpeg recording exited with code {ExitCode} for {Path}", exitCode, _targetPath); + _logger.LogInformation("FFMpeg recording exited with code {ExitCode} for {Path}", exitCode, _targetPath); - if (exitCode == 0) - { - _taskCompletionSource.TrySetResult(true); - } - else - { - _taskCompletionSource.TrySetException( - new Exception( - string.Format( - CultureInfo.InvariantCulture, - "Recording for {0} failed. Exit code {1}", - _targetPath, - exitCode))); + if (exitCode == 0) + { + _taskCompletionSource.TrySetResult(true); + } + else + { + _taskCompletionSource.TrySetException( + new Exception( + string.Format( + CultureInfo.InvariantCulture, + "Recording for {0} failed. Exit code {1}", + _targetPath, + exitCode))); + } } } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs index a716b6240..69a9cb78a 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System.Threading.Tasks; using MediaBrowser.Controller.Plugins; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs index d6a1aee38..4712724d6 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Threading; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs index 6d42a58f4..fc543dc55 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs index 4cb9f6fe8..0b0ff6cb3 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Globalization; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs index 9cc53fddc..194e4606d 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using MediaBrowser.Controller.LiveTv; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs index 330e881ef..7ebb043d8 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Concurrent; diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index e9d3105bf..89b81fd96 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Concurrent; @@ -636,7 +635,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings ListingsProviderInfo providerInfo) { // Schedules direct requires that the client support compression and will return a 400 response without it - options.DecompressionMethod = CompressionMethod.Deflate; + options.DecompressionMethod = CompressionMethods.Deflate; try { @@ -666,7 +665,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings ListingsProviderInfo providerInfo) { // Schedules direct requires that the client support compression and will return a 400 response without it - options.DecompressionMethod = CompressionMethod.Deflate; + options.DecompressionMethod = CompressionMethods.Deflate; try { diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs index c159b60a9..07f8539c5 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; @@ -84,7 +83,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings { CancellationToken = cancellationToken, Url = path, - DecompressionMethod = CompressionMethod.Gzip, + DecompressionMethod = CompressionMethods.Gzip, }, HttpMethod.Get).ConfigureAwait(false)) using (var stream = res.Content) diff --git a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs b/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs index 222fed9d9..ba916af38 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System.Collections.Generic; using MediaBrowser.Common.Configuration; diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs index 14b627f82..3b36247a9 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Globalization; @@ -23,6 +22,10 @@ namespace Emby.Server.Implementations.LiveTv { public class LiveTvDtoService { + private const string InternalVersionNumber = "4"; + + private const string ServiceName = "Emby"; + private readonly ILogger _logger; private readonly IImageProcessor _imageProcessor; @@ -33,13 +36,13 @@ namespace Emby.Server.Implementations.LiveTv public LiveTvDtoService( IDtoService dtoService, IImageProcessor imageProcessor, - ILoggerFactory loggerFactory, + ILogger<LiveTvDtoService> logger, IApplicationHost appHost, ILibraryManager libraryManager) { _dtoService = dtoService; _imageProcessor = imageProcessor; - _logger = loggerFactory.CreateLogger(nameof(LiveTvDtoService)); + _logger = logger; _appHost = appHost; _libraryManager = libraryManager; } @@ -162,7 +165,6 @@ namespace Emby.Server.Implementations.LiveTv Limit = 1, ImageTypes = new ImageType[] { ImageType.Thumb }, DtoOptions = new DtoOptions(false) - }).FirstOrDefault(); if (librarySeries != null) @@ -180,6 +182,7 @@ namespace Emby.Server.Implementations.LiveTv _logger.LogError(ex, "Error"); } } + image = librarySeries.GetImageInfo(ImageType.Backdrop, 0); if (image != null) { @@ -200,13 +203,12 @@ namespace Emby.Server.Implementations.LiveTv var program = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name }, + IncludeItemTypes = new string[] { nameof(LiveTvProgram) }, ExternalSeriesId = programSeriesId, Limit = 1, ImageTypes = new ImageType[] { ImageType.Primary }, DtoOptions = new DtoOptions(false), Name = string.IsNullOrEmpty(programSeriesId) ? seriesName : null - }).FirstOrDefault(); if (program != null) @@ -233,9 +235,10 @@ namespace Emby.Server.Implementations.LiveTv try { dto.ParentBackdropImageTags = new string[] - { + { _imageProcessor.GetImageCacheTag(program, image) - }; + }; + dto.ParentBackdropItemId = program.Id.ToString("N", CultureInfo.InvariantCulture); } catch (Exception ex) @@ -256,7 +259,6 @@ namespace Emby.Server.Implementations.LiveTv Limit = 1, ImageTypes = new ImageType[] { ImageType.Thumb }, DtoOptions = new DtoOptions(false) - }).FirstOrDefault(); if (librarySeries != null) @@ -274,6 +276,7 @@ namespace Emby.Server.Implementations.LiveTv _logger.LogError(ex, "Error"); } } + image = librarySeries.GetImageInfo(ImageType.Backdrop, 0); if (image != null) { @@ -299,7 +302,6 @@ namespace Emby.Server.Implementations.LiveTv Limit = 1, ImageTypes = new ImageType[] { ImageType.Primary }, DtoOptions = new DtoOptions(false) - }).FirstOrDefault(); if (program == null) @@ -312,7 +314,6 @@ namespace Emby.Server.Implementations.LiveTv ImageTypes = new ImageType[] { ImageType.Primary }, DtoOptions = new DtoOptions(false), Name = string.IsNullOrEmpty(programSeriesId) ? seriesName : null - }).FirstOrDefault(); } @@ -397,8 +398,6 @@ namespace Emby.Server.Implementations.LiveTv return null; } - private const string InternalVersionNumber = "4"; - public Guid GetInternalChannelId(string serviceName, string externalId) { var name = serviceName + externalId + InternalVersionNumber; @@ -406,7 +405,6 @@ namespace Emby.Server.Implementations.LiveTv return _libraryManager.GetNewItemId(name.ToLowerInvariant(), typeof(LiveTvChannel)); } - private const string ServiceName = "Emby"; public string GetInternalTimerId(string externalId) { var name = ServiceName + externalId + InternalVersionNumber; diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index f20f6140e..bbc064a24 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; @@ -42,6 +41,10 @@ namespace Emby.Server.Implementations.LiveTv /// </summary> public class LiveTvManager : ILiveTvManager, IDisposable { + private const string ExternalServiceTag = "ExternalServiceId"; + + private const string EtagKey = "ProgramEtag"; + private readonly IServerConfigurationManager _config; private readonly ILogger _logger; private readonly IItemRepository _itemRepo; @@ -92,7 +95,7 @@ namespace Emby.Server.Implementations.LiveTv _userDataManager = userDataManager; _channelManager = channelManager; - _tvDtoService = new LiveTvDtoService(dtoService, imageProcessor, loggerFactory, appHost, _libraryManager); + _tvDtoService = new LiveTvDtoService(dtoService, imageProcessor, loggerFactory.CreateLogger<LiveTvDtoService>(), appHost, _libraryManager); } public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled; @@ -179,7 +182,6 @@ namespace Emby.Server.Implementations.LiveTv { Name = i.Name, Id = i.Type - }).ToList(); } @@ -262,6 +264,7 @@ namespace Emby.Server.Implementations.LiveTv var endTime = DateTime.UtcNow; _logger.LogInformation("Live stream opened after {0}ms", (endTime - startTime).TotalMilliseconds); } + info.RequiresClosing = true; var idPrefix = service.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + "_"; @@ -363,30 +366,37 @@ namespace Emby.Server.Implementations.LiveTv { stream.BitRate = null; } + if (stream.Channels.HasValue && stream.Channels <= 0) { stream.Channels = null; } + if (stream.AverageFrameRate.HasValue && stream.AverageFrameRate <= 0) { stream.AverageFrameRate = null; } + if (stream.RealFrameRate.HasValue && stream.RealFrameRate <= 0) { stream.RealFrameRate = null; } + if (stream.Width.HasValue && stream.Width <= 0) { stream.Width = null; } + if (stream.Height.HasValue && stream.Height <= 0) { stream.Height = null; } + if (stream.SampleRate.HasValue && stream.SampleRate <= 0) { stream.SampleRate = null; } + if (stream.Level.HasValue && stream.Level <= 0) { stream.Level = null; @@ -428,7 +438,6 @@ namespace Emby.Server.Implementations.LiveTv } } - private const string ExternalServiceTag = "ExternalServiceId"; private LiveTvChannel GetChannel(ChannelInfo channelInfo, string serviceName, BaseItem parentFolder, CancellationToken cancellationToken) { var parentFolderId = parentFolder.Id; @@ -457,6 +466,7 @@ namespace Emby.Server.Implementations.LiveTv { isNew = true; } + item.Tags = channelInfo.Tags; } @@ -464,6 +474,7 @@ namespace Emby.Server.Implementations.LiveTv { isNew = true; } + item.ParentId = parentFolderId; item.ChannelType = channelInfo.ChannelType; @@ -473,24 +484,28 @@ namespace Emby.Server.Implementations.LiveTv { forceUpdate = true; } + item.SetProviderId(ExternalServiceTag, serviceName); if (!string.Equals(channelInfo.Id, item.ExternalId, StringComparison.Ordinal)) { forceUpdate = true; } + item.ExternalId = channelInfo.Id; if (!string.Equals(channelInfo.Number, item.Number, StringComparison.Ordinal)) { forceUpdate = true; } + item.Number = channelInfo.Number; if (!string.Equals(channelInfo.Name, item.Name, StringComparison.Ordinal)) { forceUpdate = true; } + item.Name = channelInfo.Name; if (!item.HasImage(ImageType.Primary)) @@ -519,8 +534,6 @@ namespace Emby.Server.Implementations.LiveTv return item; } - private const string EtagKey = "ProgramEtag"; - private Tuple<LiveTvProgram, bool, bool> GetProgram(ProgramInfo info, Dictionary<Guid, LiveTvProgram> allExistingPrograms, LiveTvChannel channel, ChannelType channelType, string serviceName, CancellationToken cancellationToken) { var id = _tvDtoService.GetInternalProgramId(info.Id); diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs index 33887bbfd..7f63991d0 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs index 419ec3635..80ee1ee33 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Concurrent; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index a2d972d19..25b2c674c 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index 56864ab11..57c5b7500 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Buffers; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index 77669da39..03ee5bfb6 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; @@ -8,8 +7,8 @@ using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Net; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs index 5354489f9..4e4f1d7f6 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; @@ -8,8 +7,8 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Controller.Library; using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 46c77e7b0..f5dda79db 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index 511af150b..59451fccd 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index 861518387..d63588bbd 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System; using System.Collections.Generic; @@ -60,7 +59,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts Url = url, CancellationToken = CancellationToken.None, BufferContent = false, - DecompressionMethod = CompressionMethod.None + DecompressionMethod = CompressionMethods.None }; foreach (var header in mediaSource.RequiredHttpHeaders) diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json index dcec26801..1363eaf85 100644 --- a/Emby.Server.Implementations/Localization/Core/af.json +++ b/Emby.Server.Implementations/Localization/Core/af.json @@ -41,7 +41,6 @@ "User": "Gebruiker", "TvShows": "TV Programme", "System": "Stelsel", - "SubtitlesDownloadedForItem": "Ondertitels afgelaai vir {0}", "SubtitleDownloadFailureFromForItem": "Ondertitels het misluk om af te laai van {0} vir {1}", "StartupEmbyServerIsLoading": "Jellyfin Bediener is besig om te laai. Probeer weer in 'n kort tyd.", "ServerNameNeedsToBeRestarted": "{0} moet herbegin word", diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index 24da1aa56..f313039a6 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -1,13 +1,13 @@ { "Albums": "ألبومات", "AppDeviceValues": "تطبيق: {0}, جهاز: {1}", - "Application": "التطبيق", + "Application": "تطبيق", "Artists": "الفنانين", "AuthenticationSucceededWithUserName": "{0} سجل الدخول بنجاح", - "Books": "كتب", + "Books": "الكتب", "CameraImageUploadedFrom": "صورة كاميرا جديدة تم رفعها من {0}", "Channels": "القنوات", - "ChapterNameValue": "فصل {0}", + "ChapterNameValue": "الفصل {0}", "Collections": "مجموعات", "DeviceOfflineWithName": "قُطِع الاتصال بـ{0}", "DeviceOnlineWithName": "{0} متصل", @@ -51,8 +51,8 @@ "NotificationOptionAudioPlaybackStopped": "تم إيقاف تشغيل المقطع الصوتي", "NotificationOptionCameraImageUploaded": "تم رفع صورة الكاميرا", "NotificationOptionInstallationFailed": "فشل في التثبيت", - "NotificationOptionNewLibraryContent": "أُضِيفَ محتوى جديد", - "NotificationOptionPluginError": "فشل في الـPlugin", + "NotificationOptionNewLibraryContent": "تم إضافة محتوى جديد", + "NotificationOptionPluginError": "فشل في البرنامج المضاف", "NotificationOptionPluginInstalled": "تم تثبيت الملحق", "NotificationOptionPluginUninstalled": "تمت إزالة الملحق", "NotificationOptionPluginUpdateInstalled": "تم تثبيت تحديثات الملحق", @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "سيرفر Jellyfin قيد التشغيل . الرجاء المحاولة بعد قليل.", "SubtitleDownloadFailureForItem": "عملية إنزال الترجمة فشلت لـ{0}", "SubtitleDownloadFailureFromForItem": "الترجمات فشلت في التحميل من {0} الى {1}", - "SubtitlesDownloadedForItem": "تم تحميل الترجمات الى {0}", "Sync": "مزامنة", "System": "النظام", "TvShows": "البرامج التلفزيونية", @@ -91,7 +90,29 @@ "UserPolicyUpdatedWithName": "تم تحديث سياسة المستخدم {0}", "UserStartedPlayingItemWithValues": "قام {0} ببدء تشغيل {1} على {2}", "UserStoppedPlayingItemWithValues": "قام {0} بإيقاف تشغيل {1} على {2}", - "ValueHasBeenAddedToLibrary": "{0} تم اضافتها الى مكتبة الوسائط", - "ValueSpecialEpisodeName": "مميز - {0}", - "VersionNumber": "الإصدار رقم {0}" + "ValueHasBeenAddedToLibrary": "تمت اضافت {0} إلى مكتبة الوسائط", + "ValueSpecialEpisodeName": "خاص - {0}", + "VersionNumber": "النسخة {0}", + "TaskCleanCacheDescription": "يحذف ملفات ذاكرة التخزين المؤقت التي لم يعد النظام بحاجة إليها.", + "TaskCleanCache": "احذف مجلد ذاكرة التخزين المؤقت", + "TasksChannelsCategory": "قنوات الإنترنت", + "TasksLibraryCategory": "مكتبة", + "TasksMaintenanceCategory": "صيانة", + "TaskRefreshLibraryDescription": "يقوم بفصح مكتبة الوسائط الخاصة بك بحثًا عن ملفات جديدة وتحديث البيانات الوصفية.", + "TaskRefreshLibrary": "افحص مكتبة الوسائط", + "TaskRefreshChapterImagesDescription": "إنشاء صور مصغرة لمقاطع الفيديو ذات فصول.", + "TaskRefreshChapterImages": "استخراج صور الفصل", + "TasksApplicationCategory": "تطبيق", + "TaskDownloadMissingSubtitlesDescription": "ابحث في الإنترنت على الترجمات المفقودة إستنادا على الميتاداتا.", + "TaskDownloadMissingSubtitles": "تحميل الترجمات المفقودة", + "TaskRefreshChannelsDescription": "تحديث معلومات قنوات الإنترنت.", + "TaskRefreshChannels": "إعادة تحديث القنوات", + "TaskCleanTranscodeDescription": "حذف ملفات الترميز الأقدم من يوم واحد.", + "TaskCleanTranscode": "حذف سجلات الترميز", + "TaskUpdatePluginsDescription": "تحميل وتثبيت الإضافات التي تم تفعيل التحديث التلقائي لها.", + "TaskUpdatePlugins": "تحديث الإضافات", + "TaskRefreshPeopleDescription": "تحديث البيانات الوصفية للممثلين والمخرجين في مكتبة الوسائط الخاصة بك.", + "TaskRefreshPeople": "إعادة تحميل الأشخاص", + "TaskCleanLogsDescription": "حذف السجلات الأقدم من {0} يوم.", + "TaskCleanLogs": "حذف دليل السجل" } diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json index 9441da591..3fc7c7dc0 100644 --- a/Emby.Server.Implementations/Localization/Core/bg-BG.json +++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json @@ -1,8 +1,8 @@ { "Albums": "Албуми", - "AppDeviceValues": "Програма: {0}, устройство: {1}", + "AppDeviceValues": "Програма: {0}, Устройство: {1}", "Application": "Програма", - "Artists": "Изпълнители", + "Artists": "Артисти", "AuthenticationSucceededWithUserName": "{0} се удостовери успешно", "Books": "Книги", "CameraImageUploadedFrom": "Нова снимка от камера беше качена от {0}", @@ -31,20 +31,20 @@ "ItemAddedWithName": "{0} е добавено към библиотеката", "ItemRemovedWithName": "{0} е премахнато от библиотеката", "LabelIpAddressValue": "ИП адрес: {0}", - "LabelRunningTimeValue": "", + "LabelRunningTimeValue": "Стартирано от: {0}", "Latest": "Последни", "MessageApplicationUpdated": "Сървърът е обновен", "MessageApplicationUpdatedTo": "Сървърът е обновен до {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "", - "MessageServerConfigurationUpdated": "", + "MessageNamedServerConfigurationUpdatedWithValue": "Секцията {0} от сървърната конфигурация се актуализира", + "MessageServerConfigurationUpdated": "Конфигурацията на сървъра се актуализира", "MixedContent": "Смесено съдържание", "Movies": "Филми", "Music": "Музика", "MusicVideos": "Музикални клипове", - "NameInstallFailed": "", + "NameInstallFailed": "{0} не можа да се инсталира", "NameSeasonNumber": "Сезон {0}", "NameSeasonUnknown": "Неразпознат сезон", - "NewVersionIsAvailable": "", + "NewVersionIsAvailable": "Нова версия на Jellyfin сървъра е достъпна за сваляне.", "NotificationOptionApplicationUpdateAvailable": "Налично е обновление на програмата", "NotificationOptionApplicationUpdateInstalled": "Обновлението на програмата е инсталирано", "NotificationOptionAudioPlayback": "Възпроизвеждането на звук започна", @@ -58,7 +58,7 @@ "NotificationOptionPluginUpdateInstalled": "Обновлението на приставката е инсталирано", "NotificationOptionServerRestartRequired": "Нужно е повторно пускане на сървъра", "NotificationOptionTaskFailed": "Грешка в планирана задача", - "NotificationOptionUserLockedOut": "", + "NotificationOptionUserLockedOut": "Потребителя е заключен", "NotificationOptionVideoPlayback": "Възпроизвеждането на видео започна", "NotificationOptionVideoPlaybackStopped": "Възпроизвеждането на видео е спряно", "Photos": "Снимки", @@ -70,28 +70,49 @@ "ProviderValue": "Доставчик: {0}", "ScheduledTaskFailedWithName": "{0} се провали", "ScheduledTaskStartedWithName": "{0} започна", - "ServerNameNeedsToBeRestarted": "", + "ServerNameNeedsToBeRestarted": "{0} е нужно да се рестартира", "Shows": "Сериали", "Songs": "Песни", "StartupEmbyServerIsLoading": "Сървърът зарежда. Моля, опитайте отново след малко.", "SubtitleDownloadFailureForItem": "Неуспешно изтегляне на субтитри за {0}", - "SubtitleDownloadFailureFromForItem": "", - "SubtitlesDownloadedForItem": "Изтеглени са субтитри за {0}", + "SubtitleDownloadFailureFromForItem": "Поднадписите за {1} от {0} не можаха да се изтеглят", "Sync": "Синхронизиране", "System": "Система", "TvShows": "Телевизионни сериали", "User": "Потребител", "UserCreatedWithName": "Потребителят {0} е създаден", "UserDeletedWithName": "Потребителят {0} е изтрит", - "UserDownloadingItemWithValues": "", - "UserLockedOutWithName": "", + "UserDownloadingItemWithValues": "{0} изтегля {1}", + "UserLockedOutWithName": "Потребител {0} се заключи", "UserOfflineFromDevice": "{0} се разкачи от {1}", "UserOnlineFromDevice": "{0} е на линия от {1}", "UserPasswordChangedWithName": "Паролата на потребителя {0} е променена", - "UserPolicyUpdatedWithName": "", + "UserPolicyUpdatedWithName": "Потребителската политика за {0} се актуализира", "UserStartedPlayingItemWithValues": "{0} пусна {1}", "UserStoppedPlayingItemWithValues": "{0} спря {1}", - "ValueHasBeenAddedToLibrary": "", + "ValueHasBeenAddedToLibrary": "{0} беше добавен във Вашата библиотека", "ValueSpecialEpisodeName": "Специални - {0}", - "VersionNumber": "Версия {0}" + "VersionNumber": "Версия {0}", + "TaskDownloadMissingSubtitlesDescription": "Търси Интернет за липсващи поднадписи, на база конфигурацията за мета-данни.", + "TaskDownloadMissingSubtitles": "Изтегляне на липсващи поднадписи", + "TaskRefreshChannelsDescription": "Обновява информацията за интернет канала.", + "TaskRefreshChannels": "Обновяване на Канали", + "TaskCleanTranscodeDescription": "Изтрива прекодирани файлове по-стари от един ден.", + "TaskCleanTranscode": "Изчиства директорията за прекодиране", + "TaskUpdatePluginsDescription": "Изтегля и инсталира актуализации за добавките, които са настроени за автоматична актуализация.", + "TaskUpdatePlugins": "Актуализира добавките", + "TaskRefreshPeopleDescription": "Актуализира мета-данните за артистите и режисьорите за Вашата медийна библиотека.", + "TaskRefreshPeople": "Обновяване на участниците", + "TaskCleanLogsDescription": "Изтрива лог файлове по-стари от {0} дни.", + "TaskCleanLogs": "Изчисти директорията с логове", + "TaskRefreshLibraryDescription": "Сканира Вашата библиотека с медия за нови файлове и обновява мета-данните.", + "TaskRefreshLibrary": "Сканиране на библиотеката с медия", + "TaskRefreshChapterImagesDescription": "Създава иконки за видеа, които имат епизоди.", + "TaskRefreshChapterImages": "Извличане на изображения за епизода", + "TaskCleanCacheDescription": "Изтриване на ненужните от системата файлове.", + "TaskCleanCache": "Изчистване на Кеш-директорията", + "TasksChannelsCategory": "Интернет Канали", + "TasksApplicationCategory": "Приложение", + "TasksLibraryCategory": "Библиотека", + "TasksMaintenanceCategory": "Поддръжка" } diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json index a7219a725..ef7792356 100644 --- a/Emby.Server.Implementations/Localization/Core/bn.json +++ b/Emby.Server.Implementations/Localization/Core/bn.json @@ -38,7 +38,6 @@ "TvShows": "টিভি শোগুলো", "System": "সিস্টেম", "Sync": "সিংক", - "SubtitlesDownloadedForItem": "{0} এর জন্য সাবটাইটেল ডাউনলোড করা হয়েছে", "SubtitleDownloadFailureFromForItem": "{2} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ", "StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।", "Songs": "গানগুলো", diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 44e7cf0ce..7464ac1c0 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -3,19 +3,19 @@ "AppDeviceValues": "Aplicació: {0}, Dispositiu: {1}", "Application": "Aplicació", "Artists": "Artistes", - "AuthenticationSucceededWithUserName": "{0} s'ha autentificat correctament", + "AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament", "Books": "Llibres", - "CameraImageUploadedFrom": "Una nova imatge de la càmera ha sigut pujada des de {0}", + "CameraImageUploadedFrom": "Una nova imatge de la càmera ha estat pujada des de {0}", "Channels": "Canals", - "ChapterNameValue": "Episodi {0}", + "ChapterNameValue": "Capítol {0}", "Collections": "Col·leccions", "DeviceOfflineWithName": "{0} s'ha desconnectat", "DeviceOnlineWithName": "{0} està connectat", "FailedLoginAttemptWithUserName": "Intent de connexió fallit des de {0}", "Favorites": "Preferits", - "Folders": "Directoris", + "Folders": "Carpetes", "Genres": "Gèneres", - "HeaderAlbumArtists": "Artistes dels Àlbums", + "HeaderAlbumArtists": "Artistes del Àlbum", "HeaderCameraUploads": "Pujades de Càmera", "HeaderContinueWatching": "Continua Veient", "HeaderFavoriteAlbums": "Àlbums Preferits", @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "El Servidor d'Jellyfin està carregant. Si et plau, prova de nou en breus.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Els subtítols no s'han pogut baixar de {0} per {1}", - "SubtitlesDownloadedForItem": "Subtítols descarregats per a {0}", "Sync": "Sincronitzar", "System": "System", "TvShows": "Espectacles de TV", diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index 86fbac380..992bb9df3 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -5,7 +5,7 @@ "Artists": "Umělci", "AuthenticationSucceededWithUserName": "{0} úspěšně ověřen", "Books": "Knihy", - "CameraImageUploadedFrom": "Z {0} byla nahrána nová fotografie", + "CameraImageUploadedFrom": "Z {0} byla nahrána nová fotografie z fotoaparátu", "Channels": "Kanály", "ChapterNameValue": "Kapitola {0}", "Collections": "Kolekce", @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "Jellyfin Server je spouštěn. Zkuste to prosím v brzké době znovu.", "SubtitleDownloadFailureForItem": "Stahování titulků selhalo pro {0}", "SubtitleDownloadFailureFromForItem": "Stažení titulků pro {1} z {0} selhalo", - "SubtitlesDownloadedForItem": "Staženy titulky pro {0}", "Sync": "Synchronizace", "System": "Systém", "TvShows": "TV seriály", @@ -93,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} zastavil přehrávání {1}", "ValueHasBeenAddedToLibrary": "{0} byl přidán do vaší knihovny médií", "ValueSpecialEpisodeName": "Speciál - {0}", - "VersionNumber": "Verze {0}" + "VersionNumber": "Verze {0}", + "TaskDownloadMissingSubtitlesDescription": "Vyhledá na internetu chybějící titulky na základě nastavení metadat.", + "TaskDownloadMissingSubtitles": "Stáhnout chybějící titulky", + "TaskRefreshChannelsDescription": "Obnoví informace o internetových kanálech.", + "TaskRefreshChannels": "Obnovit kanály", + "TaskCleanTranscodeDescription": "Odstraní více než 1 den staré transkódované soubory.", + "TaskCleanTranscode": "Vyčistit adresář s transkódovaným obsahem", + "TaskUpdatePluginsDescription": "Stáhne a nainstaluje aktualizace zásuvných modulů, které mají nastavenou automatickou aktualizaci.", + "TaskUpdatePlugins": "Aktualizovat zásuvné moduly", + "TaskRefreshPeopleDescription": "Aktualizuje metadata umělců a režisérů ve Vaší knihovně médií.", + "TaskRefreshPeople": "Obnovit umělce", + "TaskCleanLogsDescription": "Odstraní soubory protokolu, které jsou starší více než {0} dní.", + "TaskCleanLogs": "Vyčistit adresář se souborem protokolu", + "TaskRefreshLibraryDescription": "Prohledá Vaši knihovnu médií zda neobsahuje nové soubory a obnoví metadatada.", + "TaskRefreshLibrary": "Prohledat knihovnu médií", + "TaskRefreshChapterImagesDescription": "Vytvoří náhledy videí, které obsahují kapitoly.", + "TaskRefreshChapterImages": "Extrahovat obrázky kapitol", + "TaskCleanCacheDescription": "Odstraní soubory mezipaměti, které systém již nebude potřebovat.", + "TaskCleanCache": "Vyčistit složku s mezipamětí", + "TasksChannelsCategory": "Internetové kanály", + "TasksApplicationCategory": "Aplikace", + "TasksLibraryCategory": "Knihovna", + "TasksMaintenanceCategory": "Údržba" } diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json index c421db87d..f5397b62c 100644 --- a/Emby.Server.Implementations/Localization/Core/da.json +++ b/Emby.Server.Implementations/Localization/Core/da.json @@ -1,5 +1,5 @@ { - "Albums": "Album", + "Albums": "Albums", "AppDeviceValues": "App: {0}, Enhed: {1}", "Application": "Applikation", "Artists": "Kunstnere", @@ -35,8 +35,8 @@ "Latest": "Seneste", "MessageApplicationUpdated": "Jellyfin Server er blevet opdateret", "MessageApplicationUpdatedTo": "Jellyfin Server er blevet opdateret til {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfigurationsafsnit {0} er blevet opdateret", - "MessageServerConfigurationUpdated": "Serverkonfigurationen er blevet opdateret", + "MessageNamedServerConfigurationUpdatedWithValue": "Server konfiguration sektion {0} er blevet opdateret", + "MessageServerConfigurationUpdated": "Server konfigurationen er blevet opdateret", "MixedContent": "Blandet indhold", "Movies": "Film", "Music": "Musik", @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "Jellyfin Server er i gang med at starte op. Prøv venligst igen om lidt.", "SubtitleDownloadFailureForItem": "Fejlet i download af undertekster for {0}", "SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke downloades fra {0} til {1}", - "SubtitlesDownloadedForItem": "Undertekster downloadet for {0}", "Sync": "Synk", "System": "System", "TvShows": "TV serier", @@ -93,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} har afsluttet afspilning af {1} på {2}", "ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek", "ValueSpecialEpisodeName": "Special - {0}", - "VersionNumber": "Version {0}" + "VersionNumber": "Version {0}", + "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfiguration.", + "TaskDownloadMissingSubtitles": "Download manglende undertekster", + "TaskUpdatePluginsDescription": "Downloader og installere opdateringer for plugins som er konfigureret til at opdatere automatisk.", + "TaskUpdatePlugins": "Opdater Plugins", + "TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gammle.", + "TaskCleanLogs": "Ryd Log Mappe", + "TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdaterer metadata.", + "TaskRefreshLibrary": "Scan Medie Bibliotek", + "TaskCleanCacheDescription": "Sletter cache filer som systemet ikke har brug for længere.", + "TaskCleanCache": "Ryd Cache Mappe", + "TasksChannelsCategory": "Internet Kanaler", + "TasksApplicationCategory": "Applikation", + "TasksLibraryCategory": "Bibliotek", + "TasksMaintenanceCategory": "Vedligeholdelse", + "TaskRefreshChapterImages": "Udtræk Kapitel billeder", + "TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.", + "TaskRefreshChannelsDescription": "Genopfrisker internet kanal information.", + "TaskRefreshChannels": "Genopfrisk Kanaler", + "TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end en dag gammel.", + "TaskCleanTranscode": "Rengør Transcode Mappen", + "TaskRefreshPeople": "Genopfrisk Personer", + "TaskRefreshPeopleDescription": "Opdatere metadata for skuespillere og instruktører i dit bibliotek." } diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index c00d61aa5..82df43be1 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -1,9 +1,9 @@ { "Albums": "Alben", - "AppDeviceValues": "Anw: {0}, Gerät: {1}", + "AppDeviceValues": "App: {0}, Gerät: {1}", "Application": "Anwendung", "Artists": "Interpreten", - "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich angemeldet", + "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich authentifiziert", "Books": "Bücher", "CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen", "Channels": "Kanäle", @@ -50,7 +50,7 @@ "NotificationOptionAudioPlayback": "Audiowiedergabe gestartet", "NotificationOptionAudioPlaybackStopped": "Audiowiedergabe gestoppt", "NotificationOptionCameraImageUploaded": "Foto hochgeladen", - "NotificationOptionInstallationFailed": "Fehler bei der Installation", + "NotificationOptionInstallationFailed": "Installation fehlgeschlagen", "NotificationOptionNewLibraryContent": "Neuer Inhalt hinzugefügt", "NotificationOptionPluginError": "Plugin-Fehler", "NotificationOptionPluginInstalled": "Plugin installiert", @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "Jellyfin-Server startet, bitte versuche es gleich noch einmal.", "SubtitleDownloadFailureForItem": "Download der Untertitel fehlgeschlagen für {0}", "SubtitleDownloadFailureFromForItem": "Untertitel von {0} für {1} konnten nicht heruntergeladen werden", - "SubtitlesDownloadedForItem": "Untertitel heruntergeladen für {0}", "Sync": "Synchronisation", "System": "System", "TvShows": "TV-Sendungen", @@ -93,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} beendet", "ValueHasBeenAddedToLibrary": "{0} wurde deiner Bibliothek hinzugefügt", "ValueSpecialEpisodeName": "Extra - {0}", - "VersionNumber": "Version {0}" + "VersionNumber": "Version {0}", + "TaskDownloadMissingSubtitlesDescription": "Durchsucht das Internet nach fehlenden Untertiteln, basierend auf den Meta Einstellungen.", + "TaskDownloadMissingSubtitles": "Lade fehlende Untertitel herunter", + "TaskRefreshChannelsDescription": "Erneuere Internet Kanal Informationen.", + "TaskRefreshChannels": "Erneuere Kanäle", + "TaskCleanTranscodeDescription": "Löscht Transkodierdateien welche älter als ein Tag sind.", + "TaskCleanTranscode": "Lösche Transkodier Pfad", + "TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.", + "TaskUpdatePlugins": "Update Plugins", + "TaskRefreshPeopleDescription": "Erneuert Metadaten für Schausteller und Regisseure in deinen Bibliotheken.", + "TaskRefreshPeople": "Erneuere Schausteller", + "TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.", + "TaskCleanLogs": "Lösche Log Pfad", + "TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.", + "TaskRefreshLibrary": "Scanne alle Bibliotheken", + "TaskRefreshChapterImagesDescription": "Kreiert Vorschaubilder für Videos welche Kapitel haben.", + "TaskRefreshChapterImages": "Extrahiert Kapitel-Bilder", + "TaskCleanCacheDescription": "Löscht Zwischenspeicherdatein die nicht länger von System gebraucht werden.", + "TaskCleanCache": "Leere Cache Pfad", + "TasksChannelsCategory": "Internet Kanäle", + "TasksApplicationCategory": "Anwendung", + "TasksLibraryCategory": "Bibliothek", + "TasksMaintenanceCategory": "Wartung" } diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index 580b42330..53e2f58de 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "Ο Jellyfin Server φορτώνει. Παρακαλώ δοκιμάστε σε λίγο.", "SubtitleDownloadFailureForItem": "Οι υπότιτλοι απέτυχαν να κατέβουν για {0}", "SubtitleDownloadFailureFromForItem": "Αποτυχίες μεταφόρτωσης υποτίτλων από {0} για {1}", - "SubtitlesDownloadedForItem": "Οι υπότιτλοι κατέβηκαν για {0}", "Sync": "Συγχρονισμός", "System": "Σύστημα", "TvShows": "Τηλεοπτικές Σειρές", diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json index 67d4068cf..544c38cfa 100644 --- a/Emby.Server.Implementations/Localization/Core/en-GB.json +++ b/Emby.Server.Implementations/Localization/Core/en-GB.json @@ -76,7 +76,6 @@ "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", "System": "System", "TvShows": "TV Shows", @@ -93,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", "ValueSpecialEpisodeName": "Special - {0}", - "VersionNumber": "Version {0}" + "VersionNumber": "Version {0}", + "TaskDownloadMissingSubtitlesDescription": "Searches the internet for missing subtitles based on metadata configuration.", + "TaskDownloadMissingSubtitles": "Download missing subtitles", + "TaskRefreshChannelsDescription": "Refreshes internet channel information.", + "TaskRefreshChannels": "Refresh Channels", + "TaskCleanTranscodeDescription": "Deletes transcode files more than one day old.", + "TaskCleanTranscode": "Clean Transcode Directory", + "TaskUpdatePluginsDescription": "Downloads and installs updates for plugins that are configured to update automatically.", + "TaskUpdatePlugins": "Update Plugins", + "TaskRefreshPeopleDescription": "Updates metadata for actors and directors in your media library.", + "TaskRefreshPeople": "Refresh People", + "TaskCleanLogsDescription": "Deletes log files that are more than {0} days old.", + "TaskCleanLogs": "Clean Log Directory", + "TaskRefreshLibraryDescription": "Scans your media library for new files and refreshes metadata.", + "TaskRefreshLibrary": "Scan Media Library", + "TaskRefreshChapterImagesDescription": "Creates thumbnails for videos that have chapters.", + "TaskRefreshChapterImages": "Extract Chapter Images", + "TaskCleanCacheDescription": "Deletes cache files no longer needed by the system.", + "TaskCleanCache": "Clean Cache Directory", + "TasksChannelsCategory": "Internet Channels", + "TasksApplicationCategory": "Application", + "TasksLibraryCategory": "Library", + "TasksMaintenanceCategory": "Maintenance" } diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json index aa855ed21..97a843160 100644 --- a/Emby.Server.Implementations/Localization/Core/en-US.json +++ b/Emby.Server.Implementations/Localization/Core/en-US.json @@ -75,7 +75,6 @@ "Songs": "Songs", "StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.", "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", - "SubtitlesDownloadedForItem": "Subtitles downloaded for {0}", "Sync": "Sync", "System": "System", "TvShows": "TV Shows", @@ -92,5 +91,27 @@ "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", "ValueSpecialEpisodeName": "Special - {0}", - "VersionNumber": "Version {0}" + "VersionNumber": "Version {0}", + "TasksMaintenanceCategory": "Maintenance", + "TasksLibraryCategory": "Library", + "TasksApplicationCategory": "Application", + "TasksChannelsCategory": "Internet Channels", + "TaskCleanCache": "Clean Cache Directory", + "TaskCleanCacheDescription": "Deletes cache files no longer needed by the system.", + "TaskRefreshChapterImages": "Extract Chapter Images", + "TaskRefreshChapterImagesDescription": "Creates thumbnails for videos that have chapters.", + "TaskRefreshLibrary": "Scan Media Library", + "TaskRefreshLibraryDescription": "Scans your media library for new files and refreshes metadata.", + "TaskCleanLogs": "Clean Log Directory", + "TaskCleanLogsDescription": "Deletes log files that are more than {0} days old.", + "TaskRefreshPeople": "Refresh People", + "TaskRefreshPeopleDescription": "Updates metadata for actors and directors in your media library.", + "TaskUpdatePlugins": "Update Plugins", + "TaskUpdatePluginsDescription": "Downloads and installs updates for plugins that are configured to update automatically.", + "TaskCleanTranscode": "Clean Transcode Directory", + "TaskCleanTranscodeDescription": "Deletes transcode files more than one day old.", + "TaskRefreshChannels": "Refresh Channels", + "TaskRefreshChannelsDescription": "Refreshes internet channel information.", + "TaskDownloadMissingSubtitles": "Download missing subtitles", + "TaskDownloadMissingSubtitlesDescription": "Searches the internet for missing subtitles based on metadata configuration." } diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json index dc73ba6b3..1b6c6b5ae 100644 --- a/Emby.Server.Implementations/Localization/Core/es-AR.json +++ b/Emby.Server.Implementations/Localization/Core/es-AR.json @@ -11,20 +11,20 @@ "Collections": "Colecciones", "DeviceOfflineWithName": "{0} se ha desconectado", "DeviceOnlineWithName": "{0} está conectado", - "FailedLoginAttemptWithUserName": "Error al intentar iniciar sesión desde {0}", + "FailedLoginAttemptWithUserName": "Error al intentar iniciar sesión de {0}", "Favorites": "Favoritos", "Folders": "Carpetas", "Genres": "Géneros", - "HeaderAlbumArtists": "Artistas de álbumes", + "HeaderAlbumArtists": "Artistas de álbum", "HeaderCameraUploads": "Subidas de cámara", - "HeaderContinueWatching": "Continuar viendo", + "HeaderContinueWatching": "Seguir viendo", "HeaderFavoriteAlbums": "Álbumes favoritos", "HeaderFavoriteArtists": "Artistas favoritos", "HeaderFavoriteEpisodes": "Episodios favoritos", "HeaderFavoriteShows": "Programas favoritos", "HeaderFavoriteSongs": "Canciones favoritas", "HeaderLiveTV": "TV en vivo", - "HeaderNextUp": "Continuar Viendo", + "HeaderNextUp": "A Continuación", "HeaderRecordingGroups": "Grupos de grabación", "HomeVideos": "Videos caseros", "Inherit": "Heredar", @@ -35,48 +35,47 @@ "Latest": "Últimos", "MessageApplicationUpdated": "El servidor Jellyfin fue actualizado", "MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Fue actualizada la sección {0} de la configuración del servidor", - "MessageServerConfigurationUpdated": "Fue actualizada la configuración del servidor", - "MixedContent": "Contenido mixto", + "MessageNamedServerConfigurationUpdatedWithValue": "Se ha actualizado la sección {0} de la configuración del servidor", + "MessageServerConfigurationUpdated": "Se ha actualizado la configuración del servidor", + "MixedContent": "Contenido mezclado", "Movies": "Películas", "Music": "Música", "MusicVideos": "Videos musicales", - "NameInstallFailed": "{0} error de instalación", + "NameInstallFailed": "{0} instalación fallida", "NameSeasonNumber": "Temporada {0}", "NameSeasonUnknown": "Temporada desconocida", - "NewVersionIsAvailable": "Disponible una nueva versión de Jellyfin para descargar.", + "NewVersionIsAvailable": "Una nueva versión del Servidor Jellyfin está disponible para descargar.", "NotificationOptionApplicationUpdateAvailable": "Actualización de la aplicación disponible", "NotificationOptionApplicationUpdateInstalled": "Actualización de la aplicación instalada", "NotificationOptionAudioPlayback": "Se inició la reproducción de audio", "NotificationOptionAudioPlaybackStopped": "Se detuvo la reproducción de audio", - "NotificationOptionCameraImageUploaded": "Imagen de la cámara cargada", + "NotificationOptionCameraImageUploaded": "Imagen de la cámara subida", "NotificationOptionInstallationFailed": "Error de instalación", "NotificationOptionNewLibraryContent": "Nuevo contenido añadido", - "NotificationOptionPluginError": "Error en plugin", - "NotificationOptionPluginInstalled": "Plugin instalado", - "NotificationOptionPluginUninstalled": "Plugin desinstalado", - "NotificationOptionPluginUpdateInstalled": "Actualización del complemento instalada", - "NotificationOptionServerRestartRequired": "Se requiere reinicio del servidor", - "NotificationOptionTaskFailed": "Error de tarea programada", + "NotificationOptionPluginError": "Falla de complemento", + "NotificationOptionPluginInstalled": "Complemento instalado", + "NotificationOptionPluginUninstalled": "Complemento desinstalado", + "NotificationOptionPluginUpdateInstalled": "Actualización de complemento instalada", + "NotificationOptionServerRestartRequired": "Se necesita reiniciar el Servidor", + "NotificationOptionTaskFailed": "Falla de tarea programada", "NotificationOptionUserLockedOut": "Usuario bloqueado", "NotificationOptionVideoPlayback": "Se inició la reproducción de video", "NotificationOptionVideoPlaybackStopped": "Reproducción de video detenida", "Photos": "Fotos", "Playlists": "Listas de reproducción", - "Plugin": "Plugin", + "Plugin": "Complemento", "PluginInstalledWithName": "{0} fue instalado", "PluginUninstalledWithName": "{0} fue desinstalado", "PluginUpdatedWithName": "{0} fue actualizado", "ProviderValue": "Proveedor: {0}", "ScheduledTaskFailedWithName": "{0} falló", - "ScheduledTaskStartedWithName": "{0} iniciada", + "ScheduledTaskStartedWithName": "{0} iniciado", "ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado", "Shows": "Series", "Songs": "Canciones", - "StartupEmbyServerIsLoading": "Jellyfin Server se está cargando. Vuelve a intentarlo en breve.", + "StartupEmbyServerIsLoading": "El servidor Jellyfin se está cargando. Vuelve a intentarlo en breve.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", - "SubtitleDownloadFailureFromForItem": "Fallo de descarga de subtítulos desde {0} para {1}", - "SubtitlesDownloadedForItem": "Descargar subtítulos para {0}", + "SubtitleDownloadFailureFromForItem": "Falló la descarga de subtitulos desde {0} para {1}", "Sync": "Sincronizar", "System": "Sistema", "TvShows": "Series de TV", @@ -88,10 +87,32 @@ "UserOfflineFromDevice": "{0} se ha desconectado de {1}", "UserOnlineFromDevice": "{0} está en línea desde {1}", "UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}", - "UserPolicyUpdatedWithName": "Actualizada política de usuario para {0}", + "UserPolicyUpdatedWithName": "Las política de usuario ha sido actualizada para {0}", "UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}", "UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}", "ValueHasBeenAddedToLibrary": "{0} ha sido añadido a tu biblioteca multimedia", "ValueSpecialEpisodeName": "Especial - {0}", - "VersionNumber": "Versión {0}" + "VersionNumber": "Versión {0}", + "TaskDownloadMissingSubtitlesDescription": "Busca en internet los subtítulos que falten basándose en la configuración de los metadatos.", + "TaskDownloadMissingSubtitles": "Descargar subtítulos extraviados", + "TaskRefreshChannelsDescription": "Actualizar información de canales de internet.", + "TaskRefreshChannels": "Actualizar canales", + "TaskCleanTranscodeDescription": "Eliminar archivos transcodificados con mas de un día de antigüedad.", + "TaskCleanTranscode": "Limpiar directorio de Transcodificado", + "TaskUpdatePluginsDescription": "Descargar e instalar actualizaciones para complementos que estén configurados en actualizar automáticamente.", + "TaskUpdatePlugins": "Actualizar complementos", + "TaskRefreshPeopleDescription": "Actualizar metadatos de actores y directores en su librería multimedia.", + "TaskRefreshPeople": "Actualizar personas", + "TaskCleanLogsDescription": "Eliminar archivos de registro que tengan mas de {0} días de antigüedad.", + "TaskCleanLogs": "Limpiar directorio de registros", + "TaskRefreshLibraryDescription": "Escanear su librería multimedia por nuevos archivos y refrescar metadatos.", + "TaskRefreshLibrary": "Escanear librería multimedia", + "TaskRefreshChapterImagesDescription": "Crear miniaturas de videos que tengan capítulos.", + "TaskRefreshChapterImages": "Extraer imágenes de capitulo", + "TaskCleanCacheDescription": "Eliminar archivos de cache que no se necesiten en el sistema.", + "TaskCleanCache": "Limpiar directorio Cache", + "TasksChannelsCategory": "Canales de Internet", + "TasksApplicationCategory": "Solicitud", + "TasksLibraryCategory": "Biblioteca", + "TasksMaintenanceCategory": "Mantenimiento" } diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json index 99fda7aa6..e0bbe90b3 100644 --- a/Emby.Server.Implementations/Localization/Core/es-MX.json +++ b/Emby.Server.Implementations/Localization/Core/es-MX.json @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "El servidor Jellyfin esta cargando. Por favor intente de nuevo dentro de poco.", "SubtitleDownloadFailureForItem": "Falló la descarga de subtítulos para {0}", "SubtitleDownloadFailureFromForItem": "Falló la descarga de subtitulos desde {0} para {1}", - "SubtitlesDownloadedForItem": "Subtítulos descargados para {0}", "Sync": "Sincronizar", "System": "Sistema", "TvShows": "Programas de TV", @@ -93,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducirse {1} en {2}", "ValueHasBeenAddedToLibrary": "{0} se han añadido a su biblioteca de medios", "ValueSpecialEpisodeName": "Especial - {0}", - "VersionNumber": "Versión {0}" + "VersionNumber": "Versión {0}", + "TaskDownloadMissingSubtitlesDescription": "Buscar subtítulos de internet basado en configuración de metadatos.", + "TaskDownloadMissingSubtitles": "Descargar subtítulos perdidos", + "TaskRefreshChannelsDescription": "Refrescar información de canales de internet.", + "TaskRefreshChannels": "Actualizar canales", + "TaskCleanTranscodeDescription": "Eliminar archivos transcodificados que tengan mas de un día.", + "TaskCleanTranscode": "Limpiar directorio de transcodificado", + "TaskUpdatePluginsDescription": "Descargar y actualizar complementos que están configurados para actualizarse automáticamente.", + "TaskUpdatePlugins": "Actualizar complementos", + "TaskRefreshPeopleDescription": "Actualizar datos de actores y directores en su librería multimedia.", + "TaskRefreshPeople": "Refrescar persona", + "TaskCleanLogsDescription": "Eliminar archivos de registro con mas de {0} días.", + "TaskCleanLogs": "Directorio de logo limpio", + "TaskRefreshLibraryDescription": "Escanear su librería multimedia para nuevos archivos y refrescar metadatos.", + "TaskRefreshLibrary": "Escanear librería multimerdia", + "TaskRefreshChapterImagesDescription": "Crear miniaturas para videos con capítulos.", + "TaskRefreshChapterImages": "Extraer imágenes de capítulos", + "TaskCleanCacheDescription": "Eliminar archivos cache que ya no se necesiten por el sistema.", + "TaskCleanCache": "Limpiar directorio cache", + "TasksChannelsCategory": "Canales de Internet", + "TasksApplicationCategory": "Aplicación", + "TasksLibraryCategory": "Biblioteca", + "TasksMaintenanceCategory": "Mantenimiento" } diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index 2dcc2c1c8..de1baada8 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "Jellyfin Server se está cargando. Vuelve a intentarlo en breve.", "SubtitleDownloadFailureForItem": "Error al descargar subtítulos para {0}", "SubtitleDownloadFailureFromForItem": "Fallo de descarga de subtítulos desde {0} para {1}", - "SubtitlesDownloadedForItem": "Descargar subtítulos para {0}", "Sync": "Sincronizar", "System": "Sistema", "TvShows": "Programas de televisión", @@ -93,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}", "ValueHasBeenAddedToLibrary": "{0} ha sido añadido a tu biblioteca multimedia", "ValueSpecialEpisodeName": "Especial - {0}", - "VersionNumber": "Versión {0}" + "VersionNumber": "Versión {0}", + "TasksMaintenanceCategory": "Mantenimiento", + "TasksLibraryCategory": "Librería", + "TasksApplicationCategory": "Aplicación", + "TasksChannelsCategory": "Canales de internet", + "TaskCleanCache": "Eliminar archivos temporales", + "TaskCleanCacheDescription": "Elimina los archivos temporales que ya no son necesarios para el servidor.", + "TaskRefreshChapterImages": "Extraer imágenes de los capítulos", + "TaskRefreshChapterImagesDescription": "Crea las miniaturas de los vídeos que tengan capítulos.", + "TaskRefreshLibrary": "Escanear la biblioteca", + "TaskRefreshLibraryDescription": "Añade los archivos que se hayan añadido a la biblioteca y actualiza las etiquetas de los ya presentes.", + "TaskCleanLogs": "Limpiar registros", + "TaskCleanLogsDescription": "Elimina los archivos de registro que tengan más de {0} días.", + "TaskRefreshPeople": "Actualizar personas", + "TaskRefreshPeopleDescription": "Actualiza las etiquetas de los intérpretes y directores presentes en tus bibliotecas.", + "TaskUpdatePlugins": "Actualizar extensiones", + "TaskUpdatePluginsDescription": "Actualiza las extensiones que están configuradas para actualizarse automáticamente.", + "TaskCleanTranscode": "Limpiar las transcodificaciones", + "TaskCleanTranscodeDescription": "Elimina los archivos temporales de transcodificación anteriores a un día de antigüedad.", + "TaskRefreshChannels": "Actualizar canales", + "TaskRefreshChannelsDescription": "Actualiza la información de los canales de internet.", + "TaskDownloadMissingSubtitles": "Descargar los subtítulos que faltan", + "TaskDownloadMissingSubtitlesDescription": "Busca en internet los subtítulos que falten en el contenido de tus bibliotecas, basándose en la configuración de los metadatos." } diff --git a/Emby.Server.Implementations/Localization/Core/es_DO.json b/Emby.Server.Implementations/Localization/Core/es_DO.json index 1a7b57c53..0ef16542f 100644 --- a/Emby.Server.Implementations/Localization/Core/es_DO.json +++ b/Emby.Server.Implementations/Localization/Core/es_DO.json @@ -5,7 +5,7 @@ "Collections": "Colecciones", "Artists": "Artistas", "DeviceOnlineWithName": "{0} está conectado", - "DeviceOfflineWithName": "{0} ha desconectado", + "DeviceOfflineWithName": "{0} se ha desconectado", "ChapterNameValue": "Capítulo {0}", "CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}", "AuthenticationSucceededWithUserName": "{0} autenticado con éxito", diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json index faa658ed5..be6f87ee3 100644 --- a/Emby.Server.Implementations/Localization/Core/fa.json +++ b/Emby.Server.Implementations/Localization/Core/fa.json @@ -1,56 +1,56 @@ { - "Albums": "آلبوم ها", + "Albums": "آلبومها", "AppDeviceValues": "برنامه: {0} ، دستگاه: {1}", "Application": "برنامه", "Artists": "هنرمندان", "AuthenticationSucceededWithUserName": "{0} با موفقیت تایید اعتبار شد", - "Books": "کتاب ها", - "CameraImageUploadedFrom": "یک عکس جدید از دوربین ارسال شده {0}", - "Channels": "کانال ها", - "ChapterNameValue": "فصل {0}", - "Collections": "کلکسیون ها", + "Books": "کتابها", + "CameraImageUploadedFrom": "یک عکس جدید از دوربین ارسال شده است {0}", + "Channels": "کانالها", + "ChapterNameValue": "قسمت {0}", + "Collections": "مجموعهها", "DeviceOfflineWithName": "ارتباط {0} قطع شد", - "DeviceOnlineWithName": "{0} متصل شده", + "DeviceOnlineWithName": "{0} متصل شد", "FailedLoginAttemptWithUserName": "تلاش برای ورود از {0} ناموفق بود", - "Favorites": "مورد علاقه ها", - "Folders": "پوشه ها", + "Favorites": "مورد علاقهها", + "Folders": "پوشهها", "Genres": "ژانرها", "HeaderAlbumArtists": "هنرمندان آلبوم", "HeaderCameraUploads": "آپلودهای دوربین", "HeaderContinueWatching": "ادامه تماشا", - "HeaderFavoriteAlbums": "آلبوم های مورد علاقه", + "HeaderFavoriteAlbums": "آلبومهای مورد علاقه", "HeaderFavoriteArtists": "هنرمندان مورد علاقه", - "HeaderFavoriteEpisodes": "قسمت های مورد علاقه", - "HeaderFavoriteShows": "سریال های مورد علاقه", - "HeaderFavoriteSongs": "آهنگ های مورد علاقه", - "HeaderLiveTV": "پخش زنده تلویزیون", - "HeaderNextUp": "بعدی چیه", - "HeaderRecordingGroups": "گروه های ضبط", + "HeaderFavoriteEpisodes": "قسمتهای مورد علاقه", + "HeaderFavoriteShows": "سریالهای مورد علاقه", + "HeaderFavoriteSongs": "آهنگهای مورد علاقه", + "HeaderLiveTV": "تلویزیون زنده", + "HeaderNextUp": "قسمت بعدی", + "HeaderRecordingGroups": "گروههای ضبط", "HomeVideos": "ویدیوهای خانگی", "Inherit": "به ارث برده", "ItemAddedWithName": "{0} به کتابخانه افزوده شد", "ItemRemovedWithName": "{0} از کتابخانه حذف شد", "LabelIpAddressValue": "آدرس آی پی: {0}", "LabelRunningTimeValue": "زمان اجرا: {0}", - "Latest": "آخرین", + "Latest": "جدیدترینها", "MessageApplicationUpdated": "سرور Jellyfin بروزرسانی شد", - "MessageApplicationUpdatedTo": "سرور جلیفین آپدیت شده به نسخه {0}", + "MessageApplicationUpdatedTo": "سرور Jellyfin به نسخه {0} بروزرسانی شد", "MessageNamedServerConfigurationUpdatedWithValue": "پکربندی بخش {0} سرور بروزرسانی شد", "MessageServerConfigurationUpdated": "پیکربندی سرور بروزرسانی شد", - "MixedContent": "محتوای درهم", - "Movies": "فیلم های سینمایی", + "MixedContent": "محتوای مخلوط", + "Movies": "فیلمها", "Music": "موسیقی", "MusicVideos": "موزیک ویدیوها", - "NameInstallFailed": "{0} نصب با مشکل مواجه شده", + "NameInstallFailed": "{0} نصب با مشکل مواجه شد", "NameSeasonNumber": "فصل {0}", - "NameSeasonUnknown": "فصل های ناشناخته", - "NewVersionIsAvailable": "یک نسخه جدید جلیفین برای بروزرسانی آماده میباشد.", + "NameSeasonUnknown": "فصل ناشناخته", + "NewVersionIsAvailable": "یک نسخه جدید Jellyfin برای بروزرسانی آماده میباشد.", "NotificationOptionApplicationUpdateAvailable": "بروزرسانی برنامه موجود است", "NotificationOptionApplicationUpdateInstalled": "بروزرسانی برنامه نصب شد", "NotificationOptionAudioPlayback": "پخش صدا آغاز شد", "NotificationOptionAudioPlaybackStopped": "پخش صدا متوقف شد", "NotificationOptionCameraImageUploaded": "تصاویر دوربین آپلود شد", - "NotificationOptionInstallationFailed": "شکست نصب", + "NotificationOptionInstallationFailed": "نصب شکست خورد", "NotificationOptionNewLibraryContent": "محتوای جدید افزوده شد", "NotificationOptionPluginError": "خرابی افزونه", "NotificationOptionPluginInstalled": "افزونه نصب شد", @@ -58,40 +58,61 @@ "NotificationOptionPluginUpdateInstalled": "بروزرسانی افزونه نصب شد", "NotificationOptionServerRestartRequired": "شروع مجدد سرور نیاز است", "NotificationOptionTaskFailed": "شکست وظیفه برنامه ریزی شده", - "NotificationOptionUserLockedOut": "کاربر از سیستم خارج شد", + "NotificationOptionUserLockedOut": "کاربر قفل شد", "NotificationOptionVideoPlayback": "پخش ویدیو آغاز شد", "NotificationOptionVideoPlaybackStopped": "پخش ویدیو متوقف شد", - "Photos": "عکس ها", - "Playlists": "لیست های پخش", + "Photos": "عکسها", + "Playlists": "لیستهای پخش", "Plugin": "افزونه", "PluginInstalledWithName": "{0} نصب شد", "PluginUninstalledWithName": "{0} حذف شد", "PluginUpdatedWithName": "{0} آپدیت شد", "ProviderValue": "ارائه دهنده: {0}", - "ScheduledTaskFailedWithName": "{0} ناموفق بود", + "ScheduledTaskFailedWithName": "{0} شکست خورد", "ScheduledTaskStartedWithName": "{0} شروع شد", - "ServerNameNeedsToBeRestarted": "{0} احتیاج به راه اندازی مجدد", - "Shows": "سریال ها", - "Songs": "آهنگ ها", + "ServerNameNeedsToBeRestarted": "{0} نیاز به راه اندازی مجدد دارد", + "Shows": "سریالها", + "Songs": "موسیقیها", "StartupEmbyServerIsLoading": "سرور Jellyfin در حال بارگیری است. لطفا کمی بعد دوباره تلاش کنید.", "SubtitleDownloadFailureForItem": "دانلود زیرنویس برای {0} ناموفق بود", - "SubtitleDownloadFailureFromForItem": "زیرنویس برای دانلود با مشکل مواجه شده از {0} برای {1}", - "SubtitlesDownloadedForItem": "زیرنویس {0} دانلود شد", - "Sync": "همگامسازی", + "SubtitleDownloadFailureFromForItem": "بارگیری زیرنویس برای {1} از {0} شکست خورد", + "Sync": "همگامسازی", "System": "سیستم", - "TvShows": "سریال های تلویزیونی", + "TvShows": "سریالهای تلویزیونی", "User": "کاربر", "UserCreatedWithName": "کاربر {0} ایجاد شد", "UserDeletedWithName": "کاربر {0} حذف شد", - "UserDownloadingItemWithValues": "{0} در حال دانلود است {1}", - "UserLockedOutWithName": "کاربر {0} از سیستم خارج شد", + "UserDownloadingItemWithValues": "{0} در حال بارگیری {1} میباشد", + "UserLockedOutWithName": "کاربر {0} قفل شده است", "UserOfflineFromDevice": "ارتباط {0} از {1} قطع شد", - "UserOnlineFromDevice": "{0}از {1} آنلاین میباشد", - "UserPasswordChangedWithName": "رمز برای کاربر {0} تغییر یافت", + "UserOnlineFromDevice": "{0} از {1} آنلاین میباشد", + "UserPasswordChangedWithName": "گذرواژه برای کاربر {0} تغییر کرد", "UserPolicyUpdatedWithName": "سیاست کاربری برای {0} بروزرسانی شد", - "UserStartedPlayingItemWithValues": "{0} شروع به پخش {1} کرد", - "UserStoppedPlayingItemWithValues": "{0} پخش {1} را متوقف کرد", - "ValueHasBeenAddedToLibrary": "{0} اضافه شده به کتابخانه رسانه شما", - "ValueSpecialEpisodeName": "ویژه- {0}", - "VersionNumber": "نسخه {0}" + "UserStartedPlayingItemWithValues": "{0} در حال پخش {1} بر روی {2} است", + "UserStoppedPlayingItemWithValues": "{0} پخش {1} را بر روی {2} به پایان رساند", + "ValueHasBeenAddedToLibrary": "{0} به کتابخانهی رسانهی شما افزوده شد", + "ValueSpecialEpisodeName": "ویژه - {0}", + "VersionNumber": "نسخه {0}", + "TaskCleanTranscodeDescription": "فایلهای کدگذاری که قدیمیتر از یک روز هستند را حذف میکند.", + "TaskCleanTranscode": "پاکسازی مسیر کد گذاری", + "TaskUpdatePluginsDescription": "دانلود و نصب به روز رسانی افزونههایی که برای به روز رسانی خودکار پیکربندی شدهاند.", + "TaskDownloadMissingSubtitlesDescription": "جستجوی زیرنویسهای ناموجود در اینترنت بر اساس پیکربندی ابردادهها.", + "TaskDownloadMissingSubtitles": "دانلود زیرنویسهای ناموجود", + "TaskRefreshChannelsDescription": "اطلاعات کانال اینترنتی را تازه سازی میکند.", + "TaskRefreshChannels": "تازه سازی کانالها", + "TaskUpdatePlugins": "به روز رسانی افزونهها", + "TaskRefreshPeopleDescription": "ابردادهها برای بازیگران و کارگردانان در کتابخانه رسانه شما به روزرسانی می شوند.", + "TaskRefreshPeople": "تازه سازی افراد", + "TaskCleanLogsDescription": "واقعه نگارهایی را که قدیمی تر {0} روز هستند را حذف می کند.", + "TaskCleanLogs": "پاکسازی مسیر واقعه نگار", + "TaskRefreshLibraryDescription": "کتابخانه رسانه شما را اسکن میکند و ابردادهها را تازه سازی میکند.", + "TaskRefreshLibrary": "اسکن کتابخانه رسانه", + "TaskRefreshChapterImagesDescription": "عکسهای کوچک برای ویدیوهایی که سکانس دارند ایجاد میکند.", + "TaskRefreshChapterImages": "استخراج عکسهای سکانس", + "TaskCleanCacheDescription": "فایلهای حافظه موقت که توسط سیستم دیگر مورد نیاز نیستند حذف میشوند.", + "TaskCleanCache": "پاکسازی مسیر حافظه موقت", + "TasksChannelsCategory": "کانالهای داخلی", + "TasksApplicationCategory": "برنامه", + "TasksLibraryCategory": "کتابخانه", + "TasksMaintenanceCategory": "تعمیر" } diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json index a38103d25..b39adefe7 100644 --- a/Emby.Server.Implementations/Localization/Core/fi.json +++ b/Emby.Server.Implementations/Localization/Core/fi.json @@ -1,5 +1,5 @@ { - "HeaderLiveTV": "TV-lähetykset", + "HeaderLiveTV": "Suorat lähetykset", "NewVersionIsAvailable": "Uusi versio Jellyfin palvelimesta on ladattavissa.", "NameSeasonUnknown": "Tuntematon Kausi", "NameSeasonNumber": "Kausi {0}", @@ -19,12 +19,12 @@ "ItemAddedWithName": "{0} lisättiin kirjastoon", "Inherit": "Periytyä", "HomeVideos": "Kotivideot", - "HeaderRecordingGroups": "Nauhoitusryhmät", + "HeaderRecordingGroups": "Nauhoiteryhmät", "HeaderNextUp": "Seuraavaksi", "HeaderFavoriteSongs": "Lempikappaleet", "HeaderFavoriteShows": "Lempisarjat", "HeaderFavoriteEpisodes": "Lempijaksot", - "HeaderCameraUploads": "Kameralataukset", + "HeaderCameraUploads": "Kamerasta Lähetetyt", "HeaderFavoriteArtists": "Lempiartistit", "HeaderFavoriteAlbums": "Lempialbumit", "HeaderContinueWatching": "Jatka katsomista", @@ -63,34 +63,55 @@ "UserPasswordChangedWithName": "Salasana vaihdettu käyttäjälle {0}", "UserOnlineFromDevice": "{0} on paikalla osoitteesta {1}", "UserOfflineFromDevice": "{0} yhteys katkaistu {1}", - "UserLockedOutWithName": "Käyttäjä {0} kirjautui ulos", - "UserDownloadingItemWithValues": "{0} latautumassa {1}", - "UserDeletedWithName": "Poistettiin käyttäjä {0}", - "UserCreatedWithName": "Luotiin käyttäjä {0}", + "UserLockedOutWithName": "Käyttäjä {0} lukittu", + "UserDownloadingItemWithValues": "{0} lataa {1}", + "UserDeletedWithName": "Käyttäjä {0} poistettu", + "UserCreatedWithName": "Käyttäjä {0} luotu", "TvShows": "TV-Ohjelmat", "Sync": "Synkronoi", - "SubtitlesDownloadedForItem": "Tekstitys ladattu {0}", "SubtitleDownloadFailureFromForItem": "Tekstityksen lataaminen epäonnistui {0} - {1}", "StartupEmbyServerIsLoading": "Jellyfin palvelin latautuu. Kokeile hetken kuluttua uudelleen.", "Songs": "Kappaleet", "Shows": "Ohjelmat", "ServerNameNeedsToBeRestarted": "{0} vaatii uudelleenkäynnistyksen", - "ProviderValue": "Palveluntarjoaja: {0}", + "ProviderValue": "Tarjoaja: {0}", "Plugin": "Liitännäinen", - "NotificationOptionVideoPlaybackStopped": "Videon toistaminen pysäytetty", - "NotificationOptionVideoPlayback": "Videon toistaminen aloitettu", - "NotificationOptionUserLockedOut": "Käyttäjä kirjautui ulos", - "NotificationOptionTaskFailed": "Ajastetun tehtävän ongelma", + "NotificationOptionVideoPlaybackStopped": "Videon toisto pysäytetty", + "NotificationOptionVideoPlayback": "Videon toisto aloitettu", + "NotificationOptionUserLockedOut": "Käyttäjä lukittu", + "NotificationOptionTaskFailed": "Ajastettu tehtävä epäonnistui", "NotificationOptionServerRestartRequired": "Palvelimen uudelleenkäynnistys vaaditaan", - "NotificationOptionPluginUpdateInstalled": "Liitännäinen päivitetty", + "NotificationOptionPluginUpdateInstalled": "Lisäosan päivitys asennettu", "NotificationOptionPluginUninstalled": "Liitännäinen poistettu", "NotificationOptionPluginInstalled": "Liitännäinen asennettu", "NotificationOptionPluginError": "Ongelma liitännäisessä", "NotificationOptionNewLibraryContent": "Uutta sisältöä lisätty", "NotificationOptionInstallationFailed": "Asennus epäonnistui", - "NotificationOptionCameraImageUploaded": "Kuva ladattu kamerasta", - "NotificationOptionAudioPlaybackStopped": "Audion toisto pysäytetty", - "NotificationOptionAudioPlayback": "Audion toisto aloitettu", - "NotificationOptionApplicationUpdateInstalled": "Ohjelmistopäivitys asennettu", - "NotificationOptionApplicationUpdateAvailable": "Ohjelmistopäivitys saatavilla" + "NotificationOptionCameraImageUploaded": "Kameran kuva ladattu", + "NotificationOptionAudioPlaybackStopped": "Äänen toisto lopetettu", + "NotificationOptionAudioPlayback": "Toistetaan ääntä", + "NotificationOptionApplicationUpdateInstalled": "Uusi sovellusversio asennettu", + "NotificationOptionApplicationUpdateAvailable": "Sovelluksesta on uusi versio saatavilla", + "TasksMaintenanceCategory": "Ylläpito", + "TaskDownloadMissingSubtitlesDescription": "Etsii puuttuvia tekstityksiä videon metadatatietojen pohjalta.", + "TaskDownloadMissingSubtitles": "Lataa puuttuvat tekstitykset", + "TaskRefreshChannelsDescription": "Päivittää internet-kanavien tiedot.", + "TaskRefreshChannels": "Päivitä kanavat", + "TaskCleanTranscodeDescription": "Poistaa transkoodatut tiedostot jotka ovat yli päivän vanhoja.", + "TaskCleanTranscode": "Puhdista transkoodaushakemisto", + "TaskUpdatePluginsDescription": "Lataa ja asentaa päivitykset liitännäisille jotka on asetettu päivittymään automaattisesti.", + "TaskUpdatePlugins": "Päivitä liitännäiset", + "TaskRefreshPeopleDescription": "Päivittää näyttelijöiden ja ohjaajien mediatiedot kirjastossasi.", + "TaskRefreshPeople": "Päivitä henkilöt", + "TaskCleanLogsDescription": "Poistaa lokitiedostot jotka ovat yli {0} päivää vanhoja.", + "TaskCleanLogs": "Puhdista lokihakemisto", + "TaskRefreshLibraryDescription": "Skannaa mediakirjastosi uusien tiedostojen varalle, sekä virkistää metatiedot.", + "TaskRefreshLibrary": "Skannaa mediakirjasto", + "TaskRefreshChapterImagesDescription": "Luo pienoiskuvat videoille joissa on lukuja.", + "TaskRefreshChapterImages": "Eristä lukujen kuvat", + "TaskCleanCacheDescription": "Poistaa järjestelmälle tarpeettomat väliaikaistiedostot.", + "TaskCleanCache": "Tyhjennä välimuisti-hakemisto", + "TasksChannelsCategory": "Internet kanavat", + "TasksApplicationCategory": "Sovellus", + "TasksLibraryCategory": "Kirjasto" } diff --git a/Emby.Server.Implementations/Localization/Core/fil.json b/Emby.Server.Implementations/Localization/Core/fil.json index 66db059d9..47daf2044 100644 --- a/Emby.Server.Implementations/Localization/Core/fil.json +++ b/Emby.Server.Implementations/Localization/Core/fil.json @@ -16,7 +16,6 @@ "TvShows": "Pelikula", "System": "Sistema", "Sync": "Pag-sync", - "SubtitlesDownloadedForItem": "Naidownload na ang subtitles {0}", "SubtitleDownloadFailureFromForItem": "Hindi naidownload ang subtitles {0} para sa {1}", "StartupEmbyServerIsLoading": "Nagloload ang Jellyfin Server. Sandaling maghintay.", "Songs": "Kanta", @@ -91,5 +90,13 @@ "Artists": "Artista", "Application": "Aplikasyon", "AppDeviceValues": "Aplikasyon: {0}, Aparato: {1}", - "Albums": "Albums" + "Albums": "Albums", + "TaskRefreshLibrary": "Suriin ang nasa librerya", + "TaskRefreshChapterImagesDescription": "Gumawa ng larawan para sa mga pelikula na may kabanata", + "TaskRefreshChapterImages": "Kunin ang mga larawan ng kabanata", + "TaskCleanCacheDescription": "Tanggalin ang mga cache file na hindi na kailangan ng systema.", + "TasksChannelsCategory": "Palabas sa internet", + "TasksLibraryCategory": "Librerya", + "TasksMaintenanceCategory": "Pagpapanatili", + "HomeVideos": "Sariling pelikula" } diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json index 4b4db39a8..2c9dae6a1 100644 --- a/Emby.Server.Implementations/Localization/Core/fr-CA.json +++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "Le serveur Jellyfin est en cours de chargement. Veuillez réessayer dans quelques instants.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}", - "SubtitlesDownloadedForItem": "Les sous-titres de {0} ont été téléchargés", "Sync": "Synchroniser", "System": "Système", "TvShows": "Séries Télé", @@ -93,5 +92,7 @@ "UserStoppedPlayingItemWithValues": "{0} vient d'arrêter la lecture de {1} sur {2}", "ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque", "ValueSpecialEpisodeName": "Spécial - {0}", - "VersionNumber": "Version {0}" + "VersionNumber": "Version {0}", + "TasksLibraryCategory": "Bibliothèque", + "TasksMaintenanceCategory": "Entretien" } diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index 7dfee1085..ec29d3374 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} authentifié avec succès", "Books": "Livres", - "CameraImageUploadedFrom": "Une nouvelle photo a été chargée depuis {0}", + "CameraImageUploadedFrom": "Une nouvelle photographie a été chargée depuis {0}", "Channels": "Chaînes", "ChapterNameValue": "Chapitre {0}", "Collections": "Collections", @@ -15,7 +15,7 @@ "Favorites": "Favoris", "Folders": "Dossiers", "Genres": "Genres", - "HeaderAlbumArtists": "Artistes d'album", + "HeaderAlbumArtists": "Artistes de l'album", "HeaderCameraUploads": "Photos transférées", "HeaderContinueWatching": "Continuer à regarder", "HeaderFavoriteAlbums": "Albums favoris", @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "Le serveur Jellyfin est en cours de chargement. Veuillez réessayer dans quelques instants.", "SubtitleDownloadFailureForItem": "Le téléchargement des sous-titres pour {0} a échoué.", "SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}", - "SubtitlesDownloadedForItem": "Les sous-titres de {0} ont été téléchargés", "Sync": "Synchroniser", "System": "Système", "TvShows": "Séries Télé", @@ -93,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} vient d'arrêter la lecture de {1} sur {2}", "ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque", "ValueSpecialEpisodeName": "Spécial - {0}", - "VersionNumber": "Version {0}" + "VersionNumber": "Version {0}", + "TasksChannelsCategory": "Chaines en ligne", + "TaskDownloadMissingSubtitlesDescription": "Cherche les sous-titres manquant sur internet en se basant sur la configuration des métadonnées.", + "TaskDownloadMissingSubtitles": "Télécharge les sous-titres manquant", + "TaskRefreshChannelsDescription": "Rafraîchit les informations des chaines en ligne.", + "TaskRefreshChannels": "Rafraîchit les chaines", + "TaskCleanTranscodeDescription": "Supprime les fichiers transcodés de plus d'un jour.", + "TaskCleanTranscode": "Nettoie les dossier des transcodages", + "TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des plugins configurés pour être mis à jour automatiquement.", + "TaskUpdatePlugins": "Mettre à jour les plugins", + "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et directeurs dans votre bibliothèque.", + "TaskRefreshPeople": "Rafraîchit les acteurs", + "TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.", + "TaskCleanLogs": "Nettoie le répertoire des journaux", + "TaskRefreshLibraryDescription": "Scanne toute les bibliothèques pour trouver les nouveaux fichiers et rafraîchit les métadonnées.", + "TaskRefreshLibrary": "Scanne toute les Bibliothèques", + "TaskRefreshChapterImagesDescription": "Crée des images de miniature pour les vidéos ayant des chapitres.", + "TaskRefreshChapterImages": "Extrait les images de chapitre", + "TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.", + "TaskCleanCache": "Vider le répertoire cache", + "TasksApplicationCategory": "Application", + "TasksLibraryCategory": "Bibliothèque", + "TasksMaintenanceCategory": "Maintenance" } diff --git a/Emby.Server.Implementations/Localization/Core/gsw.json b/Emby.Server.Implementations/Localization/Core/gsw.json index 69c157401..9611e33f5 100644 --- a/Emby.Server.Implementations/Localization/Core/gsw.json +++ b/Emby.Server.Implementations/Localization/Core/gsw.json @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "Jellyfin Server ladt. Bitte grad noeinisch probiere.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Ondertetle vo {0} för {1} hend ned chönne abeglade wärde", - "SubtitlesDownloadedForItem": "Ondertetle abeglade för {0}", "Sync": "Synchronisation", "System": "System", "TvShows": "Färnsehserie", diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json index 5618719dd..1ce8b08a0 100644 --- a/Emby.Server.Implementations/Localization/Core/he.json +++ b/Emby.Server.Implementations/Localization/Core/he.json @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "שרת Jellyfin בהליכי טעינה. אנא נסה שנית בעוד זמן קצר.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", - "SubtitlesDownloadedForItem": "Subtitles downloaded for {0}", "Sync": "סנכרן", "System": "System", "TvShows": "סדרות טלוויזיה", diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json index f284b3cd9..6947178d7 100644 --- a/Emby.Server.Implementations/Localization/Core/hr.json +++ b/Emby.Server.Implementations/Localization/Core/hr.json @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "Jellyfin Server se učitava. Pokušajte ponovo kasnije.", "SubtitleDownloadFailureForItem": "Titlovi prijevoda nisu preuzeti za {0}", "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", - "SubtitlesDownloadedForItem": "Titlovi prijevoda preuzeti za {0}", "Sync": "Sink.", "System": "Sistem", "TvShows": "TV Shows", diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index 6017aa7f9..c5c3844e3 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -7,7 +7,7 @@ "Books": "Könyvek", "CameraImageUploadedFrom": "Új kamerakép került feltöltésre innen: {0}", "Channels": "Csatornák", - "ChapterNameValue": "Jelenet {0}", + "ChapterNameValue": "{0}. jelenet", "Collections": "Gyűjtemények", "DeviceOfflineWithName": "{0} kijelentkezett", "DeviceOnlineWithName": "{0} belépett", @@ -71,12 +71,11 @@ "ScheduledTaskFailedWithName": "{0} sikertelen", "ScheduledTaskStartedWithName": "{0} elkezdve", "ServerNameNeedsToBeRestarted": "{0}-t újra kell indítani", - "Shows": "Műsorok", + "Shows": "Sorozatok", "Songs": "Dalok", "StartupEmbyServerIsLoading": "A Jellyfin Szerver betöltődik. Kérlek, próbáld újra hamarosan.", "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}", "Sync": "Szinkronizál", "System": "Rendszer", "TvShows": "TV műsorok", @@ -93,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} befejezte {1} lejátászását itt: {2}", "ValueHasBeenAddedToLibrary": "{0} hozzáadva a médiatárhoz", "ValueSpecialEpisodeName": "Special - {0}", - "VersionNumber": "Verzió: {0}" + "VersionNumber": "Verzió: {0}", + "TaskCleanTranscode": "Átkódolási könyvtár ürítése", + "TaskUpdatePluginsDescription": "Letölti és telepíti a frissítéseket azokhoz a bővítményekhez, amelyeknél az automatikus frissítés engedélyezve van.", + "TaskUpdatePlugins": "Bővítmények frissítése", + "TaskRefreshPeopleDescription": "Frissíti a szereplők és a stábok metaadatait a könyvtáradban.", + "TaskRefreshPeople": "Személyek frissítése", + "TaskCleanLogsDescription": "Törli azokat a naplófájlokat, amelyek {0} napnál régebbiek.", + "TaskCleanLogs": "Naplózási könyvtár ürítése", + "TaskRefreshLibraryDescription": "Átvizsgálja a könyvtáraidat új fájlokért és frissíti a metaadatokat.", + "TaskRefreshLibrary": "Média könyvtár beolvasása", + "TaskRefreshChapterImagesDescription": "Miniatűröket generál olyan videókhoz, amely tartalmaz fejezeteket.", + "TaskRefreshChapterImages": "Fejezetek képeinek generálása", + "TaskCleanCacheDescription": "Törli azokat a gyorsítótárazott fájlokat, amikre a rendszernek már nincs szüksége.", + "TaskCleanCache": "Gyorsítótár könyvtárának ürítése", + "TasksChannelsCategory": "Internetes csatornák", + "TasksApplicationCategory": "Alkalmazás", + "TasksLibraryCategory": "Könyvtár", + "TasksMaintenanceCategory": "Karbantartás", + "TaskDownloadMissingSubtitlesDescription": "A metaadat konfiguráció alapján ellenőrzi és letölti a hiányzó feliratokat az internetről.", + "TaskDownloadMissingSubtitles": "Hiányzó feliratok letöltése", + "TaskRefreshChannelsDescription": "Frissíti az internetes csatornák adatait.", + "TaskRefreshChannels": "Csatornák frissítése", + "TaskCleanTranscodeDescription": "Törli az egy napnál régebbi átkódolási fájlokat." } diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json index 68fffbf0a..eabdb9138 100644 --- a/Emby.Server.Implementations/Localization/Core/id.json +++ b/Emby.Server.Implementations/Localization/Core/id.json @@ -54,7 +54,6 @@ "User": "Pengguna", "System": "Sistem", "Sync": "Sinkron", - "SubtitlesDownloadedForItem": "Talop telah diunduh untuk {0}", "Shows": "Tayangan", "ServerNameNeedsToBeRestarted": "{0} perlu dimuat ulang", "ScheduledTaskStartedWithName": "{0} dimulai", diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json index 3490a7302..ef2a57e8e 100644 --- a/Emby.Server.Implementations/Localization/Core/is.json +++ b/Emby.Server.Implementations/Localization/Core/is.json @@ -86,7 +86,6 @@ "UserOfflineFromDevice": "{0} hefur aftengst frá {1}", "UserLockedOutWithName": "Notanda {0} hefur verið hindraður aðgangur", "UserDownloadingItemWithValues": "{0} Hleður niður {1}", - "SubtitlesDownloadedForItem": "Skjátextum halað niður fyrir {0}", "SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}", "ProviderValue": "Veitandi: {0}", "MessageNamedServerConfigurationUpdatedWithValue": "Stilling {0} hefur verið uppfærð á netþjón", diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index 395924af4..0758bbe9c 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -5,7 +5,7 @@ "Artists": "Artisti", "AuthenticationSucceededWithUserName": "{0} autenticato con successo", "Books": "Libri", - "CameraImageUploadedFrom": "È stata caricata una nuova immagine della fotocamera da {0}", + "CameraImageUploadedFrom": "È stata caricata una nuova immagine della fotocamera dal device {0}", "Channels": "Canali", "ChapterNameValue": "Capitolo {0}", "Collections": "Collezioni", @@ -15,7 +15,7 @@ "Favorites": "Preferiti", "Folders": "Cartelle", "Genres": "Generi", - "HeaderAlbumArtists": "Artisti dell' Album", + "HeaderAlbumArtists": "Artisti degli Album", "HeaderCameraUploads": "Caricamenti Fotocamera", "HeaderContinueWatching": "Continua a guardare", "HeaderFavoriteAlbums": "Album Preferiti", @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "Jellyfin server si sta avviando. Per favore riprova più tardi.", "SubtitleDownloadFailureForItem": "Impossibile scaricare i sottotitoli per {0}", "SubtitleDownloadFailureFromForItem": "Impossibile scaricare i sottotitoli da {0} per {1}", - "SubtitlesDownloadedForItem": "Sottotitoli scaricati per {0}", "Sync": "Sincronizza", "System": "Sistema", "TvShows": "Serie TV", @@ -93,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} ha interrotto la riproduzione di {1} su {2}", "ValueHasBeenAddedToLibrary": "{0} è stato aggiunto alla tua libreria multimediale", "ValueSpecialEpisodeName": "Speciale - {0}", - "VersionNumber": "Versione {0}" + "VersionNumber": "Versione {0}", + "TaskRefreshChannelsDescription": "Aggiorna le informazioni dei canali Internet.", + "TaskDownloadMissingSubtitlesDescription": "Cerca su internet i sottotitoli mancanti basandosi sulle configurazioni dei metadati.", + "TaskDownloadMissingSubtitles": "Scarica i sottotitoli mancanti", + "TaskRefreshChannels": "Aggiorna i canali", + "TaskCleanTranscodeDescription": "Cancella i file di transcode più vecchi di un giorno.", + "TaskCleanTranscode": "Svuota la cartella del transcoding", + "TaskUpdatePluginsDescription": "Scarica e installa gli aggiornamenti per i plugin che sono stati configurati per essere aggiornati contemporaneamente.", + "TaskUpdatePlugins": "Aggiorna i Plugin", + "TaskRefreshPeopleDescription": "Aggiorna i metadati per gli attori e registi nella tua libreria multimediale.", + "TaskRefreshPeople": "Aggiorna persone", + "TaskCleanLogsDescription": "Rimuovi i file di log più vecchi di {0} giorni.", + "TaskCleanLogs": "Pulisci la cartella dei log", + "TaskRefreshLibraryDescription": "Analizza la tua libreria multimediale per nuovi file e rinnova i metadati.", + "TaskRefreshLibrary": "Analizza la libreria dei contenuti multimediali", + "TaskRefreshChapterImagesDescription": "Crea le thumbnail per i video che hanno capitoli.", + "TaskRefreshChapterImages": "Estrai immagini capitolo", + "TaskCleanCacheDescription": "Cancella i file di cache non più necessari al sistema.", + "TaskCleanCache": "Pulisci la directory della cache", + "TasksChannelsCategory": "Canali su Internet", + "TasksApplicationCategory": "Applicazione", + "TasksLibraryCategory": "Libreria", + "TasksMaintenanceCategory": "Manutenzione" } diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json index 4aa0637c5..5e017d4c4 100644 --- a/Emby.Server.Implementations/Localization/Core/ja.json +++ b/Emby.Server.Implementations/Localization/Core/ja.json @@ -75,7 +75,6 @@ "Songs": "曲", "StartupEmbyServerIsLoading": "Jellyfin Server は現在読み込み中です。しばらくしてからもう一度お試しください。", "SubtitleDownloadFailureFromForItem": "{0} から {1}の字幕のダウンロードに失敗しました", - "SubtitlesDownloadedForItem": "{0} の字幕がダウンロードされました", "Sync": "同期", "System": "システム", "TvShows": "テレビ番組", @@ -92,5 +91,26 @@ "UserStoppedPlayingItemWithValues": "{0} は{2}で{1} の再生が終わりました", "ValueHasBeenAddedToLibrary": "{0}はあなたのメディアライブラリに追加されました", "ValueSpecialEpisodeName": "スペシャル - {0}", - "VersionNumber": "バージョン {0}" + "VersionNumber": "バージョン {0}", + "TaskCleanLogsDescription": "{0} 日以上前のログを消去します。", + "TaskCleanLogs": "ログの掃除", + "TaskRefreshLibraryDescription": "メディアライブラリをスキャンして新しいファイルを探し、メタデータをリフレッシュします。", + "TaskRefreshLibrary": "メディアライブラリのスキャン", + "TaskCleanCacheDescription": "不要なキャッシュを消去します。", + "TaskCleanCache": "キャッシュの掃除", + "TasksChannelsCategory": "ネットチャンネル", + "TasksApplicationCategory": "アプリケーション", + "TasksLibraryCategory": "ライブラリ", + "TasksMaintenanceCategory": "メンテナンス", + "TaskRefreshChannelsDescription": "ネットチャンネルの情報をリフレッシュします。", + "TaskRefreshChannels": "チャンネルのリフレッシュ", + "TaskCleanTranscodeDescription": "一日以上前のトランスコードを消去します。", + "TaskCleanTranscode": "トランスコード用のディレクトリの掃除", + "TaskUpdatePluginsDescription": "自動更新可能なプラグインのアップデートをダウンロードしてインストールします。", + "TaskUpdatePlugins": "プラグインの更新", + "TaskRefreshPeopleDescription": "メディアライブラリで俳優や監督のメタデータをリフレッシュします。", + "TaskRefreshPeople": "俳優や監督のデータのリフレッシュ", + "TaskDownloadMissingSubtitlesDescription": "メタデータ構成に基づいて、欠落している字幕をインターネットで検索します。", + "TaskRefreshChapterImagesDescription": "チャプターのあるビデオのサムネイルを作成します。", + "TaskRefreshChapterImages": "チャプター画像を抽出する" } diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json index cbee71155..5618ff4a8 100644 --- a/Emby.Server.Implementations/Localization/Core/kk.json +++ b/Emby.Server.Implementations/Localization/Core/kk.json @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "Jellyfin Server júktelýde. Áreketti kóp uzamaı qaıtalańyz.", "SubtitleDownloadFailureForItem": "Субтитрлер {0} үшін жүктеліп алынуы сәтсіз", "SubtitleDownloadFailureFromForItem": "{1} úshin sýbtıtrlerdi {0} kózinen júktep alý sátsiz", - "SubtitlesDownloadedForItem": "{0} úshin sýbtıtrler júktelip alyndy", "Sync": "Úndestirý", "System": "Júıe", "TvShows": "TD-kórsetimder", diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json index 0320a0a1b..9e3ecd5a8 100644 --- a/Emby.Server.Implementations/Localization/Core/ko.json +++ b/Emby.Server.Implementations/Localization/Core/ko.json @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "Jellyfin 서버를 불러오고 있습니다. 잠시 후에 다시 시도하십시오.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "{0}에서 {1} 자막 다운로드에 실패했습니다", - "SubtitlesDownloadedForItem": "{0} 자막 다운로드 완료", "Sync": "동기화", "System": "시스템", "TvShows": "TV 쇼", @@ -93,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{2}에서 {0}이 {1} 재생을 마침", "ValueHasBeenAddedToLibrary": "{0}가 미디어 라이브러리에 추가되었습니다", "ValueSpecialEpisodeName": "스페셜 - {0}", - "VersionNumber": "버전 {0}" + "VersionNumber": "버전 {0}", + "TasksApplicationCategory": "어플리케이션", + "TasksMaintenanceCategory": "유지 보수", + "TaskDownloadMissingSubtitlesDescription": "메타 데이터 기반으로 누락 된 자막이 있는지 인터넷을 검색합니다.", + "TaskDownloadMissingSubtitles": "누락 된 자막 다운로드", + "TaskRefreshChannelsDescription": "인터넷 채널 정보를 새로 고칩니다.", + "TaskRefreshChannels": "채널 새로고침", + "TaskCleanTranscodeDescription": "하루 이상 지난 트랜스 코드 파일을 삭제합니다.", + "TaskCleanTranscode": "트랜스코드 폴더 청소", + "TaskUpdatePluginsDescription": "자동으로 업데이트되도록 구성된 플러그인 업데이트를 다운로드하여 설치합니다.", + "TaskUpdatePlugins": "플러그인 업데이트", + "TaskRefreshPeopleDescription": "미디어 라이브러리에서 배우 및 감독의 메타 데이터를 업데이트합니다.", + "TaskRefreshPeople": "인물 새로고침", + "TaskCleanLogsDescription": "{0} 일이 지난 로그 파일을 삭제합니다.", + "TaskCleanLogs": "로그 폴더 청소", + "TaskRefreshLibraryDescription": "미디어 라이브러리에서 새 파일을 검색하고 메타 데이터를 새로 고칩니다.", + "TaskRefreshLibrary": "미디어 라이브러리 스캔", + "TaskRefreshChapterImagesDescription": "챕터가있는 비디오의 썸네일을 만듭니다.", + "TaskRefreshChapterImages": "챕터 이미지 추출", + "TaskCleanCacheDescription": "시스템에서 더 이상 필요하지 않은 캐시 파일을 삭제합니다.", + "TaskCleanCache": "캐시 폴더 청소", + "TasksChannelsCategory": "인터넷 채널", + "TasksLibraryCategory": "라이브러리" } diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json index e8e1b7740..01a740187 100644 --- a/Emby.Server.Implementations/Localization/Core/lt-LT.json +++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "Jellyfin Server kraunasi. Netrukus pabandykite dar kartą.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "{1} subtitrai buvo nesėkmingai parsiųsti iš {0}", - "SubtitlesDownloadedForItem": "{0} subtitrai parsiųsti", "Sync": "Sinchronizuoti", "System": "System", "TvShows": "TV Serialai", diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json index 8b8d46b2e..dbcf17287 100644 --- a/Emby.Server.Implementations/Localization/Core/lv.json +++ b/Emby.Server.Implementations/Localization/Core/lv.json @@ -31,7 +31,6 @@ "TvShows": "TV Raidījumi", "Sync": "Sinhronizācija", "System": "Sistēma", - "SubtitlesDownloadedForItem": "Subtitri lejupielādēti priekš {0}", "StartupEmbyServerIsLoading": "Jellyfin Serveris lādējas. Lūdzu mēģiniet vēlreiz pēc brīža.", "Songs": "Dziesmas", "Shows": "Raidījumi", @@ -92,5 +91,27 @@ "HeaderFavoriteShows": "Raidījumu Favorīti", "HeaderFavoriteEpisodes": "Episožu Favorīti", "HeaderFavoriteArtists": "Izpildītāju Favorīti", - "HeaderFavoriteAlbums": "Albumu Favorīti" + "HeaderFavoriteAlbums": "Albumu Favorīti", + "TaskCleanCacheDescription": "Nodzēš keša datnes, kas vairs nav sistēmai vajadzīgas.", + "TaskRefreshChapterImages": "Izvilkt Nodaļu Attēlus", + "TasksApplicationCategory": "Lietotne", + "TasksLibraryCategory": "Bibliotēka", + "TaskDownloadMissingSubtitlesDescription": "Internetā meklē trūkstošus subtitrus pēc metadatu uzstādījumiem.", + "TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošus subtitrus", + "TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.", + "TaskRefreshChannels": "Atjaunot Kanālus", + "TaskCleanTranscodeDescription": "Izdzēš trans-kodēšanas datnes, kas ir vecākas par vienu dienu.", + "TaskCleanTranscode": "Iztīrīt Trans-kodēšanas Mapi", + "TaskUpdatePluginsDescription": "Lejupielādē un uzstāda atjauninājumus paplašinājumiem, kam ir uzstādīta automātiskā atjaunināšana.", + "TaskUpdatePlugins": "Atjaunot Paplašinājumus", + "TaskRefreshPeopleDescription": "Atjauno metadatus priekš aktieriem un direktoriem tavā mediju bibliotēkā.", + "TaskRefreshPeople": "Atjaunot Cilvēkus", + "TaskCleanLogsDescription": "Nodzēš log datnes, kas ir vairāk par {0} dienām vecas.", + "TaskCleanLogs": "Iztīrīt Logdatņu Mapi", + "TaskRefreshLibraryDescription": "Skenē tavas mediju bibliotēkas priekš jaunām datnēm un atjauno metadatus.", + "TaskRefreshLibrary": "Skanēt Mediju Bibliotēku", + "TaskRefreshChapterImagesDescription": "Izveido sīktēlus priekš video ar sadaļām.", + "TaskCleanCache": "Iztīrīt Kešošanas Mapi", + "TasksChannelsCategory": "Interneta Kanāli", + "TasksMaintenanceCategory": "Apkope" } diff --git a/Emby.Server.Implementations/Localization/Core/mk.json b/Emby.Server.Implementations/Localization/Core/mk.json index 684a97aad..8df137302 100644 --- a/Emby.Server.Implementations/Localization/Core/mk.json +++ b/Emby.Server.Implementations/Localization/Core/mk.json @@ -86,7 +86,6 @@ "TvShows": "ТВ Серии", "System": "Систем", "Sync": "Синхронизација", - "SubtitlesDownloadedForItem": "Спуштање превод за {0}", "SubtitleDownloadFailureFromForItem": "Преводот неуспешно се спушти од {0} за {1}", "StartupEmbyServerIsLoading": "Jellyfin Server се пушта. Ве молиме причекајте.", "Songs": "Песни", diff --git a/Emby.Server.Implementations/Localization/Core/mr.json b/Emby.Server.Implementations/Localization/Core/mr.json new file mode 100644 index 000000000..50b6360d8 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/mr.json @@ -0,0 +1,61 @@ +{ + "Books": "पुस्तकं", + "Artists": "संगीतकार", + "Albums": "अल्बम", + "Playlists": "प्लेलिस्ट", + "HeaderAlbumArtists": "अल्बम संगीतकार", + "Folders": "फोल्डर", + "HeaderFavoriteEpisodes": "आवडते भाग", + "HeaderFavoriteSongs": "आवडती गाणी", + "Movies": "चित्रपट", + "HeaderFavoriteArtists": "आवडते संगीतकार", + "Shows": "कार्यक्रम", + "HeaderFavoriteAlbums": "आवडते अल्बम", + "Channels": "वाहिन्या", + "ValueSpecialEpisodeName": "विशेष - {0}", + "HeaderFavoriteShows": "आवडते कार्यक्रम", + "Favorites": "आवडीचे", + "HeaderNextUp": "यानंतर", + "Songs": "गाणी", + "HeaderLiveTV": "लाइव्ह टीव्ही", + "Genres": "जाँनरे", + "Photos": "चित्र", + "TaskDownloadMissingSubtitles": "नसलेले सबटायटल डाउनलोड करा", + "TaskCleanTranscodeDescription": "एक दिवसापेक्षा जुन्या ट्रान्सकोड फायली काढून टाका.", + "TaskCleanTranscode": "ट्रान्सकोड डिरेक्टरी साफ करून टाका", + "TaskUpdatePlugins": "प्लगइन अपडेट करा", + "TaskCleanLogs": "लॉग डिरेक्टरी साफ करून टाका", + "TaskCleanCache": "कॅश डिरेक्टरी साफ करून टाका", + "TasksChannelsCategory": "इंटरनेट वाहिन्या", + "TasksApplicationCategory": "अॅप्लिकेशन", + "TasksLibraryCategory": "संग्रहालय", + "VersionNumber": "आवृत्ती {0}", + "UserPasswordChangedWithName": "{0} या प्रयोक्त्याचे पासवर्ड बदलण्यात आले आहे", + "UserOnlineFromDevice": "{0} हे {1} येथून ऑनलाइन आहेत", + "UserDeletedWithName": "प्रयोक्ता {0} काढून टाकण्यात आले आहे", + "UserCreatedWithName": "प्रयोक्ता {0} बनवण्यात आले आहे", + "User": "प्रयोक्ता", + "TvShows": "टीव्ही कार्यक्रम", + "StartupEmbyServerIsLoading": "जेलिफिन सर्व्हर लोड होत आहे. कृपया थोड्या वेळात पुन्हा प्रयत्न करा.", + "Plugin": "प्लगइन", + "NotificationOptionCameraImageUploaded": "कॅमेरा चित्र अपलोड केले आहे", + "NotificationOptionApplicationUpdateInstalled": "अॅप्लिकेशन अपडेट इन्स्टॉल केले आहे", + "NotificationOptionApplicationUpdateAvailable": "अॅप्लिकेशन अपडेट उपलब्ध आहे", + "NewVersionIsAvailable": "जेलिफिन सर्व्हरची एक नवीन आवृत्ती डाउनलोड करण्यास उपलब्ध आहे.", + "NameSeasonUnknown": "अज्ञात सीझन", + "NameSeasonNumber": "सीझन {0}", + "MusicVideos": "संगीत व्हिडीयो", + "Music": "संगीत", + "MessageApplicationUpdatedTo": "जेलिफिन सर्व्हर अपडेट होऊन {0} आवृत्तीवर पोहोचला आहे", + "MessageApplicationUpdated": "जेलिफिन सर्व्हर अपडेट केला गेला आहे", + "Latest": "नवीनतम", + "LabelIpAddressValue": "आयपी पत्ता: {0}", + "ItemRemovedWithName": "{0} हे संग्रहालयातून काढून टाकण्यात आले", + "ItemAddedWithName": "{0} हे संग्रहालयात जोडले गेले", + "HomeVideos": "घरचे व्हिडीयो", + "HeaderRecordingGroups": "रेकॉर्डिंग गट", + "HeaderCameraUploads": "कॅमेरा अपलोड", + "CameraImageUploadedFrom": "एक नवीन कॅमेरा चित्र {0} येथून अपलोड केले आहे", + "Application": "अॅप्लिकेशन", + "AppDeviceValues": "अॅप: {0}, यंत्र: {1}" +} diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json index 1d86257f8..79d078d4a 100644 --- a/Emby.Server.Implementations/Localization/Core/ms.json +++ b/Emby.Server.Implementations/Localization/Core/ms.json @@ -76,7 +76,6 @@ "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", "System": "Sistem", "TvShows": "TV Shows", diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index f9fa1b68c..e523ae90b 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "Jellyfin Server laster. Prøv igjen snart.", "SubtitleDownloadFailureForItem": "En feil oppstå under nedlasting av undertekster for {0}", "SubtitleDownloadFailureFromForItem": "Kunne ikke laste ned undertekster fra {0} for {1}", - "SubtitlesDownloadedForItem": "Undertekster lastet ned for {0}", "Sync": "Synkroniser", "System": "System", "TvShows": "TV-serier", @@ -93,5 +92,9 @@ "UserStoppedPlayingItemWithValues": "{0} har stoppet avspilling {1}", "ValueHasBeenAddedToLibrary": "{0} har blitt lagt til i mediebiblioteket ditt", "ValueSpecialEpisodeName": "Spesialepisode - {0}", - "VersionNumber": "Versjon {0}" + "VersionNumber": "Versjon {0}", + "TasksChannelsCategory": "Internett kanaler", + "TasksApplicationCategory": "Applikasjon", + "TasksLibraryCategory": "Bibliotek", + "TasksMaintenanceCategory": "Vedlikehold" } diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index e22f95ab4..3bc9c2a77 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "Jellyfin Server is aan het laden, probeer het later opnieuw.", "SubtitleDownloadFailureForItem": "Downloaden van ondertiteling voor {0} is mislukt", "SubtitleDownloadFailureFromForItem": "Ondertitels konden niet gedownload worden van {0} voor {1}", - "SubtitlesDownloadedForItem": "Ondertiteling voor {0} is gedownload", "Sync": "Synchronisatie", "System": "Systeem", "TvShows": "TV-series", @@ -93,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} heeft afspelen van {1} gestopt op {2}", "ValueHasBeenAddedToLibrary": "{0} is toegevoegd aan je mediabibliotheek", "ValueSpecialEpisodeName": "Speciaal - {0}", - "VersionNumber": "Versie {0}" + "VersionNumber": "Versie {0}", + "TaskDownloadMissingSubtitlesDescription": "Zoekt op het internet naar missende ondertitels gebaseerd op metadata configuratie.", + "TaskDownloadMissingSubtitles": "Download missende ondertitels", + "TaskRefreshChannelsDescription": "Vernieuwt informatie van internet kanalen.", + "TaskRefreshChannels": "Vernieuw Kanalen", + "TaskCleanTranscodeDescription": "Verwijder transcode bestanden ouder dan 1 dag.", + "TaskCleanLogs": "Log Folder Opschonen", + "TaskCleanTranscode": "Transcode Folder Opschonen", + "TaskUpdatePluginsDescription": "Download en installeert updates voor plugins waar automatisch updaten aan staat.", + "TaskUpdatePlugins": "Update Plugins", + "TaskRefreshPeopleDescription": "Update metadata for acteurs en regisseurs in de media bibliotheek.", + "TaskRefreshPeople": "Vernieuw Personen", + "TaskCleanLogsDescription": "Verwijdert log bestanden ouder dan {0} dagen.", + "TaskRefreshLibraryDescription": "Scant de media bibliotheek voor nieuwe bestanden en vernieuwt de metadata.", + "TaskRefreshLibrary": "Scan Media Bibliotheek", + "TaskRefreshChapterImagesDescription": "Maakt thumbnails aan voor videos met hoofdstukken.", + "TaskRefreshChapterImages": "Hoofdstukafbeeldingen Uitpakken", + "TaskCleanCacheDescription": "Verwijder gecachte bestanden die het systeem niet langer nodig heeft.", + "TaskCleanCache": "Cache Folder Opschonen", + "TasksChannelsCategory": "Internet Kanalen", + "TasksApplicationCategory": "Applicatie", + "TasksLibraryCategory": "Bibliotheek", + "TasksMaintenanceCategory": "Onderhoud" } diff --git a/Emby.Server.Implementations/Localization/Core/nn.json b/Emby.Server.Implementations/Localization/Core/nn.json index ec6da213f..281cadac5 100644 --- a/Emby.Server.Implementations/Localization/Core/nn.json +++ b/Emby.Server.Implementations/Localization/Core/nn.json @@ -36,5 +36,25 @@ "Artists": "Artistar", "Application": "Program", "AppDeviceValues": "App: {0}, Einheit: {1}", - "Albums": "Album" + "Albums": "Album", + "NotificationOptionServerRestartRequired": "Tenaren krev omstart", + "NotificationOptionPluginUpdateInstalled": "Tilleggsprogram-oppdatering vart installert", + "NotificationOptionPluginUninstalled": "Tilleggsprogram avinstallert", + "NotificationOptionPluginInstalled": "Tilleggsprogram installert", + "NotificationOptionPluginError": "Tilleggsprogram feila", + "NotificationOptionNewLibraryContent": "Nytt innhald er lagt til", + "NotificationOptionInstallationFailed": "Installasjonen feila", + "NotificationOptionCameraImageUploaded": "Kamerabilde vart lasta opp", + "NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppa", + "NotificationOptionAudioPlayback": "Lydavspilling påbyrja", + "NotificationOptionApplicationUpdateInstalled": "Applikasjonsoppdatering er installert", + "NotificationOptionApplicationUpdateAvailable": "Applikasjonsoppdatering er tilgjengeleg", + "NewVersionIsAvailable": "Ein ny versjon av Jellyfin serveren er tilgjengeleg for nedlasting.", + "NameSeasonUnknown": "Ukjend sesong", + "NameSeasonNumber": "Sesong {0}", + "NameInstallFailed": "{0} Installasjonen feila", + "MusicVideos": "Musikkvideoar", + "Music": "Musikk", + "Movies": "Filmar", + "MixedContent": "Blanda innhald" } diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json index e72f1a262..e9d9bbf2e 100644 --- a/Emby.Server.Implementations/Localization/Core/pl.json +++ b/Emby.Server.Implementations/Localization/Core/pl.json @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "Trwa wczytywanie serwera Jellyfin. Spróbuj ponownie za chwilę.", "SubtitleDownloadFailureForItem": "Pobieranie napisów dla {0} zakończone niepowodzeniem", "SubtitleDownloadFailureFromForItem": "Nieudane pobieranie napisów z {0} dla {1}", - "SubtitlesDownloadedForItem": "Pobrano napisy dla {0}", "Sync": "Synchronizacja", "System": "System", "TvShows": "Seriale", diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index 41a389e3b..3a69b6d7a 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-BR.json +++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "O Servidor Jellyfin está carregando. Por favor, tente novamente mais tarde.", "SubtitleDownloadFailureForItem": "Download de legendas falhou para {0}", "SubtitleDownloadFailureFromForItem": "Houve um problema ao baixar as legendas de {0} para {1}", - "SubtitlesDownloadedForItem": "Legendas baixadas para {0}", "Sync": "Sincronizar", "System": "Sistema", "TvShows": "Séries", @@ -93,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} parou de reproduzir {1} em {2}", "ValueHasBeenAddedToLibrary": "{0} foi adicionado à sua biblioteca de mídia", "ValueSpecialEpisodeName": "Especial - {0}", - "VersionNumber": "Versão {0}" + "VersionNumber": "Versão {0}", + "TaskDownloadMissingSubtitlesDescription": "Procurar na internet por legendas faltando baseado na configuração de metadados.", + "TaskDownloadMissingSubtitles": "Baixar legendas que estão faltando", + "TaskRefreshChannelsDescription": "Atualizar informação de canais da internet .", + "TaskRefreshChannels": "Atualizar Canais", + "TaskCleanTranscodeDescription": "Deletar arquivos de transcodificação com mais de um dia de criação.", + "TaskCleanTranscode": "Limpar pasta de transcodificação", + "TaskUpdatePluginsDescription": "Baixa e instala atualizações para plugins que estão configurados para atualizar automaticamente.", + "TaskUpdatePlugins": "Atualizar Plugins", + "TaskRefreshPeopleDescription": "Atualiza metadados para atores e diretores na sua biblioteca de mídia.", + "TaskRefreshPeople": "Atualizar pessoas", + "TaskCleanLogsDescription": "Deletar arquivos temporários com mais de {0} dias.", + "TaskCleanLogs": "Limpar pasta de logs", + "TaskRefreshLibraryDescription": "Escaneie a sua biblioteca de mídia para arquivos novos e atualize os metadados.", + "TaskRefreshLibrary": "Escanear a Biblioteca de Mídia", + "TaskRefreshChapterImagesDescription": "Criar miniaturas para vídeos que tem capítulos.", + "TaskRefreshChapterImages": "Extrair imagens dos capítulos", + "TaskCleanCacheDescription": "Deletar arquivos temporários que não são mais necessários para o sistema.", + "TaskCleanCache": "Limpar Arquivos Temporários", + "TasksChannelsCategory": "Canais da Internet", + "TasksApplicationCategory": "Aplicativo", + "TasksLibraryCategory": "Biblioteca", + "TasksMaintenanceCategory": "Manutenção" } diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json index b12d391c1..ebf35c492 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-PT.json +++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "O servidor Jellyfin está a iniciar. Tente novamente mais tarde.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Falha na transferência de legendas a partir de {0} para {1}", - "SubtitlesDownloadedForItem": "Transferidas legendas para {0}", "Sync": "Sincronização", "System": "Sistema", "TvShows": "Programas TV", diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index 9ee3c37a8..25c5b9053 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -31,7 +31,6 @@ "User": "Utilizador", "TvShows": "Séries", "System": "Sistema", - "SubtitlesDownloadedForItem": "Legendas transferidas para {0}", "SubtitleDownloadFailureFromForItem": "Falha na transferência de legendas de {0} para {1}", "StartupEmbyServerIsLoading": "O servidor Jellyfin está a iniciar. Tente novamente dentro de momentos.", "ServerNameNeedsToBeRestarted": "{0} necessita ser reiniciado", @@ -92,5 +91,17 @@ "CameraImageUploadedFrom": "Uma nova imagem da câmara foi enviada a partir de {0}", "AuthenticationSucceededWithUserName": "{0} autenticado com sucesso", "Application": "Aplicação", - "AppDeviceValues": "Aplicação {0}, Dispositivo: {1}" + "AppDeviceValues": "Aplicação {0}, Dispositivo: {1}", + "TaskCleanCache": "Limpar Diretório de Cache", + "TasksApplicationCategory": "Aplicação", + "TasksLibraryCategory": "Biblioteca", + "TasksMaintenanceCategory": "Manutenção", + "TaskRefreshChannels": "Atualizar Canais", + "TaskUpdatePlugins": "Atualizar Plugins", + "TaskCleanLogsDescription": "Deletar arquivos de log que existe a mais de {0} dias.", + "TaskCleanLogs": "Limpar diretório de log", + "TaskRefreshLibrary": "Escanear biblioteca de mídias", + "TaskRefreshChapterImagesDescription": "Criar miniaturas para videos que tem capítulos.", + "TaskCleanCacheDescription": "Deletar arquivos de cache que não são mais usados pelo sistema.", + "TasksChannelsCategory": "Canais de Internet" } diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json index 71bffffc6..699dd26da 100644 --- a/Emby.Server.Implementations/Localization/Core/ro.json +++ b/Emby.Server.Implementations/Localization/Core/ro.json @@ -17,7 +17,6 @@ "TvShows": "Spectacole TV", "System": "Sistem", "Sync": "Sincronizare", - "SubtitlesDownloadedForItem": "Subtitrari descarcate pentru {0}", "SubtitleDownloadFailureFromForItem": "Subtitrările nu au putut fi descărcate de la {0} pentru {1}", "StartupEmbyServerIsLoading": "Se încarcă serverul Jellyfin. Încercați din nou în scurt timp.", "Songs": "Melodii", @@ -92,5 +91,27 @@ "Artists": "Artiști", "Application": "Aplicație", "AppDeviceValues": "Aplicație: {0}, Dispozitiv: {1}", - "Albums": "Albume" + "Albums": "Albume", + "TaskDownloadMissingSubtitlesDescription": "Caută pe internet subtitrările lipsă pe baza configurației metadatelor.", + "TaskDownloadMissingSubtitles": "Descarcă subtitrările lipsă", + "TaskRefreshChannelsDescription": "Actualizează informațiile despre canalul de internet.", + "TaskRefreshChannels": "Actualizează canale", + "TaskCleanTranscodeDescription": "Șterge fișierele de transcodare mai vechi de o zi.", + "TaskCleanTranscode": "Curățați directorul de transcodare", + "TaskUpdatePluginsDescription": "Descarcă și instalează actualizări pentru pluginuri care sunt configurate să se actualizeze automat.", + "TaskUpdatePlugins": "Actualizați plugin-uri", + "TaskRefreshPeopleDescription": "Actualizează metadatele pentru actori și regizori din biblioteca media.", + "TaskRefreshPeople": "Actualizează oamenii", + "TaskCleanLogsDescription": "Șterge fișierele jurnal care au mai mult de {0} zile.", + "TaskCleanLogs": "Curățare director jurnal", + "TaskRefreshLibraryDescription": "Scanează biblioteca media pentru fișiere noi și reîmprospătează metadatele.", + "TaskRefreshLibrary": "Scanează Biblioteca Media", + "TaskRefreshChapterImagesDescription": "Creează miniaturi pentru videourile care au capitole.", + "TaskRefreshChapterImages": "Extrage Imaginile de Capitol", + "TaskCleanCacheDescription": "Șterge fișierele cache care nu mai sunt necesare sistemului.", + "TaskCleanCache": "Curățați directorul cache", + "TasksChannelsCategory": "Canale de pe Internet", + "TasksApplicationCategory": "Aplicație", + "TasksLibraryCategory": "Librărie", + "TasksMaintenanceCategory": "Mentenanță" } diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index 7cf957a94..71ee6446c 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -9,8 +9,8 @@ "Channels": "Каналы", "ChapterNameValue": "Сцена {0}", "Collections": "Коллекции", - "DeviceOfflineWithName": "{0} - подкл. разъ-но", - "DeviceOnlineWithName": "{0} - подкл. уст-но", + "DeviceOfflineWithName": "{0} - отключено", + "DeviceOnlineWithName": "{0} - подключено", "FailedLoginAttemptWithUserName": "{0} - попытка входа неудачна", "Favorites": "Избранное", "Folders": "Папки", @@ -26,30 +26,30 @@ "HeaderLiveTV": "Эфир", "HeaderNextUp": "Очередное", "HeaderRecordingGroups": "Группы записей", - "HomeVideos": "Дом. видео", + "HomeVideos": "Домашнее видео", "Inherit": "Наследуемое", "ItemAddedWithName": "{0} - добавлено в медиатеку", "ItemRemovedWithName": "{0} - изъято из медиатеки", "LabelIpAddressValue": "IP-адрес: {0}", "LabelRunningTimeValue": "Длительность: {0}", - "Latest": "Новейшее", + "Latest": "Последнее", "MessageApplicationUpdated": "Jellyfin Server был обновлён", "MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Конфиг-ия сервера (раздел {0}) была обновлена", - "MessageServerConfigurationUpdated": "Конфиг-ия сервера была обновлена", + "MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена", + "MessageServerConfigurationUpdated": "Конфигурация сервера была обновлена", "MixedContent": "Смешанное содержимое", "Movies": "Кино", "Music": "Музыка", - "MusicVideos": "Муз. видео", + "MusicVideos": "Музыкальные клипы", "NameInstallFailed": "Установка {0} неудачна", "NameSeasonNumber": "Сезон {0}", "NameSeasonUnknown": "Сезон неопознан", "NewVersionIsAvailable": "Новая версия Jellyfin Server доступна для загрузки.", "NotificationOptionApplicationUpdateAvailable": "Имеется обновление приложения", "NotificationOptionApplicationUpdateInstalled": "Обновление приложения установлено", - "NotificationOptionAudioPlayback": "Воспр-ие аудио зап-но", - "NotificationOptionAudioPlaybackStopped": "Восп-ие аудио ост-но", - "NotificationOptionCameraImageUploaded": "Произведена выкладка отснятого с камеры", + "NotificationOptionAudioPlayback": "Воспроизведение аудио запущено", + "NotificationOptionAudioPlaybackStopped": "Воспроизведение аудио остановлено", + "NotificationOptionCameraImageUploaded": "Изображения с камеры загружены", "NotificationOptionInstallationFailed": "Сбой установки", "NotificationOptionNewLibraryContent": "Новое содержание добавлено", "NotificationOptionPluginError": "Сбой плагина", @@ -59,8 +59,8 @@ "NotificationOptionServerRestartRequired": "Требуется перезапуск сервера", "NotificationOptionTaskFailed": "Сбой назначенной задачи", "NotificationOptionUserLockedOut": "Пользователь заблокирован", - "NotificationOptionVideoPlayback": "Воспр-ие видео зап-но", - "NotificationOptionVideoPlaybackStopped": "Восп-ие видео ост-но", + "NotificationOptionVideoPlayback": "Воспроизведение видео запущено", + "NotificationOptionVideoPlaybackStopped": "Воспроизведение видео остановлено", "Photos": "Фото", "Playlists": "Плей-листы", "Plugin": "Плагин", @@ -76,22 +76,43 @@ "StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.", "SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить", "SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}", - "SubtitlesDownloadedForItem": "Субтитры к {0} загружены", - "Sync": "Синхро", + "Sync": "Синхронизация", "System": "Система", "TvShows": "ТВ", - "User": "Польз-ль", + "User": "Пользователь", "UserCreatedWithName": "Пользователь {0} был создан", "UserDeletedWithName": "Пользователь {0} был удалён", "UserDownloadingItemWithValues": "{0} загружает {1}", "UserLockedOutWithName": "Пользователь {0} был заблокирован", - "UserOfflineFromDevice": "{0} - подкл. с {1} разъ-но", - "UserOnlineFromDevice": "{0} - подкл. с {1} уст-но", - "UserPasswordChangedWithName": "Пароль польз-ля {0} был изменён", - "UserPolicyUpdatedWithName": "Польз-ие политики {0} были обновлены", - "UserStartedPlayingItemWithValues": "{0} - воспр. «{1}» на {2}", - "UserStoppedPlayingItemWithValues": "{0} - воспр. «{1}» ост-но на {2}", + "UserOfflineFromDevice": "{0} отключился с {1}", + "UserOnlineFromDevice": "{0} подключился с {1}", + "UserPasswordChangedWithName": "Пароль пользователя {0} был изменён", + "UserPolicyUpdatedWithName": "Политики пользователя {0} были обновлены", + "UserStartedPlayingItemWithValues": "{0} - воспроизведение «{1}» на {2}", + "UserStoppedPlayingItemWithValues": "{0} - воспроизведение остановлено «{1}» на {2}", "ValueHasBeenAddedToLibrary": "{0} (добавлено в медиатеку)", - "ValueSpecialEpisodeName": "Спецэпизод - {0}", - "VersionNumber": "Версия {0}" + "ValueSpecialEpisodeName": "Специальный эпизод - {0}", + "VersionNumber": "Версия {0}", + "TaskDownloadMissingSubtitles": "Загрузка отсутствующих субтитров", + "TaskRefreshChannels": "Обновление каналов", + "TaskCleanTranscode": "Очистка каталога перекодировки", + "TaskUpdatePlugins": "Обновление плагинов", + "TaskRefreshPeople": "Обновление метаданных людей", + "TaskCleanLogs": "Очистка каталога журналов", + "TaskRefreshLibrary": "Сканирование медиатеки", + "TaskRefreshChapterImages": "Извлечение изображений сцен", + "TaskCleanCache": "Очистка каталога кеша", + "TasksChannelsCategory": "Интернет-каналы", + "TasksApplicationCategory": "Приложение", + "TasksLibraryCategory": "Медиатека", + "TasksMaintenanceCategory": "Обслуживание", + "TaskDownloadMissingSubtitlesDescription": "Выполняется поиск в Интернете отсутствующих субтитров на основе конфигурации метаданных.", + "TaskRefreshChannelsDescription": "Обновляются данные интернет-каналов.", + "TaskCleanTranscodeDescription": "Удаляются файлы перекодировки старше одного дня.", + "TaskUpdatePluginsDescription": "Загружаются и устанавливаются обновления для плагинов, у которых включено автоматическое обновление.", + "TaskRefreshPeopleDescription": "Обновляются метаданные актеров и режиссёров в медиатеке.", + "TaskCleanLogsDescription": "Удаляются файлы журнала, возраст которых превышает {0} дн(я/ей).", + "TaskRefreshLibraryDescription": "Сканируется медиатека на новые файлы и обновляются метаданные.", + "TaskRefreshChapterImagesDescription": "Создаются эскизы для видео, которые содержат сцены.", + "TaskCleanCacheDescription": "Удаляются файлы кэша, которые больше не нужны системе." } diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json index 1988bda52..0ee652637 100644 --- a/Emby.Server.Implementations/Localization/Core/sk.json +++ b/Emby.Server.Implementations/Localization/Core/sk.json @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "Jellyfin Server sa spúšťa. Prosím, skúste to o chvíľu znova.", "SubtitleDownloadFailureForItem": "Sťahovanie titulkov pre {0} zlyhalo", "SubtitleDownloadFailureFromForItem": "Sťahovanie titulkov z {0} pre {1} zlyhalo", - "SubtitlesDownloadedForItem": "Titulky pre {0} stiahnuté", "Sync": "Synchronizácia", "System": "Systém", "TvShows": "TV seriály", @@ -93,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} ukončil prehrávanie {1} na {2}", "ValueHasBeenAddedToLibrary": "{0} bol pridané do vašej knižnice médií", "ValueSpecialEpisodeName": "Špeciál - {0}", - "VersionNumber": "Verzia {0}" + "VersionNumber": "Verzia {0}", + "TaskDownloadMissingSubtitlesDescription": "Vyhľadá na internete chýbajúce titulky podľa toho, ako sú nakonfigurované metadáta.", + "TaskDownloadMissingSubtitles": "Stiahnuť chýbajúce titulky", + "TaskRefreshChannelsDescription": "Obnoví informácie o internetových kanáloch.", + "TaskRefreshChannels": "Obnoviť kanály", + "TaskCleanTranscodeDescription": "Vymaže súbory transkódovania, ktoré sú staršie ako jeden deň.", + "TaskCleanTranscode": "Vyčistiť priečinok pre transkódovanie", + "TaskUpdatePluginsDescription": "Stiahne a nainštaluje aktualizácie pre zásuvné moduly, ktoré sú nastavené tak, aby sa aktualizovali automaticky.", + "TaskUpdatePlugins": "Aktualizovať zásuvné moduly", + "TaskRefreshPeopleDescription": "Aktualizuje metadáta pre hercov a režisérov vo vašej mediálnej knižnici.", + "TaskRefreshPeople": "Obnoviť osoby", + "TaskCleanLogsDescription": "Vymaže log súbory, ktoré su staršie ako {0} deň/dni/dní.", + "TaskCleanLogs": "Vyčistiť priečinok s logmi", + "TaskRefreshLibraryDescription": "Hľadá vo vašej mediálnej knižnici nové súbory a obnovuje metadáta.", + "TaskRefreshLibrary": "Prehľadávať knižnicu medií", + "TaskRefreshChapterImagesDescription": "Vytvorí náhľady pre videá, ktoré majú kapitoly.", + "TaskRefreshChapterImages": "Extrahovať obrázky kapitol", + "TaskCleanCacheDescription": "Vymaže cache súbory, ktoré nie sú už potrebné pre systém.", + "TaskCleanCache": "Vyčistiť Cache priečinok", + "TasksChannelsCategory": "Internetové kanály", + "TasksApplicationCategory": "Aplikácia", + "TasksLibraryCategory": "Knižnica", + "TasksMaintenanceCategory": "Údržba" } diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json index 0fc8379de..b60dd33bd 100644 --- a/Emby.Server.Implementations/Localization/Core/sl-SI.json +++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "Jellyfin Server se nalaga. Poskusi ponovno kasneje.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Neuspešen prenos podnapisov iz {0} za {1}", - "SubtitlesDownloadedForItem": "Podnapisi preneseni za {0}", "Sync": "Sinhroniziraj", "System": "System", "TvShows": "TV serije", diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json index da0088991..5f3cbb1c8 100644 --- a/Emby.Server.Implementations/Localization/Core/sr.json +++ b/Emby.Server.Implementations/Localization/Core/sr.json @@ -17,7 +17,6 @@ "TvShows": "ТВ серије", "System": "Систем", "Sync": "Усклади", - "SubtitlesDownloadedForItem": "Титлови преузети за {0}", "SubtitleDownloadFailureFromForItem": "Неуспело преузимање титлова за {1} са {0}", "StartupEmbyServerIsLoading": "Џелифин сервер се подиже. Покушајте поново убрзо.", "Songs": "Песме", @@ -82,7 +81,7 @@ "Favorites": "Омиљено", "FailedLoginAttemptWithUserName": "Неуспела пријава са {0}", "DeviceOnlineWithName": "{0} се повезао", - "DeviceOfflineWithName": "{0} се одвезао", + "DeviceOfflineWithName": "{0} је прекинуо везу", "Collections": "Колекције", "ChapterNameValue": "Поглавље {0}", "Channels": "Канали", @@ -92,5 +91,27 @@ "Artists": "Извођач", "Application": "Апликација", "AppDeviceValues": "Апл: {0}, уређај: {1}", - "Albums": "Албуми" + "Albums": "Албуми", + "TaskDownloadMissingSubtitlesDescription": "Претражује интернет за недостајуће титлове на основу конфигурације метаподатака.", + "TaskDownloadMissingSubtitles": "Преузмите недостајуће титлове", + "TaskRefreshChannelsDescription": "Освежава информације о интернет каналу.", + "TaskRefreshChannels": "Освежи канале", + "TaskCleanTranscodeDescription": "Брише датотеке за кодирање старије од једног дана.", + "TaskCleanTranscode": "Очистите директоријум преноса", + "TaskUpdatePluginsDescription": "Преузима и инсталира исправке за додатке који су конфигурисани за аутоматско ажурирање.", + "TaskUpdatePlugins": "Ажурирајте додатке", + "TaskRefreshPeopleDescription": "Ажурира метаподатке за глумце и редитеље у вашој медијској библиотеци.", + "TaskRefreshPeople": "Освежите људе", + "TaskCleanLogsDescription": "Брише логове старије од {0} дана.", + "TaskCleanLogs": "Очистите директоријум логова", + "TaskRefreshLibraryDescription": "Скенира вашу медијску библиотеку за нове датотеке и освежава метаподатке.", + "TaskRefreshLibrary": "Скенирај Библиотеку Медија", + "TaskRefreshChapterImagesDescription": "Ствара сличице за видео записе који имају поглавља.", + "TaskRefreshChapterImages": "Издвоји слике из поглавља", + "TaskCleanCacheDescription": "Брише Кеш фајлове који више нису потребни систему.", + "TaskCleanCache": "Очистите Кеш Директоријум", + "TasksChannelsCategory": "Интернет канали", + "TasksApplicationCategory": "Апликација", + "TasksLibraryCategory": "Библиотека", + "TasksMaintenanceCategory": "Одржавање" } diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json index b2934545d..b7c50394a 100644 --- a/Emby.Server.Implementations/Localization/Core/sv.json +++ b/Emby.Server.Implementations/Localization/Core/sv.json @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "Jellyfin Server arbetar. Pröva igen snart.", "SubtitleDownloadFailureForItem": "Nerladdning av undertexter för {0} misslyckades", "SubtitleDownloadFailureFromForItem": "Undertexter kunde inte laddas ner från {0} för {1}", - "SubtitlesDownloadedForItem": "Undertexter har laddats ner till {0}", "Sync": "Synk", "System": "System", "TvShows": "TV-serier", @@ -93,5 +92,26 @@ "UserStoppedPlayingItemWithValues": "{0} har avslutat uppspelningen av {1} på {2}", "ValueHasBeenAddedToLibrary": "{0} har lagts till i ditt mediebibliotek", "ValueSpecialEpisodeName": "Specialavsnitt - {0}", - "VersionNumber": "Version {0}" + "VersionNumber": "Version {0}", + "TaskDownloadMissingSubtitlesDescription": "Söker på internet efter saknade undertexter baserad på metadatas konfiguration.", + "TaskDownloadMissingSubtitles": "Ladda ned saknade undertexter", + "TaskRefreshChannelsDescription": "Uppdaterar information för internetkanaler.", + "TaskRefreshChannels": "Uppdatera kanaler", + "TaskCleanTranscodeDescription": "Raderar transkodningsfiler som är mer än en dag gamla.", + "TaskCleanTranscode": "Töm transkodningskatalog", + "TaskUpdatePluginsDescription": "Laddar ned och installerar uppdateringar till insticksprogram som är konfigurerade att uppdateras automatiskt.", + "TaskUpdatePlugins": "Uppdatera insticksprogram", + "TaskRefreshPeopleDescription": "Uppdaterar metadata för skådespelare och regissörer i ditt mediabibliotek.", + "TaskCleanLogsDescription": "Raderar loggfiler som är mer än {0} dagar gamla.", + "TaskCleanLogs": "Töm loggkatalog", + "TaskRefreshLibraryDescription": "Söker igenom ditt mediabibliotek efter nya filer och förnyar metadata.", + "TaskRefreshLibrary": "Genomsök mediabibliotek", + "TaskRefreshChapterImagesDescription": "Skapa miniatyrbilder för videor med kapitel.", + "TaskRefreshChapterImages": "Extrahera kapitelbilder", + "TaskCleanCacheDescription": "Radera cachade filer som systemet inte längre behöver.", + "TaskCleanCache": "Rensa cachekatalog", + "TasksChannelsCategory": "Internetkanaler", + "TasksApplicationCategory": "Applikation", + "TasksLibraryCategory": "Bibliotek", + "TasksMaintenanceCategory": "Underhåll" } diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index d3552225b..62d205516 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "Jellyfin Sunucusu yükleniyor. Lütfen kısa süre sonra tekrar deneyin.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "{1} için alt yazılar {0} 'dan indirilemedi", - "SubtitlesDownloadedForItem": "{0} için altyazılar indirildi", "Sync": "Eşitle", "System": "Sistem", "TvShows": "Diziler", @@ -93,5 +92,10 @@ "UserStoppedPlayingItemWithValues": "{0}, {2} cihazında {1} izlemeyi bitirdi", "ValueHasBeenAddedToLibrary": "Medya kitaplığınıza {0} eklendi", "ValueSpecialEpisodeName": "Özel - {0}", - "VersionNumber": "Versiyon {0}" + "VersionNumber": "Versiyon {0}", + "TaskCleanCache": "Geçici dosya klasörünü temizle", + "TasksChannelsCategory": "İnternet kanalları", + "TasksApplicationCategory": "Yazılım", + "TasksLibraryCategory": "Kütüphane", + "TasksMaintenanceCategory": "Onarım" } diff --git a/Emby.Server.Implementations/Localization/Core/ur_PK.json b/Emby.Server.Implementations/Localization/Core/ur_PK.json new file mode 100644 index 000000000..9a5874e29 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/ur_PK.json @@ -0,0 +1,117 @@ +{ + "HeaderFavoriteAlbums": "پسندیدہ البمز", + "HeaderNextUp": "اگلا", + "HeaderFavoriteArtists": "پسندیدہ فنکار", + "HeaderAlbumArtists": "البم کے فنکار", + "Movies": "فلمیں", + "HeaderFavoriteEpisodes": "پسندیدہ اقساط", + "Collections": "مجموعہ", + "Folders": "فولڈرز", + "HeaderLiveTV": "براہ راست ٹی وی", + "Channels": "چینل", + "HeaderContinueWatching": "دیکھنا جاری رکھیں", + "Playlists": "پلے لسٹس", + "ValueSpecialEpisodeName": "خاص - {0}", + "Shows": "شوز", + "Genres": "انواع", + "Artists": "فنکار", + "Sync": "مطابقت", + "Photos": "تصوریں", + "Albums": "البم", + "Favorites": "پسندیدہ", + "Songs": "گانے", + "Books": "کتابیں", + "HeaderFavoriteSongs": "پسندیدہ گانے", + "HeaderFavoriteShows": "پسندیدہ شوز", + "TaskDownloadMissingSubtitlesDescription": "میٹا ڈیٹا کی تشکیل پر مبنی ذیلی عنوانات کے غائب عنوانات انٹرنیٹ پے تلاش کرتا ہے۔", + "TaskDownloadMissingSubtitles": "غائب سب ٹائٹلز ڈاؤن لوڈ کریں", + "TaskRefreshChannelsDescription": "انٹرنیٹ چینل کی معلومات کو تازہ دم کرتا ہے۔", + "TaskRefreshChannels": "چینلز ریفریش کریں", + "TaskCleanTranscodeDescription": "ایک دن سے زیادہ پرانی ٹرانسکوڈ فائلوں کو حذف کرتا ہے۔", + "TaskCleanTranscode": "ٹرانس کوڈ ڈائرکٹری صاف کریں", + "TaskUpdatePluginsDescription": "پلگ انز کے لئے اپ ڈیٹس ڈاؤن لوڈ اور انسٹال کرتے ہیں جو خود بخود اپ ڈیٹ کرنے کیلئے تشکیل شدہ ہیں۔", + "TaskUpdatePlugins": "پلگ انز کو اپ ڈیٹ کریں", + "TaskRefreshPeopleDescription": "آپ کی میڈیا لائبریری میں اداکاروں اور ہدایت کاروں کے لئے میٹا ڈیٹا کی تازہ کاری۔", + "TaskRefreshPeople": "لوگوں کو تروتازہ کریں", + "TaskCleanLogsDescription": "لاگ فائلوں کو حذف کریں جو {0} دن سے زیادہ پرانی ہیں۔", + "TaskCleanLogs": "لاگ ڈائرکٹری کو صاف کریں", + "TaskRefreshLibraryDescription": "میڈیا لائبریری کو اسکین کرتا ھے ہر میٹا دیٹا کہ تازہ دم کرتا ھے.", + "TaskRefreshLibrary": "اسکین میڈیا لائبریری", + "TaskRefreshChapterImagesDescription": "بابوں والی ویڈیوز کے لئے تمبنیل بنایں۔", + "TaskRefreshChapterImages": "باب کی تصاویر نکالیں", + "TaskCleanCacheDescription": "فائلوں کو حذف کریں جنکی ضرورت نھیں ھے۔", + "TaskCleanCache": "کیش ڈائرکٹری کلیر کریں", + "TasksChannelsCategory": "انٹرنیٹ چینلز", + "TasksApplicationCategory": "پروگرام", + "TasksLibraryCategory": "لآیبریری", + "TasksMaintenanceCategory": "مرمت", + "VersionNumber": "ورژن {0}", + "ValueHasBeenAddedToLibrary": "{0} آپ کی میڈیا لائبریری میں شامل کر دیا گیا ہے", + "UserStoppedPlayingItemWithValues": "{0} نے {1} چلانا ختم کر دیا ھے {2} پے", + "UserStartedPlayingItemWithValues": "{0} چلا رہا ہے {1} {2} پے", + "UserPolicyUpdatedWithName": "صارف {0} کی پالیسی کیلئے تازہ کاری کی گئی ہے", + "UserPasswordChangedWithName": "صارف {0} کے لئے پاس ورڈ تبدیل کر دیا گیا ہے", + "UserOnlineFromDevice": "{0} آن لائن ہے {1} سے", + "UserOfflineFromDevice": "{0} سے منقطع ہوگیا ہے {1}", + "UserLockedOutWithName": "صارف {0} کو لاک آؤٹ کردیا گیا ہے", + "UserDownloadingItemWithValues": "{0} ڈاؤن لوڈ کر رھا ھے {1}", + "UserDeletedWithName": "صارف {0} کو ہٹا دیا گیا ہے", + "UserCreatedWithName": "صارف {0} تشکیل دیا گیا ہے", + "User": "صارف", + "TvShows": "ٹی وی کے پروگرام", + "System": "نظام", + "SubtitleDownloadFailureFromForItem": "ذیلی عنوانات {0} سے ڈاؤن لوڈ کرنے میں ناکام {1} کے لیے", + "StartupEmbyServerIsLoading": "جیلیفن سرور لوڈ ہورہا ہے۔ براہ کرم جلد ہی دوبارہ کوشش کریں۔", + "ServerNameNeedsToBeRestarted": "{0} دوبارہ چلانے کرنے کی ضرورت ہے", + "ScheduledTaskStartedWithName": "{0} شروع", + "ScheduledTaskFailedWithName": "{0} ناکام", + "ProviderValue": "فراہم کرنے والا: {0}", + "PluginUpdatedWithName": "{0} تازہ کاری کی گئی تھی", + "PluginUninstalledWithName": "[0} ہٹا دیا گیا تھا", + "PluginInstalledWithName": "{0} انسٹال کیا گیا تھا", + "Plugin": "پلگن", + "NotificationOptionVideoPlaybackStopped": "ویڈیو پلے بیک رک گیا", + "NotificationOptionVideoPlayback": "ویڈیو پلے بیک شروع ہوا", + "NotificationOptionUserLockedOut": "صارف کو لاک آؤٹ کیا گیا", + "NotificationOptionTaskFailed": "طے شدہ کام کی ناکامی", + "NotificationOptionServerRestartRequired": "سرور دوبارہ چلانے کرنے کی ضرورت ہے", + "NotificationOptionPluginUpdateInstalled": "پلگ ان اپ ڈیٹ انسٹال", + "NotificationOptionPluginUninstalled": "پلگ ان ہٹا دیا گیا", + "NotificationOptionPluginInstalled": "پلگ ان انسٹال ہوا", + "NotificationOptionPluginError": "پلگ ان کی ناکامی", + "NotificationOptionNewLibraryContent": "نیا مواد شامل کیا گیا", + "NotificationOptionInstallationFailed": "تنصیب کی ناکامی", + "NotificationOptionCameraImageUploaded": "کیمرے کی تصویر اپ لوڈ ہوگئی", + "NotificationOptionAudioPlaybackStopped": "آڈیو پلے بیک رک گیا", + "NotificationOptionAudioPlayback": "آڈیو پلے بیک شروع ہوا", + "NotificationOptionApplicationUpdateInstalled": "پروگرام اپ ڈیٹ انسٹال ہوچکا ھے", + "NotificationOptionApplicationUpdateAvailable": "پروگرام کی تازہ کاری دستیاب ہے", + "NewVersionIsAvailable": "جیلیفن سرور کا ایک نیا ورژن ڈاؤن لوڈ کے لئے دستیاب ہے۔", + "NameSeasonUnknown": "نامعلوم باب", + "NameSeasonNumber": "باب {0}", + "NameInstallFailed": "{0} تنصیب ناکام ہوگئی", + "MusicVideos": "موسیقی ویڈیو", + "Music": "موسیقی", + "MixedContent": "مخلوط مواد", + "MessageServerConfigurationUpdated": "سرور کو اپ ڈیٹ کر دیا گیا ہے", + "MessageNamedServerConfigurationUpdatedWithValue": "سرور ضمن {0} کو ترتیب دے دیا گیا ھے", + "MessageApplicationUpdatedTo": "جیلیفن سرور کو اپ ڈیٹ کیا ہے {0}", + "MessageApplicationUpdated": "جیلیفن سرور کو اپ ڈیٹ کر دیا گیا ہے", + "Latest": "تازہ ترین", + "LabelRunningTimeValue": "چلانے کی مدت", + "LabelIpAddressValue": "ای پی پتے {0}", + "ItemRemovedWithName": "لائبریری سے ہٹا دیا گیا ھے", + "ItemAddedWithName": "[0} لائبریری میں شامل کیا گیا ھے", + "Inherit": "وراثت میں", + "HomeVideos": "ہوم ویڈیو", + "HeaderRecordingGroups": "ریکارڈنگ گروپس", + "HeaderCameraUploads": "کیمرہ اپلوڈز", + "FailedLoginAttemptWithUserName": "لاگن کئ کوشش ناکام {0}", + "DeviceOnlineWithName": "{0} متصل ھو چکا ھے", + "DeviceOfflineWithName": "{0} منقطع ھو چکا ھے", + "ChapterNameValue": "باب", + "AuthenticationSucceededWithUserName": "{0} کامیابی کے ساتھ تصدیق ھوچکی ھے", + "CameraImageUploadedFrom": "ایک نئی کیمرہ تصویر اپ لوڈ کی گئی ہے {0}", + "Application": "پروگرام", + "AppDeviceValues": "پروگرام:{0}, آلہ:{1}" +} diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index 85ebce0f5..6b563a9b1 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -3,11 +3,11 @@ "AppDeviceValues": "应用: {0}, 设备: {1}", "Application": "应用程序", "Artists": "艺术家", - "AuthenticationSucceededWithUserName": "{0} 验证成功", + "AuthenticationSucceededWithUserName": "{0} 认证成功", "Books": "书籍", "CameraImageUploadedFrom": "新的相机图像已从 {0} 上传", "Channels": "频道", - "ChapterNameValue": "第 {0} 章", + "ChapterNameValue": "第 {0} 集", "Collections": "合集", "DeviceOfflineWithName": "{0} 已断开", "DeviceOnlineWithName": "{0} 已连接", @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "Jellyfin 服务器加载中。请稍后再试。", "SubtitleDownloadFailureForItem": "为 {0} 下载字幕失败", "SubtitleDownloadFailureFromForItem": "无法从 {0} 下载 {1} 的字幕", - "SubtitlesDownloadedForItem": "已为 {0} 下载了字幕", "Sync": "同步", "System": "系统", "TvShows": "电视剧", @@ -93,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} 已在 {2} 上停止播放 {1}", "ValueHasBeenAddedToLibrary": "{0} 已添加至您的媒体库中", "ValueSpecialEpisodeName": "特典 - {0}", - "VersionNumber": "版本 {0}" + "VersionNumber": "版本 {0}", + "TaskUpdatePluginsDescription": "为已设置为自动更新的插件下载和安装更新。", + "TaskRefreshPeople": "刷新人员", + "TasksChannelsCategory": "互联网频道", + "TasksLibraryCategory": "媒体库", + "TaskDownloadMissingSubtitlesDescription": "根据元数据设置在互联网上搜索缺少的字幕。", + "TaskDownloadMissingSubtitles": "下载缺少的字幕", + "TaskRefreshChannelsDescription": "刷新互联网频道信息。", + "TaskRefreshChannels": "刷新频道", + "TaskCleanTranscodeDescription": "删除存在超过 1 天的转码文件。", + "TaskCleanTranscode": "清理转码目录", + "TaskUpdatePlugins": "更新插件", + "TaskRefreshPeopleDescription": "更新媒体库中演员和导演的元数据。", + "TaskCleanLogsDescription": "删除存在超过 {0} 天的的日志文件。", + "TaskCleanLogs": "清理日志目录", + "TaskRefreshLibraryDescription": "扫描你的媒体库以获取新文件并刷新元数据。", + "TaskRefreshLibrary": "扫描媒体库", + "TaskRefreshChapterImagesDescription": "为包含剧集的视频提取缩略图。", + "TaskRefreshChapterImages": "提取剧集图片", + "TaskCleanCacheDescription": "删除系统不再需要的缓存文件。", + "TaskCleanCache": "清理缓存目录", + "TasksApplicationCategory": "应用程序", + "TasksMaintenanceCategory": "维护" } diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index f3d9e5fce..224748e61 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -76,7 +76,6 @@ "StartupEmbyServerIsLoading": "Jellyfin 伺服器載入中,請稍後再試。", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "無法從 {0} 下載 {1} 的字幕", - "SubtitlesDownloadedForItem": "已為 {0} 下載了字幕", "Sync": "同步", "System": "System", "TvShows": "電視節目", diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json index acd211f22..c423c7ea7 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-TW.json +++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json @@ -20,7 +20,7 @@ "HeaderContinueWatching": "繼續觀賞", "HeaderFavoriteAlbums": "最愛專輯", "HeaderFavoriteArtists": "最愛演出者", - "HeaderFavoriteEpisodes": "最愛級數", + "HeaderFavoriteEpisodes": "最愛影集", "HeaderFavoriteShows": "最愛節目", "HeaderFavoriteSongs": "最愛歌曲", "HeaderLiveTV": "電視直播", @@ -72,7 +72,6 @@ "Shows": "節目", "Songs": "歌曲", "StartupEmbyServerIsLoading": "Jellyfin Server正在啟動,請稍後再試一次。", - "SubtitlesDownloadedForItem": "已為 {0} 下載字幕", "Sync": "同步", "System": "系統", "TvShows": "電視節目", diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index 840aca7a6..677d68b4c 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -26,14 +26,20 @@ namespace Emby.Server.Implementations.MediaEncoder private readonly IChapterManager _chapterManager; private readonly ILibraryManager _libraryManager; + /// <summary> + /// The first chapter ticks. + /// </summary> + private static readonly long _firstChapterTicks = TimeSpan.FromSeconds(15).Ticks; + public EncodingManager( + ILogger<EncodingManager> logger, IFileSystem fileSystem, - ILoggerFactory loggerFactory, IMediaEncoder encoder, - IChapterManager chapterManager, ILibraryManager libraryManager) + IChapterManager chapterManager, + ILibraryManager libraryManager) { + _logger = logger; _fileSystem = fileSystem; - _logger = loggerFactory.CreateLogger(nameof(EncodingManager)); _encoder = encoder; _chapterManager = chapterManager; _libraryManager = libraryManager; @@ -97,12 +103,7 @@ namespace Emby.Server.Implementations.MediaEncoder return video.DefaultVideoStreamIndex.HasValue; } - /// <summary> - /// The first chapter ticks - /// </summary> - private static readonly long FirstChapterTicks = TimeSpan.FromSeconds(15).Ticks; - - public async Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, List<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken) + public async Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken) { if (!IsEligibleForChapterImageExtraction(video)) { @@ -135,7 +136,7 @@ namespace Emby.Server.Implementations.MediaEncoder try { // Add some time for the first chapter to make sure we don't end up with a black image - var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(FirstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks); + var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(_firstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks); var protocol = MediaProtocol.File; @@ -152,9 +153,9 @@ namespace Emby.Server.Implementations.MediaEncoder { _fileSystem.DeleteFile(tempFile); } - catch + catch (IOException ex) { - + _logger.LogError(ex, "Error deleting temporary chapter image encoding file {Path}", tempFile); } chapter.ImagePath = path; @@ -184,7 +185,7 @@ namespace Emby.Server.Implementations.MediaEncoder if (saveChapters && changesMade) { - _chapterManager.SaveChapters(video.Id.ToString(), chapters); + _chapterManager.SaveChapters(video.Id, chapters); } DeleteDeadImages(currentImages, chapters); @@ -199,22 +200,21 @@ namespace Emby.Server.Implementations.MediaEncoder return Path.Combine(GetChapterImagesPath(video), filename); } - private static List<string> GetSavedChapterImages(Video video, IDirectoryService directoryService) + private static IReadOnlyList<string> GetSavedChapterImages(Video video, IDirectoryService directoryService) { var path = GetChapterImagesPath(video); if (!Directory.Exists(path)) { - return new List<string>(); + return Array.Empty<string>(); } try { - return directoryService.GetFilePaths(path) - .ToList(); + return directoryService.GetFilePaths(path); } catch (IOException) { - return new List<string>(); + return Array.Empty<string>(); } } @@ -227,7 +227,7 @@ namespace Emby.Server.Implementations.MediaEncoder foreach (var image in deadImages) { - _logger.LogDebug("Deleting dead chapter image {path}", image); + _logger.LogDebug("Deleting dead chapter image {Path}", image); try { @@ -235,7 +235,7 @@ namespace Emby.Server.Implementations.MediaEncoder } catch (IOException ex) { - _logger.LogError(ex, "Error deleting {path}.", image); + _logger.LogError(ex, "Error deleting {Path}.", image); } } } diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs index 1d8d3cf39..b3e88b667 100644 --- a/Emby.Server.Implementations/Networking/NetworkManager.cs +++ b/Emby.Server.Implementations/Networking/NetworkManager.cs @@ -500,7 +500,7 @@ namespace Emby.Server.Implementations.Networking { if (ip.Address.Equals(address) && ip.IPv4Mask != null) { - return ip.IPv4Mask; + return ip.IPv4Mask; } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index 5822c467b..ea6a70615 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -15,6 +15,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; +using MediaBrowser.Model.Globalization; namespace Emby.Server.Implementations.ScheduledTasks { @@ -39,11 +40,19 @@ namespace Emby.Server.Implementations.ScheduledTasks private readonly IEncodingManager _encodingManager; private readonly IFileSystem _fileSystem; + private readonly ILocalizationManager _localization; /// <summary> /// Initializes a new instance of the <see cref="ChapterImagesTask" /> class. /// </summary> - public ChapterImagesTask(ILoggerFactory loggerFactory, ILibraryManager libraryManager, IItemRepository itemRepo, IApplicationPaths appPaths, IEncodingManager encodingManager, IFileSystem fileSystem) + public ChapterImagesTask( + ILoggerFactory loggerFactory, + ILibraryManager libraryManager, + IItemRepository itemRepo, + IApplicationPaths appPaths, + IEncodingManager encodingManager, + IFileSystem fileSystem, + ILocalizationManager localization) { _logger = loggerFactory.CreateLogger(GetType().Name); _libraryManager = libraryManager; @@ -51,6 +60,7 @@ namespace Emby.Server.Implementations.ScheduledTasks _appPaths = appPaths; _encodingManager = encodingManager; _fileSystem = fileSystem; + _localization = localization; } /// <summary> @@ -159,11 +169,11 @@ namespace Emby.Server.Implementations.ScheduledTasks } } - public string Name => "Extract Chapter Images"; + public string Name => _localization.GetLocalizedString("TaskRefreshChapterImages"); - public string Description => "Creates thumbnails for videos that have chapters."; + public string Description => _localization.GetLocalizedString("TaskRefreshChapterImagesDescription"); - public string Category => "Library"; + public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); public string Key => "RefreshChapterImages"; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs index b7668c872..9df7c538b 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs @@ -8,6 +8,7 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; +using MediaBrowser.Model.Globalization; namespace Emby.Server.Implementations.ScheduledTasks.Tasks { @@ -25,6 +26,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks private readonly ILogger _logger; private readonly IFileSystem _fileSystem; + private readonly ILocalizationManager _localization; /// <summary> /// Initializes a new instance of the <see cref="DeleteCacheFileTask" /> class. @@ -32,11 +34,13 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks public DeleteCacheFileTask( IApplicationPaths appPaths, ILogger<DeleteCacheFileTask> logger, - IFileSystem fileSystem) + IFileSystem fileSystem, + ILocalizationManager localization) { ApplicationPaths = appPaths; _logger = logger; _fileSystem = fileSystem; + _localization = localization; } /// <summary> @@ -161,11 +165,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks } } - public string Name => "Clean Cache Directory"; + public string Name => _localization.GetLocalizedString("TaskCleanCache"); - public string Description => "Deletes cache files no longer needed by the system."; + public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription"); - public string Category => "Maintenance"; + public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); public string Key => "DeleteCacheFiles"; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs index 9f9c6353a..3140aa489 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; +using MediaBrowser.Model.Globalization; namespace Emby.Server.Implementations.ScheduledTasks.Tasks { @@ -21,15 +22,17 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks private IConfigurationManager ConfigurationManager { get; set; } private readonly IFileSystem _fileSystem; + private readonly ILocalizationManager _localization; /// <summary> /// Initializes a new instance of the <see cref="DeleteLogFileTask" /> class. /// </summary> /// <param name="configurationManager">The configuration manager.</param> - public DeleteLogFileTask(IConfigurationManager configurationManager, IFileSystem fileSystem) + public DeleteLogFileTask(IConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization) { ConfigurationManager = configurationManager; _fileSystem = fileSystem; + _localization = localization; } /// <summary> @@ -79,11 +82,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks return Task.CompletedTask; } - public string Name => "Clean Log Directory"; + public string Name => _localization.GetLocalizedString("TaskCleanLogs"); - public string Description => string.Format("Deletes log files that are more than {0} days old.", ConfigurationManager.CommonConfiguration.LogFileRetentionDays); + public string Description => string.Format(_localization.GetLocalizedString("TaskCleanLogsDescription"), ConfigurationManager.CommonConfiguration.LogFileRetentionDays); - public string Category => "Maintenance"; + public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); public string Key => "CleanLogFiles"; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs index 37136f438..1d133dcda 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs @@ -8,6 +8,7 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; +using MediaBrowser.Model.Globalization; namespace Emby.Server.Implementations.ScheduledTasks.Tasks { @@ -19,6 +20,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks private readonly ILogger _logger; private readonly IConfigurationManager _configurationManager; private readonly IFileSystem _fileSystem; + private readonly ILocalizationManager _localization; /// <summary> /// Initializes a new instance of the <see cref="DeleteTranscodeFileTask" /> class. @@ -26,11 +28,13 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks public DeleteTranscodeFileTask( ILogger<DeleteTranscodeFileTask> logger, IFileSystem fileSystem, - IConfigurationManager configurationManager) + IConfigurationManager configurationManager, + ILocalizationManager localization) { _logger = logger; _fileSystem = fileSystem; _configurationManager = configurationManager; + _localization = localization; } /// <summary> @@ -128,11 +132,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks } } - public string Name => "Clean Transcode Directory"; + public string Name => _localization.GetLocalizedString("TaskCleanTranscode"); - public string Description => "Deletes transcode files more than one day old."; + public string Description => _localization.GetLocalizedString("TaskCleanTranscodeDescription"); - public string Category => "Maintenance"; + public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); public string Key => "DeleteTranscodeFiles"; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs index eaf17aace..63f867bf6 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using MediaBrowser.Controller; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Tasks; +using MediaBrowser.Model.Globalization; namespace Emby.Server.Implementations.ScheduledTasks { @@ -19,16 +20,18 @@ namespace Emby.Server.Implementations.ScheduledTasks private readonly ILibraryManager _libraryManager; private readonly IServerApplicationHost _appHost; + private readonly ILocalizationManager _localization; /// <summary> /// Initializes a new instance of the <see cref="PeopleValidationTask" /> class. /// </summary> /// <param name="libraryManager">The library manager.</param> /// <param name="appHost">The server application host</param> - public PeopleValidationTask(ILibraryManager libraryManager, IServerApplicationHost appHost) + public PeopleValidationTask(ILibraryManager libraryManager, IServerApplicationHost appHost, ILocalizationManager localization) { _libraryManager = libraryManager; _appHost = appHost; + _localization = localization; } /// <summary> @@ -57,11 +60,11 @@ namespace Emby.Server.Implementations.ScheduledTasks return _libraryManager.ValidatePeople(cancellationToken, progress); } - public string Name => "Refresh People"; + public string Name => _localization.GetLocalizedString("TaskRefreshPeople"); - public string Description => "Updates metadata for actors and directors in your media library."; + public string Description => _localization.GetLocalizedString("TaskRefreshPeopleDescription"); - public string Category => "Library"; + public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); public string Key => "RefreshPeople"; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs index 9d87316e4..6a1afced7 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs @@ -8,6 +8,7 @@ using MediaBrowser.Common.Updates; using MediaBrowser.Model.Net; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; +using MediaBrowser.Model.Globalization; namespace Emby.Server.Implementations.ScheduledTasks { @@ -22,11 +23,13 @@ namespace Emby.Server.Implementations.ScheduledTasks private readonly ILogger _logger; private readonly IInstallationManager _installationManager; + private readonly ILocalizationManager _localization; - public PluginUpdateTask(ILogger<PluginUpdateTask> logger, IInstallationManager installationManager) + public PluginUpdateTask(ILogger<PluginUpdateTask> logger, IInstallationManager installationManager, ILocalizationManager localization) { _logger = logger; _installationManager = installationManager; + _localization = localization; } /// <summary> @@ -52,9 +55,8 @@ namespace Emby.Server.Implementations.ScheduledTasks { progress.Report(0); - var packagesToInstall = await _installationManager.GetAvailablePluginUpdates(cancellationToken) - .ToListAsync(cancellationToken) - .ConfigureAwait(false); + var packageFetchTask = _installationManager.GetAvailablePluginUpdates(cancellationToken); + var packagesToInstall = (await packageFetchTask.ConfigureAwait(false)).ToList(); progress.Report(10); @@ -96,13 +98,13 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// <inheritdoc /> - public string Name => "Update Plugins"; + public string Name => _localization.GetLocalizedString("TaskUpdatePlugins"); /// <inheritdoc /> - public string Description => "Downloads and installs updates for plugins that are configured to update automatically."; + public string Description => _localization.GetLocalizedString("TaskUpdatePluginsDescription"); /// <inheritdoc /> - public string Category => "Application"; + public string Category => _localization.GetLocalizedString("TasksApplicationCategory"); /// <inheritdoc /> public string Key => "PluginUpdates"; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs index 073678019..74cb01444 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs @@ -6,6 +6,7 @@ using Emby.Server.Implementations.Library; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Tasks; +using MediaBrowser.Model.Globalization; namespace Emby.Server.Implementations.ScheduledTasks { @@ -19,15 +20,17 @@ namespace Emby.Server.Implementations.ScheduledTasks /// </summary> private readonly ILibraryManager _libraryManager; private readonly IServerConfigurationManager _config; + private readonly ILocalizationManager _localization; /// <summary> /// Initializes a new instance of the <see cref="RefreshMediaLibraryTask" /> class. /// </summary> /// <param name="libraryManager">The library manager.</param> - public RefreshMediaLibraryTask(ILibraryManager libraryManager, IServerConfigurationManager config) + public RefreshMediaLibraryTask(ILibraryManager libraryManager, IServerConfigurationManager config, ILocalizationManager localization) { _libraryManager = libraryManager; _config = config; + _localization = localization; } /// <summary> @@ -38,7 +41,8 @@ namespace Emby.Server.Implementations.ScheduledTasks { yield return new TaskTriggerInfo { - Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(12).Ticks + Type = TaskTriggerInfo.TriggerInterval, + IntervalTicks = TimeSpan.FromHours(12).Ticks }; } @@ -57,11 +61,11 @@ namespace Emby.Server.Implementations.ScheduledTasks return ((LibraryManager)_libraryManager).ValidateMediaLibraryInternal(progress, cancellationToken); } - public string Name => "Scan Media Library"; + public string Name => _localization.GetLocalizedString("TaskRefreshLibrary"); - public string Description => "Scans your media library for new files and refreshes metadata."; + public string Description => _localization.GetLocalizedString("TaskRefreshLibraryDescription"); - public string Category => "Library"; + public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); public string Key => "RefreshLibrary"; diff --git a/Emby.Server.Implementations/Services/ServiceController.cs b/Emby.Server.Implementations/Services/ServiceController.cs index d963f9043..e24a95dbb 100644 --- a/Emby.Server.Implementations/Services/ServiceController.cs +++ b/Emby.Server.Implementations/Services/ServiceController.cs @@ -3,14 +3,27 @@ using System.Collections.Generic; using System.Threading.Tasks; using Emby.Server.Implementations.HttpServer; using MediaBrowser.Model.Services; +using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Services { public delegate object ActionInvokerFn(object intance, object request); + public delegate void VoidActionInvokerFn(object intance, object request); public class ServiceController { + private readonly ILogger _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="ServiceController"/> class. + /// </summary> + /// <param name="logger">The <see cref="ServiceController"/> logger.</param> + public ServiceController(ILogger<ServiceController> logger) + { + _logger = logger; + } + public void Init(HttpListenerHost appHost, IEnumerable<Type> serviceTypes) { foreach (var serviceType in serviceTypes) @@ -21,6 +34,13 @@ namespace Emby.Server.Implementations.Services public void RegisterService(HttpListenerHost appHost, Type serviceType) { + // Make sure the provided type implements IService + if (!typeof(IService).IsAssignableFrom(serviceType)) + { + _logger.LogWarning("Tried to register a service that does not implement IService: {ServiceType}", serviceType); + return; + } + var processedReqs = new HashSet<Type>(); var actions = ServiceExecGeneral.Reset(serviceType); diff --git a/Emby.Server.Implementations/Services/SwaggerService.cs b/Emby.Server.Implementations/Services/SwaggerService.cs index c30f32af9..5177251c3 100644 --- a/Emby.Server.Implementations/Services/SwaggerService.cs +++ b/Emby.Server.Implementations/Services/SwaggerService.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using Emby.Server.Implementations.HttpServer; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Services; -using Emby.Server.Implementations.HttpServer; namespace Emby.Server.Implementations.Services { @@ -241,7 +241,7 @@ namespace Emby.Server.Implementations.Services responses = responses, - security = new [] { apiKeySecurity } + security = new[] { apiKeySecurity } }; } diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index dfcd3843c..de768333d 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1401,6 +1401,16 @@ namespace Emby.Server.Implementations.Session user = _userManager.GetUserByName(request.Username); } + if (enforcePassword) + { + user = await _userManager.AuthenticateUser( + request.Username, + request.Password, + request.PasswordSha1, + request.RemoteEndPoint, + true).ConfigureAwait(false); + } + if (user == null) { AuthenticationFailed?.Invoke(this, new GenericEventArgs<AuthenticationRequest>(request)); @@ -1413,16 +1423,6 @@ namespace Emby.Server.Implementations.Session throw new SecurityException("User is not allowed access from this device."); } - if (enforcePassword) - { - user = await _userManager.AuthenticateUser( - request.Username, - request.Password, - request.PasswordSha1, - request.RemoteEndPoint, - true).ConfigureAwait(false); - } - var token = GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName); var session = LogSessionActivity( diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index c897036eb..25f70471a 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -3,8 +3,10 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Net.Http; using System.Runtime.CompilerServices; +using System.Runtime.Serialization; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; @@ -18,6 +20,7 @@ using MediaBrowser.Model.Events; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Updates; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Updates @@ -28,6 +31,11 @@ namespace Emby.Server.Implementations.Updates public class InstallationManager : IInstallationManager { /// <summary> + /// The key for a setting that specifies a URL for the plugin repository JSON manifest. + /// </summary> + public const string PluginManifestUrlKey = "InstallationManager:PluginManifestUrl"; + + /// <summary> /// The _logger. /// </summary> private readonly ILogger _logger; @@ -44,6 +52,7 @@ namespace Emby.Server.Implementations.Updates private readonly IApplicationHost _applicationHost; private readonly IZipClient _zipClient; + private readonly IConfiguration _appConfig; private readonly object _currentInstallationsLock = new object(); @@ -65,7 +74,8 @@ namespace Emby.Server.Implementations.Updates IJsonSerializer jsonSerializer, IServerConfigurationManager config, IFileSystem fileSystem, - IZipClient zipClient) + IZipClient zipClient, + IConfiguration appConfig) { if (logger == null) { @@ -83,6 +93,7 @@ namespace Emby.Server.Implementations.Updates _config = config; _fileSystem = fileSystem; _zipClient = zipClient; + _appConfig = appConfig; } /// <inheritdoc /> @@ -112,19 +123,43 @@ namespace Emby.Server.Implementations.Updates /// <inheritdoc /> public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default) { - using (var response = await _httpClient.SendAsync( - new HttpRequestOptions + var manifestUrl = _appConfig.GetValue<string>(PluginManifestUrlKey); + + try + { + using (var response = await _httpClient.SendAsync( + new HttpRequestOptions + { + Url = manifestUrl, + CancellationToken = cancellationToken, + CacheMode = CacheMode.Unconditional, + CacheLength = TimeSpan.FromMinutes(3) + }, + HttpMethod.Get).ConfigureAwait(false)) + using (Stream stream = response.Content) { - Url = "https://repo.jellyfin.org/releases/plugin/manifest.json", - CancellationToken = cancellationToken, - CacheMode = CacheMode.Unconditional, - CacheLength = TimeSpan.FromMinutes(3) - }, - HttpMethod.Get).ConfigureAwait(false)) - using (Stream stream = response.Content) + try + { + return await _jsonSerializer.DeserializeFromStreamAsync<IReadOnlyList<PackageInfo>>(stream).ConfigureAwait(false); + } + catch (SerializationException ex) + { + const string LogTemplate = + "Failed to deserialize the plugin manifest retrieved from {PluginManifestUrl}. If you " + + "have specified a custom plugin repository manifest URL with --plugin-manifest-url or " + + PluginManifestUrlKey + ", please ensure that it is correct."; + _logger.LogError(ex, LogTemplate, manifestUrl); + throw; + } + } + } + catch (UriFormatException ex) { - return await _jsonSerializer.DeserializeFromStreamAsync<IReadOnlyList<PackageInfo>>( - stream).ConfigureAwait(false); + const string LogTemplate = + "The URL configured for the plugin repository manifest URL is not valid: {PluginManifestUrl}. " + + "Please check the URL configured by --plugin-manifest-url or " + PluginManifestUrlKey; + _logger.LogError(ex, LogTemplate, manifestUrl); + throw; } } @@ -189,16 +224,17 @@ namespace Emby.Server.Implementations.Updates } /// <inheritdoc /> - public async IAsyncEnumerable<PackageVersionInfo> GetAvailablePluginUpdates([EnumeratorCancellation] CancellationToken cancellationToken = default) + public async Task<IEnumerable<PackageVersionInfo>> GetAvailablePluginUpdates(CancellationToken cancellationToken = default) { var catalog = await GetAvailablePackages(cancellationToken).ConfigureAwait(false); + return GetAvailablePluginUpdates(catalog); + } - var systemUpdateLevel = _applicationHost.SystemUpdateLevel; - - // Figure out what needs to be installed + private IEnumerable<PackageVersionInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog) + { foreach (var plugin in _applicationHost.Plugins) { - var compatibleversions = GetCompatibleVersions(catalog, plugin.Name, plugin.Id, plugin.Version, systemUpdateLevel); + var compatibleversions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, plugin.Version, _applicationHost.SystemUpdateLevel); var version = compatibleversions.FirstOrDefault(y => y.Version > plugin.Version); if (version != null && !CompletedInstallations.Any(x => string.Equals(x.AssemblyGuid, version.guid, StringComparison.OrdinalIgnoreCase))) diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 4241d9b95..8f23ef9d0 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -8,7 +8,7 @@ <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" /> - <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.1" /> + <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.3" /> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0" /> </ItemGroup> diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index f9ce0bbe1..d0a99e1e2 100644 --- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -14,7 +14,7 @@ <ItemGroup> <PackageReference Include="SkiaSharp" Version="1.68.1" /> <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="1.68.1" /> - <PackageReference Include="Jellyfin.SkiaSharp.NativeAssets.LinuxArm" Version="1.68.0" /> + <PackageReference Include="Jellyfin.SkiaSharp.NativeAssets.LinuxArm" Version="1.68.1" /> </ItemGroup> <ItemGroup> diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 2ea690650..a67118f18 100644 --- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -308,8 +308,7 @@ namespace Jellyfin.Drawing.Skia if (requiresTransparencyHack || forceCleanBitmap) { - using (var stream = new SKFileStream(NormalizePath(path))) - using (var codec = SKCodec.Create(stream)) + using (var codec = SKCodec.Create(NormalizePath(path))) { if (codec == null) { diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index ed5968ad6..1d5313c13 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -4,7 +4,6 @@ using Emby.Server.Implementations; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Drawing; using MediaBrowser.Model.IO; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Jellyfin.Server diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index dd4f9cd23..71ef9a69a 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -71,6 +71,11 @@ namespace Jellyfin.Server.Extensions // Clear app parts to avoid other assemblies being picked up .ConfigureApplicationPartManager(a => a.ApplicationParts.Clear()) .AddApplicationPart(typeof(StartupController).Assembly) + .AddJsonOptions(options => + { + // Setting the naming policy to null leaves the property names as-is when serializing objects to JSON. + options.JsonSerializerOptions.PropertyNamingPolicy = null; + }) .AddControllersAsServices(); } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index bc18f11fd..02ae202b4 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -36,8 +36,8 @@ <ItemGroup> <PackageReference Include="CommandLineParser" Version="2.7.82" /> - <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.1" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.1" /> + <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.3" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.3" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" /> <PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" /> <PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" /> diff --git a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs index 673f0e415..6f8e4a8ff 100644 --- a/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs +++ b/Jellyfin.Server/Migrations/Routines/DisableTranscodingThrottling.cs @@ -1,8 +1,6 @@ using System; -using System.IO; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Configuration; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Migrations.Routines diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index e9e852349..e55b0d4ed 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics; -using System.Globalization; using System.IO; using System.Linq; using System.Net; @@ -13,20 +12,23 @@ using System.Threading.Tasks; using CommandLine; using Emby.Drawing; using Emby.Server.Implementations; +using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Networking; using Jellyfin.Drawing.Skia; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; -using MediaBrowser.Model.Globalization; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.WebDashboard.Api; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Serilog; -using Serilog.Events; using Serilog.Extensions.Logging; using SQLitePCL; using ILogger = Microsoft.Extensions.Logging.ILogger; @@ -112,9 +114,10 @@ namespace Jellyfin.Server // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); - // Create an instance of the application configuration to use for application startup await InitLoggingConfigFile(appPaths).ConfigureAwait(false); - IConfiguration startupConfig = CreateAppConfiguration(appPaths); + + // Create an instance of the application configuration to use for application startup + IConfiguration startupConfig = CreateAppConfiguration(options, appPaths); // Initialize logging framework InitializeLoggingFramework(startupConfig, appPaths); @@ -183,15 +186,31 @@ namespace Jellyfin.Server new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths), GetImageEncoder(appPaths), new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>())); + try { + // If hosting the web client, validate the client content path + if (startupConfig.HostWebClient()) + { + string webContentPath = DashboardService.GetDashboardUIPath(startupConfig, appHost.ServerConfigurationManager); + if (!Directory.Exists(webContentPath) || Directory.GetFiles(webContentPath).Length == 0) + { + throw new InvalidOperationException( + "The server is expected to host the web client, but the provided content directory is either " + + $"invalid or empty: {webContentPath}. If you do not want to host the web client with the " + + "server, you may set the '--nowebclient' command line flag, or set" + + $"'{MediaBrowser.Controller.Extensions.ConfigurationExtensions.HostWebClientKey}=false' in your config settings."); + } + } + ServiceCollection serviceCollection = new ServiceCollection(); await appHost.InitAsync(serviceCollection, startupConfig).ConfigureAwait(false); - var webHost = CreateWebHostBuilder(appHost, serviceCollection, appPaths).Build(); + var webHost = CreateWebHostBuilder(appHost, serviceCollection, options, startupConfig, appPaths).Build(); - // A bit hacky to re-use service provider since ASP.NET doesn't allow a custom service collection. + // Re-use the web host service provider in the app host since ASP.NET doesn't allow a custom service collection. appHost.ServiceProvider = webHost.Services; + appHost.InitializeServices(); appHost.FindParts(); Migrations.MigrationRunner.Run(appHost, _loggerFactory); @@ -233,10 +252,15 @@ namespace Jellyfin.Server } } - private static IWebHostBuilder CreateWebHostBuilder(ApplicationHost appHost, IServiceCollection serviceCollection, IApplicationPaths appPaths) + private static IWebHostBuilder CreateWebHostBuilder( + ApplicationHost appHost, + IServiceCollection serviceCollection, + StartupOptions commandLineOpts, + IConfiguration startupConfig, + IApplicationPaths appPaths) { return new WebHostBuilder() - .UseKestrel(options => + .UseKestrel((builderContext, options) => { var addresses = appHost.ServerConfigurationManager .Configuration @@ -253,10 +277,19 @@ namespace Jellyfin.Server if (appHost.EnableHttps && appHost.Certificate != null) { - options.Listen( - address, - appHost.HttpsPort, - listenOptions => listenOptions.UseHttps(appHost.Certificate)); + options.Listen(address, appHost.HttpsPort, listenOptions => + { + listenOptions.UseHttps(appHost.Certificate); + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + }); + } + else if (builderContext.HostingEnvironment.IsDevelopment()) + { + options.Listen(address, appHost.HttpsPort, listenOptions => + { + listenOptions.UseHttps(); + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + }); } } } @@ -267,15 +300,24 @@ namespace Jellyfin.Server if (appHost.EnableHttps && appHost.Certificate != null) { - options.ListenAnyIP( - appHost.HttpsPort, - listenOptions => listenOptions.UseHttps(appHost.Certificate)); + options.ListenAnyIP(appHost.HttpsPort, listenOptions => + { + listenOptions.UseHttps(appHost.Certificate); + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + }); + } + else if (builderContext.HostingEnvironment.IsDevelopment()) + { + options.ListenAnyIP(appHost.HttpsPort, listenOptions => + { + listenOptions.UseHttps(); + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + }); } } }) - .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(appPaths)) + .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(commandLineOpts, appPaths, startupConfig)) .UseSerilog() - .UseContentRoot(appHost.ContentRoot) .ConfigureServices(services => { // Merge the external ServiceCollection into ASP.NET DI @@ -398,9 +440,8 @@ namespace Jellyfin.Server // webDir // IF --webdir // ELSE IF $JELLYFIN_WEB_DIR - // ELSE use <bindir>/jellyfin-web + // ELSE <bindir>/jellyfin-web var webDir = options.WebDir; - if (string.IsNullOrEmpty(webDir)) { webDir = Environment.GetEnvironmentVariable("JELLYFIN_WEB_DIR"); @@ -471,21 +512,33 @@ namespace Jellyfin.Server await resource.CopyToAsync(dst).ConfigureAwait(false); } - private static IConfiguration CreateAppConfiguration(IApplicationPaths appPaths) + private static IConfiguration CreateAppConfiguration(StartupOptions commandLineOpts, IApplicationPaths appPaths) { return new ConfigurationBuilder() - .ConfigureAppConfiguration(appPaths) + .ConfigureAppConfiguration(commandLineOpts, appPaths) .Build(); } - private static IConfigurationBuilder ConfigureAppConfiguration(this IConfigurationBuilder config, IApplicationPaths appPaths) + private static IConfigurationBuilder ConfigureAppConfiguration( + this IConfigurationBuilder config, + StartupOptions commandLineOpts, + IApplicationPaths appPaths, + IConfiguration? startupConfig = null) { + // Use the swagger API page as the default redirect path if not hosting the web client + var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration; + if (startupConfig != null && !startupConfig.HostWebClient()) + { + inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "swagger/index.html"; + } + return config .SetBasePath(appPaths.ConfigurationDirectoryPath) - .AddInMemoryCollection(ConfigurationOptions.Configuration) + .AddInMemoryCollection(inMemoryDefaultConfig) .AddJsonFile(LoggingConfigFileDefault, optional: false, reloadOnChange: true) .AddJsonFile(LoggingConfigFileSystem, optional: true, reloadOnChange: true) - .AddEnvironmentVariables("JELLYFIN_"); + .AddEnvironmentVariables("JELLYFIN_") + .AddInMemoryCollection(commandLineOpts.ConvertToConfig()); } /// <summary> @@ -531,7 +584,7 @@ namespace Jellyfin.Server } catch (Exception ex) { - _logger.LogWarning(ex, "Skia not available. Will fallback to NullIMageEncoder."); + _logger.LogWarning(ex, $"Skia not available. Will fallback to {nameof(NullImageEncoder)}."); } return new NullImageEncoder(); diff --git a/Jellyfin.Server/Properties/launchSettings.json b/Jellyfin.Server/Properties/launchSettings.json new file mode 100644 index 000000000..b6e2bcf97 --- /dev/null +++ b/Jellyfin.Server/Properties/launchSettings.json @@ -0,0 +1,17 @@ +{ + "profiles": { + "Jellyfin.Server": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Jellyfin.Server (nowebclient)": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "commandLineArgs": "--nowebclient" + } + } +} diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index 1fb1c5af8..6e15d058f 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -1,5 +1,8 @@ +using System.Collections.Generic; using CommandLine; using Emby.Server.Implementations; +using Emby.Server.Implementations.Updates; +using MediaBrowser.Controller.Extensions; namespace Jellyfin.Server { @@ -16,6 +19,12 @@ namespace Jellyfin.Server public string? DataDir { get; set; } /// <summary> + /// Gets or sets a value indicating whether the server should not host the web client. + /// </summary> + [Option("nowebclient", Required = false, HelpText = "Indicates that the web server should not host the web client.")] + public bool NoWebClient { get; set; } + + /// <summary> /// Gets or sets the path to the web directory. /// </summary> /// <value>The path to the web directory.</value> @@ -66,5 +75,30 @@ namespace Jellyfin.Server /// <inheritdoc /> [Option("restartargs", Required = false, HelpText = "Arguments for restart script.")] public string? RestartArgs { get; set; } + + /// <inheritdoc /> + [Option("plugin-manifest-url", Required = false, HelpText = "A custom URL for the plugin repository JSON manifest")] + public string? PluginManifestUrl { get; set; } + + /// <summary> + /// Gets the command line options as a dictionary that can be used in the .NET configuration system. + /// </summary> + /// <returns>The configuration dictionary.</returns> + public Dictionary<string, string> ConvertToConfig() + { + var config = new Dictionary<string, string>(); + + if (PluginManifestUrl != null) + { + config.Add(InstallationManager.PluginManifestUrlKey, PluginManifestUrl); + } + + if (NoWebClient) + { + config.Add(ConfigurationExtensions.HostWebClientKey, bool.FalseString); + } + + return config; + } } } diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index 4bd13df00..6691080bc 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -86,12 +86,9 @@ namespace MediaBrowser.Api return Array.Empty<string>(); } - if (removeEmpty) - { - return value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries); - } - - return value.Split(separator); + return removeEmpty + ? value.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries) + : value.Split(separator); } public SemaphoreSlim GetTranscodingLock(string outputPath) @@ -258,7 +255,7 @@ namespace MediaBrowser.Api public void ReportTranscodingProgress(TranscodingJob job, StreamState state, TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate) { - var ticks = transcodingPosition.HasValue ? transcodingPosition.Value.Ticks : (long?)null; + var ticks = transcodingPosition?.Ticks; if (job != null) { @@ -487,16 +484,9 @@ namespace MediaBrowser.Api /// <returns>Task.</returns> internal Task KillTranscodingJobs(string deviceId, string playSessionId, Func<string, bool> deleteFiles) { - return KillTranscodingJobs(j => - { - if (!string.IsNullOrWhiteSpace(playSessionId)) - { - return string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase); - } - - return string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase); - - }, deleteFiles); + return KillTranscodingJobs(j => string.IsNullOrWhiteSpace(playSessionId) + ? string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase) + : string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase), deleteFiles); } /// <summary> @@ -561,10 +551,7 @@ namespace MediaBrowser.Api lock (job.ProcessLock) { - if (job.TranscodingThrottler != null) - { - job.TranscodingThrottler.Stop().GetAwaiter().GetResult(); - } + job.TranscodingThrottler?.Stop().GetAwaiter().GetResult(); var process = job.Process; diff --git a/MediaBrowser.Api/BaseApiService.cs b/MediaBrowser.Api/BaseApiService.cs index 2b994d279..1a1d86362 100644 --- a/MediaBrowser.Api/BaseApiService.cs +++ b/MediaBrowser.Api/BaseApiService.cs @@ -9,8 +9,8 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Services; using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; namespace MediaBrowser.Api @@ -58,12 +58,9 @@ namespace MediaBrowser.Api public static string[] SplitValue(string value, char delim) { - if (value == null) - { - return Array.Empty<string>(); - } - - return value.Split(new[] { delim }, StringSplitOptions.RemoveEmptyEntries); + return value == null + ? Array.Empty<string>() + : value.Split(new[] { delim }, StringSplitOptions.RemoveEmptyEntries); } public static Guid[] GetGuids(string value) @@ -97,19 +94,10 @@ namespace MediaBrowser.Api var authenticatedUser = auth.User; // If they're going to update the record of another user, they must be an administrator - if (!userId.Equals(auth.UserId)) + if ((!userId.Equals(auth.UserId) && !authenticatedUser.Policy.IsAdministrator) + || (restrictUserPreferences && !authenticatedUser.Policy.EnableUserPreferenceAccess)) { - if (!authenticatedUser.Policy.IsAdministrator) - { - throw new SecurityException("Unauthorized access."); - } - } - else if (restrictUserPreferences) - { - if (!authenticatedUser.Policy.EnableUserPreferenceAccess) - { - throw new SecurityException("Unauthorized access."); - } + throw new SecurityException("Unauthorized access."); } } @@ -138,8 +126,8 @@ namespace MediaBrowser.Api options.Fields = hasFields.GetItemFields(); } - if (!options.ContainsField(Model.Querying.ItemFields.RecursiveItemCount) - || !options.ContainsField(Model.Querying.ItemFields.ChildCount)) + if (!options.ContainsField(ItemFields.RecursiveItemCount) + || !options.ContainsField(ItemFields.ChildCount)) { var client = authContext.GetAuthorizationInfo(Request).Client ?? string.Empty; if (client.IndexOf("kodi", StringComparison.OrdinalIgnoreCase) != -1 || @@ -150,7 +138,7 @@ namespace MediaBrowser.Api int oldLen = options.Fields.Length; var arr = new ItemFields[oldLen + 1]; options.Fields.CopyTo(arr, 0); - arr[oldLen] = Model.Querying.ItemFields.RecursiveItemCount; + arr[oldLen] = ItemFields.RecursiveItemCount; options.Fields = arr; } @@ -166,7 +154,7 @@ namespace MediaBrowser.Api int oldLen = options.Fields.Length; var arr = new ItemFields[oldLen + 1]; options.Fields.CopyTo(arr, 0); - arr[oldLen] = Model.Querying.ItemFields.ChildCount; + arr[oldLen] = ItemFields.ChildCount; options.Fields = arr; } } @@ -282,27 +270,21 @@ namespace MediaBrowser.Api }).OfType<T>().FirstOrDefault(); - if (result == null) + result ??= libraryManager.GetItemList(new InternalItemsQuery { - result = libraryManager.GetItemList(new InternalItemsQuery - { - Name = name.Replace(BaseItem.SlugChar, '/'), - IncludeItemTypes = new[] { typeof(T).Name }, - DtoOptions = dtoOptions + Name = name.Replace(BaseItem.SlugChar, '/'), + IncludeItemTypes = new[] { typeof(T).Name }, + DtoOptions = dtoOptions - }).OfType<T>().FirstOrDefault(); - } + }).OfType<T>().FirstOrDefault(); - if (result == null) + result ??= libraryManager.GetItemList(new InternalItemsQuery { - result = libraryManager.GetItemList(new InternalItemsQuery - { - Name = name.Replace(BaseItem.SlugChar, '?'), - IncludeItemTypes = new[] { typeof(T).Name }, - DtoOptions = dtoOptions + Name = name.Replace(BaseItem.SlugChar, '?'), + IncludeItemTypes = new[] { typeof(T).Name }, + DtoOptions = dtoOptions - }).OfType<T>().FirstOrDefault(); - } + }).OfType<T>().FirstOrDefault(); return result; } diff --git a/MediaBrowser.Api/ChannelService.cs b/MediaBrowser.Api/ChannelService.cs index 92c32f2ad..fd9b8c396 100644 --- a/MediaBrowser.Api/ChannelService.cs +++ b/MediaBrowser.Api/ChannelService.cs @@ -4,8 +4,8 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Api.UserLibrary; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; @@ -116,12 +116,9 @@ namespace MediaBrowser.Api { var val = Filters; - if (string.IsNullOrEmpty(val)) - { - return new ItemFilter[] { }; - } - - return val.Split(',').Select(v => (ItemFilter)Enum.Parse(typeof(ItemFilter), v, true)); + return string.IsNullOrEmpty(val) + ? Array.Empty<ItemFilter>() + : val.Split(',').Select(v => Enum.Parse<ItemFilter>(v, true)); } /// <summary> @@ -173,14 +170,9 @@ namespace MediaBrowser.Api /// <returns>IEnumerable{ItemFilter}.</returns> public IEnumerable<ItemFilter> GetFilters() { - var val = Filters; - - if (string.IsNullOrEmpty(val)) - { - return new ItemFilter[] { }; - } - - return val.Split(',').Select(v => (ItemFilter)Enum.Parse(typeof(ItemFilter), v, true)); + return string.IsNullOrEmpty(Filters) + ? Array.Empty<ItemFilter>() + : Filters.Split(',').Select(v => Enum.Parse<ItemFilter>(v, true)); } } @@ -241,7 +233,7 @@ namespace MediaBrowser.Api { Limit = request.Limit, StartIndex = request.StartIndex, - ChannelIds = new Guid[] { new Guid(request.Id) }, + ChannelIds = new[] { new Guid(request.Id) }, ParentId = string.IsNullOrWhiteSpace(request.FolderId) ? Guid.Empty : new Guid(request.FolderId), OrderBy = request.GetOrderBy(), DtoOptions = new Controller.Dto.DtoOptions diff --git a/MediaBrowser.Api/Devices/DeviceService.cs b/MediaBrowser.Api/Devices/DeviceService.cs index 8b63decd2..7004a2559 100644 --- a/MediaBrowser.Api/Devices/DeviceService.cs +++ b/MediaBrowser.Api/Devices/DeviceService.cs @@ -155,16 +155,14 @@ namespace MediaBrowser.Api.Devices Id = id }); } - else + + return _deviceManager.AcceptCameraUpload(deviceId, request.RequestStream, new LocalFileInfo { - return _deviceManager.AcceptCameraUpload(deviceId, request.RequestStream, new LocalFileInfo - { - MimeType = Request.ContentType, - Album = album, - Name = name, - Id = id - }); - } + MimeType = Request.ContentType, + Album = album, + Name = name, + Id = id + }); } } } diff --git a/MediaBrowser.Api/EnvironmentService.cs b/MediaBrowser.Api/EnvironmentService.cs index 322b9805b..d199ce154 100644 --- a/MediaBrowser.Api/EnvironmentService.cs +++ b/MediaBrowser.Api/EnvironmentService.cs @@ -177,7 +177,7 @@ namespace MediaBrowser.Api } public object Get(GetDefaultDirectoryBrowser request) => - ToOptimizedResult(new DefaultDirectoryBrowserInfo {Path = null}); + ToOptimizedResult(new DefaultDirectoryBrowserInfo { Path = null }); /// <summary> /// Gets the specified request. @@ -258,12 +258,7 @@ namespace MediaBrowser.Api return false; } - if (!request.IncludeDirectories && isDirectory) - { - return false; - } - - return true; + return request.IncludeDirectories || !isDirectory; }); return entries.Select(f => new FileSystemEntryInfo diff --git a/MediaBrowser.Api/FilterService.cs b/MediaBrowser.Api/FilterService.cs index 25f23bcd1..5eb72cdb1 100644 --- a/MediaBrowser.Api/FilterService.cs +++ b/MediaBrowser.Api/FilterService.cs @@ -133,7 +133,7 @@ namespace MediaBrowser.Api // Non recursive not yet supported for library folders if ((request.Recursive ?? true) || parentItem is UserView || parentItem is ICollectionFolder) { - genreQuery.AncestorIds = parentItem == null ? Array.Empty<Guid>() : new Guid[] { parentItem.Id }; + genreQuery.AncestorIds = parentItem == null ? Array.Empty<Guid>() : new[] { parentItem.Id }; } else { @@ -231,7 +231,7 @@ namespace MediaBrowser.Api EnableTotalRecordCount = false, DtoOptions = new Controller.Dto.DtoOptions { - Fields = new ItemFields[] { ItemFields.Genres, ItemFields.Tags }, + Fields = new[] { ItemFields.Genres, ItemFields.Tags }, EnableImages = false, EnableUserData = false } diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index af455987b..e8ea31251 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -650,7 +650,7 @@ namespace MediaBrowser.Api.Images if (!string.IsNullOrWhiteSpace(request.Format) && Enum.TryParse(request.Format, true, out ImageFormat format)) { - return new ImageFormat[] { format }; + return new[] { format }; } return GetClientSupportedFormats(); @@ -743,24 +743,22 @@ namespace MediaBrowser.Api.Images /// <returns>Task.</returns> public async Task PostImage(BaseItem entity, Stream inputStream, ImageType imageType, string mimeType) { - using (var reader = new StreamReader(inputStream)) - { - var text = await reader.ReadToEndAsync().ConfigureAwait(false); + using var reader = new StreamReader(inputStream); + var text = await reader.ReadToEndAsync().ConfigureAwait(false); - var bytes = Convert.FromBase64String(text); + var bytes = Convert.FromBase64String(text); - var memoryStream = new MemoryStream(bytes) - { - Position = 0 - }; + var memoryStream = new MemoryStream(bytes) + { + Position = 0 + }; - // Handle image/png; charset=utf-8 - mimeType = mimeType.Split(';').FirstOrDefault(); + // Handle image/png; charset=utf-8 + mimeType = mimeType.Split(';').FirstOrDefault(); - await _providerManager.SaveImage(entity, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); + await _providerManager.SaveImage(entity, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); - entity.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); - } + entity.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); } } } diff --git a/MediaBrowser.Api/Images/RemoteImageService.cs b/MediaBrowser.Api/Images/RemoteImageService.cs index f03f5efd8..222bb34d3 100644 --- a/MediaBrowser.Api/Images/RemoteImageService.cs +++ b/MediaBrowser.Api/Images/RemoteImageService.cs @@ -261,27 +261,25 @@ namespace MediaBrowser.Api.Images /// <returns>Task.</returns> private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath) { - using (var result = await _httpClient.GetResponse(new HttpRequestOptions + using var result = await _httpClient.GetResponse(new HttpRequestOptions { Url = url, BufferContent = false - }).ConfigureAwait(false)) - { - var ext = result.ContentType.Split('/').Last(); - - var fullCachePath = GetFullCachePath(urlHash + "." + ext); + }).ConfigureAwait(false); + var ext = result.ContentType.Split('/').Last(); - Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); - using (var stream = result.Content) - using (var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true)) - { - await stream.CopyToAsync(filestream).ConfigureAwait(false); - } + var fullCachePath = GetFullCachePath(urlHash + "." + ext); - Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath)); - File.WriteAllText(pointerCachePath, fullCachePath); + Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); + using (var stream = result.Content) + { + using var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true); + await stream.CopyToAsync(filestream).ConfigureAwait(false); } + + Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath)); + File.WriteAllText(pointerCachePath, fullCachePath); } /// <summary> diff --git a/MediaBrowser.Api/ItemLookupService.cs b/MediaBrowser.Api/ItemLookupService.cs index a76369a15..0bbe7e1cf 100644 --- a/MediaBrowser.Api/ItemLookupService.cs +++ b/MediaBrowser.Api/ItemLookupService.cs @@ -305,9 +305,16 @@ namespace MediaBrowser.Api Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath)); using (var stream = result.Content) - using (var filestream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true)) { - await stream.CopyToAsync(filestream).ConfigureAwait(false); + using var fileStream = new FileStream( + fullCachePath, + FileMode.Create, + FileAccess.Write, + FileShare.Read, + IODefaults.FileStreamBufferSize, + true); + + await stream.CopyToAsync(fileStream).ConfigureAwait(false); } Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath)); diff --git a/MediaBrowser.Api/ItemUpdateService.cs b/MediaBrowser.Api/ItemUpdateService.cs index c81e89ca3..2db6d717a 100644 --- a/MediaBrowser.Api/ItemUpdateService.cs +++ b/MediaBrowser.Api/ItemUpdateService.cs @@ -263,8 +263,7 @@ namespace MediaBrowser.Api item.Overview = request.Overview; item.Genres = request.Genres; - var episode = item as Episode; - if (episode != null) + if (item is Episode episode) { episode.AirsAfterSeasonNumber = request.AirsAfterSeasonNumber; episode.AirsBeforeEpisodeNumber = request.AirsBeforeEpisodeNumber; @@ -302,14 +301,12 @@ namespace MediaBrowser.Api item.PreferredMetadataCountryCode = request.PreferredMetadataCountryCode; item.PreferredMetadataLanguage = request.PreferredMetadataLanguage; - var hasDisplayOrder = item as IHasDisplayOrder; - if (hasDisplayOrder != null) + if (item is IHasDisplayOrder hasDisplayOrder) { hasDisplayOrder.DisplayOrder = request.DisplayOrder; } - var hasAspectRatio = item as IHasAspectRatio; - if (hasAspectRatio != null) + if (item is IHasAspectRatio hasAspectRatio) { hasAspectRatio.AspectRatio = request.AspectRatio; } @@ -337,16 +334,14 @@ namespace MediaBrowser.Api item.ProviderIds = request.ProviderIds; - var video = item as Video; - if (video != null) + if (item is Video video) { video.Video3DFormat = request.Video3DFormat; } if (request.AlbumArtists != null) { - var hasAlbumArtists = item as IHasAlbumArtist; - if (hasAlbumArtists != null) + if (item is IHasAlbumArtist hasAlbumArtists) { hasAlbumArtists.AlbumArtists = request .AlbumArtists @@ -357,8 +352,7 @@ namespace MediaBrowser.Api if (request.ArtistItems != null) { - var hasArtists = item as IHasArtist; - if (hasArtists != null) + if (item is IHasArtist hasArtists) { hasArtists.Artists = request .ArtistItems @@ -367,20 +361,17 @@ namespace MediaBrowser.Api } } - var song = item as Audio; - if (song != null) + if (item is Audio song) { song.Album = request.Album; } - var musicVideo = item as MusicVideo; - if (musicVideo != null) + if (item is MusicVideo musicVideo) { musicVideo.Album = request.Album; } - var series = item as Series; - if (series != null) + if (item is Series series) { series.Status = GetSeriesStatus(request); @@ -400,7 +391,6 @@ namespace MediaBrowser.Api } return (SeriesStatus)Enum.Parse(typeof(SeriesStatus), item.Status, true); - } } } diff --git a/MediaBrowser.Api/Library/LibraryService.cs b/MediaBrowser.Api/Library/LibraryService.cs index 15284958d..a54640b2f 100644 --- a/MediaBrowser.Api/Library/LibraryService.cs +++ b/MediaBrowser.Api/Library/LibraryService.cs @@ -348,28 +348,19 @@ namespace MediaBrowser.Api.Library private string[] GetRepresentativeItemTypes(string contentType) { - switch (contentType) + return contentType switch { - case CollectionType.BoxSets: - return new string[] { "BoxSet" }; - case CollectionType.Playlists: - return new string[] { "Playlist" }; - case CollectionType.Movies: - return new string[] { "Movie" }; - case CollectionType.TvShows: - return new string[] { "Series", "Season", "Episode" }; - case CollectionType.Books: - return new string[] { "Book" }; - case CollectionType.Music: - return new string[] { "MusicAlbum", "MusicArtist", "Audio", "MusicVideo" }; - case CollectionType.HomeVideos: - case CollectionType.Photos: - return new string[] { "Video", "Photo" }; - case CollectionType.MusicVideos: - return new string[] { "MusicVideo" }; - default: - return new string[] { "Series", "Season", "Episode", "Movie" }; - } + CollectionType.BoxSets => new[] {"BoxSet"}, + CollectionType.Playlists => new[] {"Playlist"}, + CollectionType.Movies => new[] {"Movie"}, + CollectionType.TvShows => new[] {"Series", "Season", "Episode"}, + CollectionType.Books => new[] {"Book"}, + CollectionType.Music => new[] {"MusicAlbum", "MusicArtist", "Audio", "MusicVideo"}, + CollectionType.HomeVideos => new[] {"Video", "Photo"}, + CollectionType.Photos => new[] {"Video", "Photo"}, + CollectionType.MusicVideos => new[] {"MusicVideo"}, + _ => new[] {"Series", "Season", "Episode", "Movie"} + }; } private bool IsSaverEnabledByDefault(string name, string[] itemTypes, bool isNewLibrary) @@ -397,54 +388,22 @@ namespace MediaBrowser.Api.Library { if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - if (string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - if (string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - if (string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - return true; - } - else if (string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - else if (string.Equals(name, "The Open Movie Database", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - else if (string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - else if (string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase)) - { - return true; + return !(string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase) + || string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase) + || string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase)); } - return false; + return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "MusicBrainz", StringComparison.OrdinalIgnoreCase); } var metadataOptions = ServerConfigurationManager.Configuration.MetadataOptions .Where(i => string.Equals(i.ItemType, type, StringComparison.OrdinalIgnoreCase)) .ToArray(); - if (metadataOptions.Length == 0) - { - return true; - } - - return metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase)); + return metadataOptions.Length == 0 + || metadataOptions.Any(i => !i.DisabledMetadataFetchers.Contains(name, StringComparer.OrdinalIgnoreCase)); } private bool IsImageFetcherEnabledByDefault(string name, string type, bool isNewLibrary) @@ -453,50 +412,17 @@ namespace MediaBrowser.Api.Library { if (string.Equals(name, "TheMovieDb", StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - if (string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - if (string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - if (string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - return true; - } - else if (string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - else if (string.Equals(name, "The Open Movie Database", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - else if (string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - else if (string.Equals(name, "Emby Designs", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - else if (string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - else if (string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase)) - { - return true; + return !string.Equals(type, "Series", StringComparison.OrdinalIgnoreCase) + && !string.Equals(type, "Season", StringComparison.OrdinalIgnoreCase) + && !string.Equals(type, "Episode", StringComparison.OrdinalIgnoreCase) + && !string.Equals(type, "MusicVideo", StringComparison.OrdinalIgnoreCase); } - return false; + return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "Emby Designs", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase); } var metadataOptions = ServerConfigurationManager.Configuration.MetadataOptions @@ -561,8 +487,7 @@ namespace MediaBrowser.Api.Library foreach (var type in types) { - ImageOption[] defaultImageOptions = null; - TypeOptions.DefaultImageOptions.TryGetValue(type, out defaultImageOptions); + TypeOptions.DefaultImageOptions.TryGetValue(type, out var defaultImageOptions); typeOptions.Add(new LibraryTypeOptions { @@ -609,8 +534,6 @@ namespace MediaBrowser.Api.Library public object Get(GetSimilarItems request) { - var user = !request.UserId.Equals(Guid.Empty) ? _userManager.GetUserById(request.UserId) : null; - var item = string.IsNullOrEmpty(request.Id) ? (!request.UserId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.RootFolder) : _libraryManager.GetItemById(request.Id); @@ -668,7 +591,7 @@ namespace MediaBrowser.Api.Library // ExcludeArtistIds if (!string.IsNullOrEmpty(request.ExcludeArtistIds)) { - query.ExcludeArtistIds = BaseApiService.GetGuids(request.ExcludeArtistIds); + query.ExcludeArtistIds = GetGuids(request.ExcludeArtistIds); } List<BaseItem> itemsResult; @@ -689,7 +612,6 @@ namespace MediaBrowser.Api.Library var result = new QueryResult<BaseItemDto> { Items = returnList, - TotalRecordCount = itemsResult.Count }; @@ -919,12 +841,10 @@ namespace MediaBrowser.Api.Library private BaseItem TranslateParentItem(BaseItem item, User user) { - if (item.GetParent() is AggregateFolder) - { - return _libraryManager.GetUserRootFolder().GetChildren(user, true).FirstOrDefault(i => i.PhysicalLocations.Contains(item.Path)); - } - - return item; + return item.GetParent() is AggregateFolder + ? _libraryManager.GetUserRootFolder().GetChildren(user, true) + .FirstOrDefault(i => i.PhysicalLocations.Contains(item.Path)) + : item; } /// <summary> @@ -1086,7 +1006,7 @@ namespace MediaBrowser.Api.Library var item = string.IsNullOrEmpty(request.Id) ? (!request.UserId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() - : (Folder)_libraryManager.RootFolder) + : _libraryManager.RootFolder) : _libraryManager.GetItemById(request.Id); if (item == null) @@ -1094,18 +1014,13 @@ namespace MediaBrowser.Api.Library throw new ResourceNotFoundException("Item not found."); } - BaseItem[] themeItems = Array.Empty<BaseItem>(); + IEnumerable<BaseItem> themeItems; while (true) { - themeItems = item.GetThemeSongs().ToArray(); - - if (themeItems.Length > 0) - { - break; - } + themeItems = item.GetThemeSongs(); - if (!request.InheritFromParent) + if (themeItems.Any() || !request.InheritFromParent) { break; } @@ -1119,11 +1034,9 @@ namespace MediaBrowser.Api.Library } var dtoOptions = GetDtoOptions(_authContext, request); - - var dtos = themeItems - .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)); - - var items = dtos.ToArray(); + var items = themeItems + .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) + .ToArray(); return new ThemeMediaResult { @@ -1140,9 +1053,7 @@ namespace MediaBrowser.Api.Library /// <returns>System.Object.</returns> public object Get(GetThemeVideos request) { - var result = GetThemeVideos(request); - - return ToOptimizedResult(result); + return ToOptimizedResult(GetThemeVideos(request)); } public ThemeMediaResult GetThemeVideos(GetThemeVideos request) @@ -1152,7 +1063,7 @@ namespace MediaBrowser.Api.Library var item = string.IsNullOrEmpty(request.Id) ? (!request.UserId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() - : (Folder)_libraryManager.RootFolder) + : _libraryManager.RootFolder) : _libraryManager.GetItemById(request.Id); if (item == null) @@ -1160,18 +1071,13 @@ namespace MediaBrowser.Api.Library throw new ResourceNotFoundException("Item not found."); } - BaseItem[] themeItems = Array.Empty<BaseItem>(); + IEnumerable<BaseItem> themeItems; while (true) { - themeItems = item.GetThemeVideos().ToArray(); - - if (themeItems.Length > 0) - { - break; - } + themeItems = item.GetThemeVideos(); - if (!request.InheritFromParent) + if (themeItems.Any() || !request.InheritFromParent) { break; } @@ -1186,10 +1092,9 @@ namespace MediaBrowser.Api.Library var dtoOptions = GetDtoOptions(_authContext, request); - var dtos = themeItems - .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)); - - var items = dtos.ToArray(); + var items = themeItems + .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) + .ToArray(); return new ThemeMediaResult { diff --git a/MediaBrowser.Api/Library/LibraryStructureService.cs b/MediaBrowser.Api/Library/LibraryStructureService.cs index c071b42f7..1e300814f 100644 --- a/MediaBrowser.Api/Library/LibraryStructureService.cs +++ b/MediaBrowser.Api/Library/LibraryStructureService.cs @@ -327,15 +327,11 @@ namespace MediaBrowser.Api.Library try { - var mediaPath = request.PathInfo; - - if (mediaPath == null) + var mediaPath = request.PathInfo ?? new MediaPathInfo { - mediaPath = new MediaPathInfo - { - Path = request.Path - }; - } + Path = request.Path + }; + _libraryManager.AddMediaPath(request.Name, mediaPath); } finally diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index 4b4496139..5fe4c0cca 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -885,11 +885,10 @@ 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 Hex.Encode( - sha.ComputeHash(Encoding.UTF8.GetBytes(str))); - } + using SHA1 sha = SHA1.Create(); + + return Hex.Encode( + sha.ComputeHash(Encoding.UTF8.GetBytes(str))); } public void Delete(DeleteListingProvider request) @@ -1050,8 +1049,7 @@ namespace MediaBrowser.Api.LiveTv { query.IsSeries = true; - var series = _libraryManager.GetItemById(request.LibrarySeriesId) as Series; - if (series != null) + if (_libraryManager.GetItemById(request.LibrarySeriesId) is Series series) { query.Name = series.Name; } diff --git a/MediaBrowser.Api/Movies/MoviesService.cs b/MediaBrowser.Api/Movies/MoviesService.cs index 889ebc928..46da8b909 100644 --- a/MediaBrowser.Api/Movies/MoviesService.cs +++ b/MediaBrowser.Api/Movies/MoviesService.cs @@ -394,7 +394,7 @@ namespace MediaBrowser.Api.Movies { var people = _libraryManager.GetPeople(new InternalPeopleQuery { - PersonTypes = new string[] + PersonTypes = new[] { PersonType.Director } diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 5029ce0bb..770539357 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -134,15 +134,12 @@ namespace MediaBrowser.Api.Playback var data = $"{state.MediaPath}-{state.UserAgent}-{state.Request.DeviceId}-{state.Request.PlaySessionId}"; var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture); - var ext = outputFileExtension.ToLowerInvariant(); + var ext = outputFileExtension?.ToLowerInvariant(); var folder = ServerConfigurationManager.GetTranscodePath(); - if (EnableOutputInSubFolder) - { - return Path.Combine(folder, filename, filename + ext); - } - - return Path.Combine(folder, filename + ext); + return EnableOutputInSubFolder + ? Path.Combine(folder, filename, filename + ext) + : Path.Combine(folder, filename + ext); } protected virtual string GetDefaultEncoderPreset() @@ -248,14 +245,8 @@ namespace MediaBrowser.Api.Playback if (state.VideoRequest != null && string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase)) - { - logFilePrefix = "ffmpeg-remux"; - } - else - { - logFilePrefix = "ffmpeg-directstream"; - } + logFilePrefix = string.Equals(state.OutputAudioCodec, "copy", StringComparison.OrdinalIgnoreCase) + ? "ffmpeg-remux" : "ffmpeg-directstream"; } var logFilePath = Path.Combine(ServerConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt"); @@ -389,195 +380,181 @@ namespace MediaBrowser.Api.Playback continue; } - if (i == 0) - { - request.DeviceProfileId = val; - } - else if (i == 1) - { - request.DeviceId = val; - } - else if (i == 2) - { - request.MediaSourceId = val; - } - else if (i == 3) - { - request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); - } - else if (i == 4) - { - if (videoRequest != null) - { - videoRequest.VideoCodec = val; - } - } - else if (i == 5) - { - request.AudioCodec = val; - } - else if (i == 6) - { - if (videoRequest != null) - { - videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture); - } - } - else if (i == 7) - { - if (videoRequest != null) - { - videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture); - } - } - else if (i == 8) - { - if (videoRequest != null) - { - videoRequest.VideoBitRate = int.Parse(val, CultureInfo.InvariantCulture); - } - } - else if (i == 9) - { - request.AudioBitRate = int.Parse(val, CultureInfo.InvariantCulture); - } - else if (i == 10) - { - request.MaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture); - } - else if (i == 11) - { - if (videoRequest != null) - { - videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture); - } - } - else if (i == 12) - { - if (videoRequest != null) - { - videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture); - } - } - else if (i == 13) - { - if (videoRequest != null) - { - videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture); - } - } - else if (i == 14) - { - request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture); - } - else if (i == 15) - { - if (videoRequest != null) - { - videoRequest.Level = val; - } - } - else if (i == 16) - { - if (videoRequest != null) - { - videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture); - } - } - else if (i == 17) - { - if (videoRequest != null) - { - videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture); - } - } - else if (i == 18) - { - if (videoRequest != null) - { - videoRequest.Profile = val; - } - } - else if (i == 19) - { - // cabac no longer used - } - else if (i == 20) - { - request.PlaySessionId = val; - } - else if (i == 21) - { - // api_key - } - else if (i == 22) - { - request.LiveStreamId = val; - } - else if (i == 23) - { - // Duplicating ItemId because of MediaMonkey - } - else if (i == 24) - { - if (videoRequest != null) - { - videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); - } - } - else if (i == 25) - { - if (!string.IsNullOrWhiteSpace(val) && videoRequest != null) - { - if (Enum.TryParse(val, out SubtitleDeliveryMethod method)) + switch (i) + { + case 0: + request.DeviceProfileId = val; + break; + case 1: + request.DeviceId = val; + break; + case 2: + request.MediaSourceId = val; + break; + case 3: + request.Static = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); + break; + case 4: + if (videoRequest != null) { - videoRequest.SubtitleMethod = method; + videoRequest.VideoCodec = val; } - } - } - else if (i == 26) - { - request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture); - } - else if (i == 27) - { - if (videoRequest != null) - { - videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); - } - } - else if (i == 28) - { - request.Tag = val; - } - else if (i == 29) - { - if (videoRequest != null) - { - videoRequest.RequireAvc = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); - } - } - else if (i == 30) - { - request.SubtitleCodec = val; - } - else if (i == 31) - { - if (videoRequest != null) - { - videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); - } - } - else if (i == 32) - { - if (videoRequest != null) - { - videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); - } - } - else if (i == 33) - { - request.TranscodeReasons = val; + + break; + case 5: + request.AudioCodec = val; + break; + case 6: + if (videoRequest != null) + { + videoRequest.AudioStreamIndex = int.Parse(val, CultureInfo.InvariantCulture); + } + + break; + case 7: + if (videoRequest != null) + { + videoRequest.SubtitleStreamIndex = int.Parse(val, CultureInfo.InvariantCulture); + } + + break; + case 8: + if (videoRequest != null) + { + videoRequest.VideoBitRate = int.Parse(val, CultureInfo.InvariantCulture); + } + + break; + case 9: + request.AudioBitRate = int.Parse(val, CultureInfo.InvariantCulture); + break; + case 10: + request.MaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture); + break; + case 11: + if (videoRequest != null) + { + videoRequest.MaxFramerate = float.Parse(val, CultureInfo.InvariantCulture); + } + + break; + case 12: + if (videoRequest != null) + { + videoRequest.MaxWidth = int.Parse(val, CultureInfo.InvariantCulture); + } + + break; + case 13: + if (videoRequest != null) + { + videoRequest.MaxHeight = int.Parse(val, CultureInfo.InvariantCulture); + } + + break; + case 14: + request.StartTimeTicks = long.Parse(val, CultureInfo.InvariantCulture); + break; + case 15: + if (videoRequest != null) + { + videoRequest.Level = val; + } + + break; + case 16: + if (videoRequest != null) + { + videoRequest.MaxRefFrames = int.Parse(val, CultureInfo.InvariantCulture); + } + + break; + case 17: + if (videoRequest != null) + { + videoRequest.MaxVideoBitDepth = int.Parse(val, CultureInfo.InvariantCulture); + } + + break; + case 18: + if (videoRequest != null) + { + videoRequest.Profile = val; + } + + break; + case 19: + // cabac no longer used + break; + case 20: + request.PlaySessionId = val; + break; + case 21: + // api_key + break; + case 22: + request.LiveStreamId = val; + break; + case 23: + // Duplicating ItemId because of MediaMonkey + break; + case 24: + if (videoRequest != null) + { + videoRequest.CopyTimestamps = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); + } + + break; + case 25: + if (!string.IsNullOrWhiteSpace(val) && videoRequest != null) + { + if (Enum.TryParse(val, out SubtitleDeliveryMethod method)) + { + videoRequest.SubtitleMethod = method; + } + } + + break; + case 26: + request.TranscodingMaxAudioChannels = int.Parse(val, CultureInfo.InvariantCulture); + break; + case 27: + if (videoRequest != null) + { + videoRequest.EnableSubtitlesInManifest = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); + } + + break; + case 28: + request.Tag = val; + break; + case 29: + if (videoRequest != null) + { + videoRequest.RequireAvc = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); + } + + break; + case 30: + request.SubtitleCodec = val; + break; + case 31: + if (videoRequest != null) + { + videoRequest.RequireNonAnamorphic = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); + } + + break; + case 32: + if (videoRequest != null) + { + videoRequest.DeInterlace = string.Equals("true", val, StringComparison.OrdinalIgnoreCase); + } + + break; + case 33: + request.TranscodeReasons = val; + break; } } } @@ -630,14 +607,9 @@ namespace MediaBrowser.Api.Playback throw new ArgumentException("Invalid timeseek header"); } int index = value.IndexOf('-'); - if (index == -1) - { - value = value.Substring(Npt.Length); - } - else - { - value = value.Substring(Npt.Length, index - Npt.Length); - } + value = index == -1 + ? value.Substring(Npt.Length) + : value.Substring(Npt.Length, index - Npt.Length); if (value.IndexOf(':') == -1) { @@ -856,21 +828,11 @@ namespace MediaBrowser.Api.Playback { state.DeviceProfile = DlnaManager.GetProfile(state.Request.DeviceProfileId); } - else + else if (!string.IsNullOrWhiteSpace(state.Request.DeviceId)) { - if (!string.IsNullOrWhiteSpace(state.Request.DeviceId)) - { - var caps = DeviceManager.GetCapabilities(state.Request.DeviceId); + var caps = DeviceManager.GetCapabilities(state.Request.DeviceId); - if (caps != null) - { - state.DeviceProfile = caps.DeviceProfile; - } - else - { - state.DeviceProfile = DlnaManager.GetProfile(headers); - } - } + state.DeviceProfile = caps == null ? DlnaManager.GetProfile(headers) : caps.DeviceProfile; } var profile = state.DeviceProfile; diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index 0cbfe4bdf..52962366c 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -140,7 +140,7 @@ namespace MediaBrowser.Api.Playback.Hls if (isLive) { - job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType); + job ??= ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType); if (job != null) { @@ -156,7 +156,7 @@ namespace MediaBrowser.Api.Playback.Hls var playlistText = GetMasterPlaylistFileText(playlist, videoBitrate + audioBitrate, baselineStreamBitrate); - job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType); + job ??= ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlist, TranscodingJobType); if (job != null) { @@ -168,22 +168,19 @@ namespace MediaBrowser.Api.Playback.Hls private string GetLivePlaylistText(string path, int segmentLength) { - using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) - { - using (var reader = new StreamReader(stream)) - { - var text = reader.ReadToEnd(); + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using var reader = new StreamReader(stream); - text = text.Replace("#EXTM3U", "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT"); + var text = reader.ReadToEnd(); - var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture); + text = text.Replace("#EXTM3U", "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT"); - text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength - 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase); - //text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase); + var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture); - return text; - } - } + text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength - 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase); + //text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase); + + return text; } private string GetMasterPlaylistFileText(string firstPlaylist, int bitrate, int baselineStreamBitrate) @@ -212,29 +209,25 @@ namespace MediaBrowser.Api.Playback.Hls try { // Need to use FileShare.ReadWrite because we're reading the file at the same time it's being written - using (var fileStream = GetPlaylistFileStream(playlist)) + using var fileStream = GetPlaylistFileStream(playlist); + using var reader = new StreamReader(fileStream); + var count = 0; + + while (!reader.EndOfStream) { - using (var reader = new StreamReader(fileStream)) - { - var count = 0; + var line = reader.ReadLine(); - while (!reader.EndOfStream) + if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1) + { + count++; + if (count >= segmentCount) { - var line = reader.ReadLine(); - - if (line.IndexOf("#EXTINF:", StringComparison.OrdinalIgnoreCase) != -1) - { - count++; - if (count >= segmentCount) - { - Logger.LogDebug("Finished waiting for {0} segments in {1}", segmentCount, playlist); - return; - } - } + Logger.LogDebug("Finished waiting for {0} segments in {1}", segmentCount, playlist); + return; } - await Task.Delay(100, cancellationToken).ConfigureAwait(false); } } + await Task.Delay(100, cancellationToken).ConfigureAwait(false); } catch (IOException) { @@ -247,17 +240,13 @@ namespace MediaBrowser.Api.Playback.Hls protected Stream GetPlaylistFileStream(string path) { - var tmpPath = path + ".tmp"; - tmpPath = path; - - try - { - return new FileStream(tmpPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.SequentialScan); - } - catch (IOException) - { - return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.SequentialScan); - } + return new FileStream( + path, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite, + IODefaults.FileStreamBufferSize, + FileOptions.SequentialScan); } protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding) diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index f03e481df..20e18cc26 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -284,7 +284,7 @@ namespace MediaBrowser.Api.Playback.Hls //} Logger.LogDebug("returning {0} [general case]", segmentPath); - job = job ?? ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); + job ??= ApiEntryPoint.Instance.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, requestedIndex, job, cancellationToken).ConfigureAwait(false); } @@ -438,8 +438,7 @@ namespace MediaBrowser.Api.Playback.Hls { var segmentId = "0"; - var segmentRequest = request as GetHlsVideoSegment; - if (segmentRequest != null) + if (request is GetHlsVideoSegment segmentRequest) { segmentId = segmentRequest.SegmentId; } @@ -690,8 +689,7 @@ namespace MediaBrowser.Api.Playback.Hls return false; } - var request = state.Request as IMasterHlsRequest; - if (request != null && !request.EnableAdaptiveBitrateStreaming) + if (state.Request is IMasterHlsRequest request && !request.EnableAdaptiveBitrateStreaming) { return false; } @@ -927,61 +925,69 @@ namespace MediaBrowser.Api.Playback.Hls } else { + var gopArg = string.Empty; var keyFrameArg = string.Format( CultureInfo.InvariantCulture, " -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"", GetStartNumber(state) * state.SegmentLength, state.SegmentLength); - if (state.TargetFramerate.HasValue) + + var framerate = state.VideoStream?.RealFrameRate; + + if (framerate.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( + // be created outside of segment, which breaks seeking + // -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe + gopArg = string.Format( CultureInfo.InvariantCulture, - " -g {0} -keyint_min {0}", - (int)(state.SegmentLength * state.TargetFramerate) + " -g {0} -keyint_min {0} -sc_threshold 0", + Math.Ceiling(state.SegmentLength * framerate.Value) ); } - var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; - args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultEncoderPreset()); - // Unable to force key frames to h264_qsv transcode - if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + // Unable to force key frames using these hw encoders, set key frames by GOP + if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase)) { - Logger.LogInformation("Bug Workaround: Disabling force_key_frames for h264_qsv"); + args += " " + gopArg; } else { - args += " " + keyFrameArg; + args += " " + keyFrameArg + gopArg; } //args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0"; + var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + + // This is for graphical subs + if (hasGraphicalSubs) + { + args += EncodingHelper.GetGraphicalSubtitleParam(state, encodingOptions, codec); + } // Add resolution params, if specified - if (!hasGraphicalSubs) + else { - args += EncodingHelper.GetOutputSizeParam(state, encodingOptions, codec, true); + args += EncodingHelper.GetOutputSizeParam(state, encodingOptions, codec); } - // This is for internal graphical subs - if (hasGraphicalSubs) + // -start_at_zero is necessary to use with -ss when seeking, + // otherwise the target position cannot be determined. + if (!(state.SubtitleStream != null && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)) { - args += EncodingHelper.GetGraphicalSubtitleParam(state, encodingOptions, codec); + args += " -start_at_zero"; } //args += " -flags -global_header"; } - if (args.IndexOf("-copyts", StringComparison.OrdinalIgnoreCase) == -1) - { - args += " -copyts"; - } - if (!string.IsNullOrEmpty(state.OutputVideoSync)) { args += " -vsync " + state.OutputVideoSync; @@ -1025,7 +1031,7 @@ namespace MediaBrowser.Api.Playback.Hls } return string.Format( - "{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}\"", + "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -f hls -max_delay 5000000 -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, diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs index 2aa5e2df1..db24eaca6 100644 --- a/MediaBrowser.Api/Playback/MediaInfoService.cs +++ b/MediaBrowser.Api/Playback/MediaInfoService.cs @@ -5,8 +5,8 @@ using System; using System.Buffers; using System.Globalization; -using System.Text.Json; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Net; @@ -234,7 +234,7 @@ namespace MediaBrowser.Api.Playback OpenToken = mediaSource.OpenToken }).ConfigureAwait(false); - info.MediaSources = new MediaSourceInfo[] { openStreamResult.MediaSource }; + info.MediaSources = new[] { openStreamResult.MediaSource }; } } @@ -289,7 +289,7 @@ namespace MediaBrowser.Api.Playback { var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false); - mediaSources = new MediaSourceInfo[] { mediaSource }; + mediaSources = new[] { mediaSource }; } if (mediaSources.Length == 0) @@ -366,7 +366,7 @@ namespace MediaBrowser.Api.Playback var options = new VideoOptions { - MediaSources = new MediaSourceInfo[] { mediaSource }, + MediaSources = new[] { mediaSource }, Context = EncodingContext.Streaming, DeviceId = auth.DeviceId, ItemId = item.Id, @@ -470,7 +470,7 @@ namespace MediaBrowser.Api.Playback else { options.MaxBitrate = GetMaxBitrate(maxBitrate, user); - + if (item is Audio) { if (!user.Policy.EnableAudioPlaybackTranscoding) @@ -486,10 +486,10 @@ namespace MediaBrowser.Api.Playback } } - // The MediaSource supports direct stream, now test to see if the client supports it - var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) - ? streamBuilder.BuildAudioItem(options) - : streamBuilder.BuildVideoItem(options); + // The MediaSource supports direct stream, now test to see if the client supports it + var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase) + ? streamBuilder.BuildAudioItem(options) + : streamBuilder.BuildVideoItem(options); if (streamInfo == null || !streamInfo.IsDirectStream) { @@ -516,20 +516,17 @@ namespace MediaBrowser.Api.Playback { if (streamInfo != null) { - streamInfo.PlaySessionId = playSessionId; + streamInfo.PlaySessionId = playSessionId; streamInfo.StartPositionTicks = startTimeTicks; mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-'); mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false"; - if (!allowAudioStreamCopy) - { - mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; - } + mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false"; mediaSource.TranscodingContainer = streamInfo.Container; mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol; - + // Do this after the above so that StartPositionTicks is set SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token); - } + } } else { @@ -572,8 +569,7 @@ namespace MediaBrowser.Api.Playback { attachment.DeliveryUrl = string.Format( CultureInfo.InvariantCulture, - "{0}/Videos/{1}/{2}/Attachments/{3}", - ServerConfigurationManager.Configuration.BaseUrl, + "/Videos/{0}/{1}/Attachments/{2}", item.Id, mediaSource.Id, attachment.Index); @@ -583,7 +579,7 @@ namespace MediaBrowser.Api.Playback private long? GetMaxBitrate(long? clientMaxBitrate, User user) { var maxBitrate = clientMaxBitrate; - var remoteClientMaxBitrate = user == null ? 0 : user.Policy.RemoteClientBitrateLimit; + var remoteClientMaxBitrate = user?.Policy.RemoteClientBitrateLimit ?? 0; if (remoteClientMaxBitrate <= 0) { @@ -662,17 +658,9 @@ namespace MediaBrowser.Api.Playback }; }).ThenBy(i => { - if (maxBitrate.HasValue) + if (maxBitrate.HasValue && i.Bitrate.HasValue) { - if (i.Bitrate.HasValue) - { - if (i.Bitrate.Value <= maxBitrate.Value) - { - return 0; - } - - return 2; - } + return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2; } return 1; diff --git a/MediaBrowser.Api/Playback/UniversalAudioService.cs b/MediaBrowser.Api/Playback/UniversalAudioService.cs index cbf981dfe..cebd4b49a 100644 --- a/MediaBrowser.Api/Playback/UniversalAudioService.cs +++ b/MediaBrowser.Api/Playback/UniversalAudioService.cs @@ -167,7 +167,7 @@ namespace MediaBrowser.Api.Playback AudioCodec = request.AudioCodec, Protocol = request.TranscodingProtocol, BreakOnNonKeyFrames = request.BreakOnNonKeyFrames, - MaxAudioChannels = request.TranscodingAudioChannels.HasValue ? request.TranscodingAudioChannels.Value.ToString(CultureInfo.InvariantCulture) : null + MaxAudioChannels = request.TranscodingAudioChannels?.ToString(CultureInfo.InvariantCulture) } }; @@ -300,7 +300,7 @@ namespace MediaBrowser.Api.Playback // hls segment container can only be mpegts or fmp4 per ffmpeg documentation // TODO: remove this when we switch back to the segment muxer - var supportedHLSContainers = new string[] { "mpegts", "fmp4" }; + var supportedHLSContainers = new[] { "mpegts", "fmp4" }; var newRequest = new GetMasterHlsAudioPlaylist { diff --git a/MediaBrowser.Api/PluginService.cs b/MediaBrowser.Api/PluginService.cs index 16d3268b9..7f74511ee 100644 --- a/MediaBrowser.Api/PluginService.cs +++ b/MediaBrowser.Api/PluginService.cs @@ -243,9 +243,7 @@ namespace MediaBrowser.Api // https://code.google.com/p/servicestack/source/browse/trunk/Common/ServiceStack.Text/ServiceStack.Text/Controller/PathInfo.cs var id = Guid.Parse(GetPathValue(1)); - var plugin = _appHost.Plugins.First(p => p.Id == id) as IHasPluginConfiguration; - - if (plugin == null) + if (!(_appHost.Plugins.First(p => p.Id == id) is IHasPluginConfiguration plugin)) { throw new FileNotFoundException(); } diff --git a/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs b/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs index 2bd387229..e08a8482e 100644 --- a/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs +++ b/MediaBrowser.Api/ScheduledTasks/ScheduledTaskService.cs @@ -123,9 +123,7 @@ namespace MediaBrowser.Api.ScheduledTasks { var isHidden = false; - var configurableTask = i.ScheduledTask as IConfigurableScheduledTask; - - if (configurableTask != null) + if (i.ScheduledTask is IConfigurableScheduledTask configurableTask) { isHidden = configurableTask.IsHidden; } @@ -142,9 +140,7 @@ namespace MediaBrowser.Api.ScheduledTasks { var isEnabled = true; - var configurableTask = i.ScheduledTask as IConfigurableScheduledTask; - - if (configurableTask != null) + if (i.ScheduledTask is IConfigurableScheduledTask configurableTask) { isEnabled = configurableTask.IsEnabled; } diff --git a/MediaBrowser.Api/SearchService.cs b/MediaBrowser.Api/SearchService.cs index 0a3dc19dc..e9d339c6e 100644 --- a/MediaBrowser.Api/SearchService.cs +++ b/MediaBrowser.Api/SearchService.cs @@ -234,59 +234,48 @@ namespace MediaBrowser.Api SetThumbImageInfo(result, item); SetBackdropImageInfo(result, item); - var program = item as LiveTvProgram; - if (program != null) + switch (item) { - result.StartDate = program.StartDate; - } - - var hasSeries = item as IHasSeries; - if (hasSeries != null) - { - result.Series = hasSeries.SeriesName; - } - - var series = item as Series; - if (series != null) - { - if (series.Status.HasValue) - { - result.Status = series.Status.Value.ToString(); - } - } - - var album = item as MusicAlbum; - - if (album != null) - { - result.Artists = album.Artists; - result.AlbumArtist = album.AlbumArtist; - } - - var song = item as Audio; - - if (song != null) - { - result.AlbumArtist = song.AlbumArtists.FirstOrDefault(); - result.Artists = song.Artists; - - album = song.AlbumEntity; - - if (album != null) - { - result.Album = album.Name; - result.AlbumId = album.Id; - } - else - { - result.Album = song.Album; - } + case IHasSeries hasSeries: + result.Series = hasSeries.SeriesName; + break; + case LiveTvProgram program: + result.StartDate = program.StartDate; + break; + case Series series: + if (series.Status.HasValue) + { + result.Status = series.Status.Value.ToString(); + } + + break; + case MusicAlbum album: + result.Artists = album.Artists; + result.AlbumArtist = album.AlbumArtist; + break; + case Audio song: + result.AlbumArtist = song.AlbumArtists.FirstOrDefault(); + result.Artists = song.Artists; + + MusicAlbum musicAlbum = song.AlbumEntity; + + if (musicAlbum != null) + { + result.Album = musicAlbum.Name; + result.AlbumId = musicAlbum.Id; + } + else + { + result.Album = song.Album; + } + + break; } if (!item.ChannelId.Equals(Guid.Empty)) { var channel = _libraryManager.GetItemById(item.ChannelId); - result.ChannelName = channel == null ? null : channel.Name; + result.ChannelName = channel?.Name; } return result; @@ -296,12 +285,9 @@ namespace MediaBrowser.Api { var itemWithImage = item.HasImage(ImageType.Thumb) ? item : null; - if (itemWithImage == null) + if (itemWithImage == null && item is Episode) { - if (item is Episode) - { - itemWithImage = GetParentWithImage<Series>(item, ImageType.Thumb); - } + itemWithImage = GetParentWithImage<Series>(item, ImageType.Thumb); } if (itemWithImage == null) @@ -323,12 +309,8 @@ namespace MediaBrowser.Api private void SetBackdropImageInfo(SearchHint hint, BaseItem item) { - var itemWithImage = item.HasImage(ImageType.Backdrop) ? item : null; - - if (itemWithImage == null) - { - itemWithImage = GetParentWithImage<BaseItem>(item, ImageType.Backdrop); - } + var itemWithImage = (item.HasImage(ImageType.Backdrop) ? item : null) + ?? GetParentWithImage<BaseItem>(item, ImageType.Backdrop); if (itemWithImage != null) { diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs index c4a7ae78e..f2968c6b5 100644 --- a/MediaBrowser.Api/Subtitles/SubtitleService.cs +++ b/MediaBrowser.Api/Subtitles/SubtitleService.cs @@ -230,17 +230,14 @@ namespace MediaBrowser.Api.Subtitles if (string.Equals(request.Format, "vtt", StringComparison.OrdinalIgnoreCase) && request.AddVttTimeMap) { - using (var stream = await GetSubtitles(request).ConfigureAwait(false)) - { - using (var reader = new StreamReader(stream)) - { - var text = reader.ReadToEnd(); + using var stream = await GetSubtitles(request).ConfigureAwait(false); + using var reader = new StreamReader(stream); - text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000"); + var text = reader.ReadToEnd(); - return ResultFactory.GetResult(Request, text, MimeTypes.GetMimeType("file." + request.Format)); - } - } + text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000"); + + return ResultFactory.GetResult(Request, text, MimeTypes.GetMimeType("file." + request.Format)); } return ResultFactory.GetResult(Request, await GetSubtitles(request).ConfigureAwait(false), MimeTypes.GetMimeType("file." + request.Format)); diff --git a/MediaBrowser.Api/System/SystemService.cs b/MediaBrowser.Api/System/SystemService.cs index 3a3eeb8b8..c57cc93d5 100644 --- a/MediaBrowser.Api/System/SystemService.cs +++ b/MediaBrowser.Api/System/SystemService.cs @@ -168,12 +168,9 @@ namespace MediaBrowser.Api.System .First(i => string.Equals(i.Name, request.Name, StringComparison.OrdinalIgnoreCase)); // For older files, assume fully static - if (file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1)) - { - return ResultFactory.GetStaticFileResult(Request, file.FullName, FileShare.Read); - } + var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite; - return ResultFactory.GetStaticFileResult(Request, file.FullName, FileShare.ReadWrite); + return ResultFactory.GetStaticFileResult(Request, file.FullName, fileShare); } /// <summary> diff --git a/MediaBrowser.Api/TranscodingJob.cs b/MediaBrowser.Api/TranscodingJob.cs index 6d944d19e..8c24e3ce1 100644 --- a/MediaBrowser.Api/TranscodingJob.cs +++ b/MediaBrowser.Api/TranscodingJob.cs @@ -92,10 +92,7 @@ namespace MediaBrowser.Api { lock (_timerLock) { - if (KillTimer != null) - { - KillTimer.Change(Timeout.Infinite, Timeout.Infinite); - } + KillTimer?.Change(Timeout.Infinite, Timeout.Infinite); } } diff --git a/MediaBrowser.Api/TvShowsService.cs b/MediaBrowser.Api/TvShowsService.cs index b843f7096..cd8e8dfbe 100644 --- a/MediaBrowser.Api/TvShowsService.cs +++ b/MediaBrowser.Api/TvShowsService.cs @@ -12,8 +12,8 @@ using MediaBrowser.Controller.Net; using MediaBrowser.Controller.TV; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Services; using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; namespace MediaBrowser.Api @@ -424,9 +424,7 @@ namespace MediaBrowser.Api if (!string.IsNullOrWhiteSpace(request.SeasonId)) { - var season = _libraryManager.GetItemById(new Guid(request.SeasonId)) as Season; - - if (season == null) + if (!(_libraryManager.GetItemById(new Guid(request.SeasonId)) is Season season)) { throw new ResourceNotFoundException("No season exists with Id " + request.SeasonId); } @@ -444,14 +442,7 @@ namespace MediaBrowser.Api var season = series.GetSeasons(user, dtoOptions).FirstOrDefault(i => i.IndexNumber == request.Season.Value); - if (season == null) - { - episodes = new List<BaseItem>(); - } - else - { - episodes = ((Season)season).GetEpisodes(user, dtoOptions); - } + episodes = season == null ? new List<BaseItem>() : ((Season)season).GetEpisodes(user, dtoOptions); } else { diff --git a/MediaBrowser.Api/UserLibrary/ArtistsService.cs b/MediaBrowser.Api/UserLibrary/ArtistsService.cs index adb0a440f..3d08d5437 100644 --- a/MediaBrowser.Api/UserLibrary/ArtistsService.cs +++ b/MediaBrowser.Api/UserLibrary/ArtistsService.cs @@ -126,12 +126,7 @@ namespace MediaBrowser.Api.UserLibrary protected override QueryResult<(BaseItem, ItemCounts)> GetItems(GetItemsByName request, InternalItemsQuery query) { - if (request is GetAlbumArtists) - { - return LibraryManager.GetAlbumArtists(query); - } - - return LibraryManager.GetArtists(query); + return request is GetAlbumArtists ? LibraryManager.GetAlbumArtists(query) : LibraryManager.GetArtists(query); } /// <summary> diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs index 9fa222d32..c4a52d5f5 100644 --- a/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs +++ b/MediaBrowser.Api/UserLibrary/BaseItemsByNameService.cs @@ -82,8 +82,7 @@ namespace MediaBrowser.Api.UserLibrary { var parent = GetParentItem(request); - var collectionFolder = parent as IHasCollectionType; - if (collectionFolder != null) + if (parent is IHasCollectionType collectionFolder) { return collectionFolder.CollectionType; } @@ -274,7 +273,7 @@ namespace MediaBrowser.Api.UserLibrary DtoOptions = dtoOptions }; - Func<BaseItem, bool> filter = i => FilterItem(request, i, excludeItemTypes, includeItemTypes, mediaTypes); + bool Filter(BaseItem i) => FilterItem(request, i, excludeItemTypes, includeItemTypes, mediaTypes); if (parentItem.IsFolder) { @@ -284,18 +283,18 @@ namespace MediaBrowser.Api.UserLibrary { items = request.Recursive ? folder.GetRecursiveChildren(user, query).ToList() : - folder.GetChildren(user, true).Where(filter).ToList(); + folder.GetChildren(user, true).Where(Filter).ToList(); } else { items = request.Recursive ? - folder.GetRecursiveChildren(filter) : - folder.Children.Where(filter).ToList(); + folder.GetRecursiveChildren(Filter) : + folder.Children.Where(Filter).ToList(); } } else { - items = new[] { parentItem }.Where(filter).ToList(); + items = new[] { parentItem }.Where(Filter).ToList(); } var extractedItems = GetAllItems(request, items); @@ -346,30 +345,21 @@ namespace MediaBrowser.Api.UserLibrary private bool FilterItem(GetItemsByName request, BaseItem f, string[] excludeItemTypes, string[] includeItemTypes, string[] mediaTypes) { // Exclude item types - if (excludeItemTypes.Length > 0) + if (excludeItemTypes.Length > 0 && excludeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase)) { - if (excludeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase)) - { - return false; - } + return false; } // Include item types - if (includeItemTypes.Length > 0) + if (includeItemTypes.Length > 0 && !includeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase)) { - if (!includeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase)) - { - return false; - } + return false; } // Include MediaTypes - if (mediaTypes.Length > 0) + if (mediaTypes.Length > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) { - if (!mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase)) - { - return false; - } + return false; } return true; diff --git a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs index a26f59573..7561b5c89 100644 --- a/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs +++ b/MediaBrowser.Api/UserLibrary/BaseItemsRequest.cs @@ -396,12 +396,10 @@ namespace MediaBrowser.Api.UserLibrary public VideoType[] GetVideoTypes() { - if (string.IsNullOrEmpty(VideoTypes)) - { - return Array.Empty<VideoType>(); - } - - return VideoTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(v => (VideoType)Enum.Parse(typeof(VideoType), v, true)).ToArray(); + return string.IsNullOrEmpty(VideoTypes) + ? Array.Empty<VideoType>() + : VideoTypes.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(v => Enum.Parse<VideoType>(v, true)).ToArray(); } /// <summary> @@ -412,12 +410,10 @@ namespace MediaBrowser.Api.UserLibrary { var val = Filters; - if (string.IsNullOrEmpty(val)) - { - return new ItemFilter[] { }; - } - - return val.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(v => (ItemFilter)Enum.Parse(typeof(ItemFilter), v, true)).ToArray(); + return string.IsNullOrEmpty(val) + ? Array.Empty<ItemFilter>() + : val.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(v => Enum.Parse<ItemFilter>(v, true)).ToArray(); } /// <summary> @@ -428,12 +424,9 @@ namespace MediaBrowser.Api.UserLibrary { var val = ImageTypes; - if (string.IsNullOrEmpty(val)) - { - return new ImageType[] { }; - } - - return val.Split(',').Select(v => (ImageType)Enum.Parse(typeof(ImageType), v, true)).ToArray(); + return string.IsNullOrEmpty(val) + ? Array.Empty<ImageType>() + : val.Split(',').Select(v => Enum.Parse<ImageType>(v, true)).ToArray(); } /// <summary> @@ -469,7 +462,9 @@ namespace MediaBrowser.Api.UserLibrary var sortOrderIndex = sortOrders.Length > i ? i : 0; var sortOrderValue = sortOrders.Length > sortOrderIndex ? sortOrders[sortOrderIndex] : null; - var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase) ? MediaBrowser.Model.Entities.SortOrder.Descending : MediaBrowser.Model.Entities.SortOrder.Ascending; + var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase) + ? MediaBrowser.Model.Entities.SortOrder.Descending + : MediaBrowser.Model.Entities.SortOrder.Ascending; result[i] = new ValueTuple<string, SortOrder>(vals[i], sortOrder); } diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs index c7b505171..c4d44042b 100644 --- a/MediaBrowser.Api/UserLibrary/ItemsService.cs +++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs @@ -199,21 +199,22 @@ namespace MediaBrowser.Api.UserLibrary item = _libraryManager.GetUserRootFolder(); } - Folder folder = item as Folder; - if (folder == null) + if (!(item is Folder folder)) { folder = _libraryManager.GetUserRootFolder(); } - var hasCollectionType = folder as IHasCollectionType; - if (hasCollectionType != null + if (folder is IHasCollectionType hasCollectionType && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) { request.Recursive = true; request.IncludeItemTypes = "Playlist"; } - bool isInEnabledFolder = user.Policy.EnabledFolders.Any(i => new Guid(i) == item.Id); + bool isInEnabledFolder = user.Policy.EnabledFolders.Any(i => new Guid(i) == item.Id) + // Assume all folders inside an EnabledChannel are enabled + || user.Policy.EnabledChannels.Any(i => new Guid(i) == item.Id); + var collectionFolders = _libraryManager.GetCollectionFolders(item); foreach (var collectionFolder in collectionFolders) { @@ -225,7 +226,7 @@ namespace MediaBrowser.Api.UserLibrary } } - if (!(item is UserRootFolder) && !user.Policy.EnableAllFolders && !isInEnabledFolder) + if (!(item is UserRootFolder) && !user.Policy.EnableAllFolders && !isInEnabledFolder && !user.Policy.EnableAllChannels) { Logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Name, item.Name); return new QueryResult<BaseItem> @@ -241,11 +242,11 @@ namespace MediaBrowser.Api.UserLibrary return folder.GetItems(GetItemsQuery(request, dtoOptions, user)); } - var itemsArray = folder.GetChildren(user, true).ToArray(); + var itemsArray = folder.GetChildren(user, true); return new QueryResult<BaseItem> { Items = itemsArray, - TotalRecordCount = itemsArray.Length, + TotalRecordCount = itemsArray.Count, StartIndex = 0 }; } diff --git a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs index 2ec08f578..7fa750adb 100644 --- a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs +++ b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs @@ -361,7 +361,8 @@ namespace MediaBrowser.Api.UserLibrary var dtoOptions = GetDtoOptions(_authContext, request); - var dtos = item.GetDisplayExtras() + var dtos = item + .GetExtras(BaseItem.DisplayExtraTypes) .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)); return dtos.ToArray(); diff --git a/MediaBrowser.Api/VideosService.cs b/MediaBrowser.Api/VideosService.cs index 46b6d5a94..b11fd48d3 100644 --- a/MediaBrowser.Api/VideosService.cs +++ b/MediaBrowser.Api/VideosService.cs @@ -139,17 +139,11 @@ namespace MediaBrowser.Api .ToList(); var primaryVersion = videosWithVersions.FirstOrDefault(); - if (primaryVersion == null) { primaryVersion = items.OrderBy(i => { - if (i.Video3DFormat.HasValue) - { - return 1; - } - - if (i.VideoType != Model.Entities.VideoType.VideoFile) + if (i.Video3DFormat.HasValue || i.VideoType != Model.Entities.VideoType.VideoFile) { return 1; } @@ -158,10 +152,7 @@ namespace MediaBrowser.Api }) .ThenByDescending(i => { - var stream = i.GetDefaultVideoStream(); - - return stream == null || stream.Width == null ? 0 : stream.Width.Value; - + return i.GetDefaultVideoStream()?.Width ?? 0; }).First(); } diff --git a/MediaBrowser.Common/Configuration/IApplicationPaths.cs b/MediaBrowser.Common/Configuration/IApplicationPaths.cs index 5bdea7d8b..870b90796 100644 --- a/MediaBrowser.Common/Configuration/IApplicationPaths.cs +++ b/MediaBrowser.Common/Configuration/IApplicationPaths.cs @@ -1,3 +1,5 @@ +using MediaBrowser.Model.Configuration; + namespace MediaBrowser.Common.Configuration { /// <summary> @@ -12,9 +14,12 @@ namespace MediaBrowser.Common.Configuration string ProgramDataPath { get; } /// <summary> - /// Gets the path to the web UI resources folder + /// Gets the path to the web UI resources folder. /// </summary> - /// <value>The web UI resources path.</value> + /// <remarks> + /// This value is not relevant if the server is configured to not host any static web content. Additionally, + /// the value for <see cref="ServerConfiguration.DashboardSourcePath"/> takes precedence over this one. + /// </remarks> string WebPath { get; } /// <summary> diff --git a/MediaBrowser.Common/Cryptography/Extensions.cs b/MediaBrowser.Common/Cryptography/CryptoExtensions.cs index 1e32a6d1a..157b0ed10 100644 --- a/MediaBrowser.Common/Cryptography/Extensions.cs +++ b/MediaBrowser.Common/Cryptography/CryptoExtensions.cs @@ -9,7 +9,7 @@ namespace MediaBrowser.Common.Cryptography /// <summary> /// Class containing extension methods for working with Jellyfin cryptography objects. /// </summary> - public static class Extensions + public static class CryptoExtensions { /// <summary> /// Creates a new <see cref="PasswordHash" /> instance. diff --git a/MediaBrowser.Common/Extensions/ProcessExtensions.cs b/MediaBrowser.Common/Extensions/ProcessExtensions.cs new file mode 100644 index 000000000..c74787122 --- /dev/null +++ b/MediaBrowser.Common/Extensions/ProcessExtensions.cs @@ -0,0 +1,80 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.Extensions +{ + /// <summary> + /// Extension methods for <see cref="Process"/>. + /// </summary> + public static class ProcessExtensions + { + /// <summary> + /// Asynchronously wait for the process to exit. + /// </summary> + /// <param name="process">The process to wait for.</param> + /// <param name="timeout">The duration to wait before cancelling waiting for the task.</param> + /// <returns>True if the task exited normally, false if the timeout elapsed before the process exited.</returns> + /// <exception cref="InvalidOperationException">If <see cref="Process.EnableRaisingEvents"/> is not set to true for the process.</exception> + public static async Task<bool> WaitForExitAsync(this Process process, TimeSpan timeout) + { + using (var cancelTokenSource = new CancellationTokenSource(timeout)) + { + return await WaitForExitAsync(process, cancelTokenSource.Token).ConfigureAwait(false); + } + } + + /// <summary> + /// Asynchronously wait for the process to exit. + /// </summary> + /// <param name="process">The process to wait for.</param> + /// <param name="cancelToken">A <see cref="CancellationToken"/> to observe while waiting for the process to exit.</param> + /// <returns>True if the task exited normally, false if cancelled before the process exited.</returns> + public static async Task<bool> WaitForExitAsync(this Process process, CancellationToken cancelToken) + { + if (!process.EnableRaisingEvents) + { + throw new InvalidOperationException("EnableRisingEvents must be enabled to async wait for a task to exit."); + } + + // Add an event handler for the process exit event + var tcs = new TaskCompletionSource<bool>(); + process.Exited += (sender, args) => tcs.TrySetResult(true); + + // Return immediately if the process has already exited + if (process.HasExitedSafe()) + { + return true; + } + + // Register with the cancellation token then await + using (var cancelRegistration = cancelToken.Register(() => tcs.TrySetResult(process.HasExitedSafe()))) + { + return await tcs.Task.ConfigureAwait(false); + } + } + + /// <summary> + /// Gets a value indicating whether the associated process has been terminated using + /// <see cref="Process.HasExited"/>. This is safe to call even if there is no operating system process + /// associated with the <see cref="Process"/>. + /// </summary> + /// <param name="process">The process to check the exit status for.</param> + /// <returns> + /// True if the operating system process referenced by the <see cref="Process"/> component has + /// terminated, or if there is no associated operating system process; otherwise, false. + /// </returns> + private static bool HasExitedSafe(this Process process) + { + try + { + return process.HasExited; + } + catch (InvalidOperationException) + { + return true; + } + } + } +} diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 04e0ee67a..3b0347802 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -12,8 +12,8 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.1" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.1" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.3" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.3" /> <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" /> </ItemGroup> @@ -30,7 +30,7 @@ <!-- Code analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <!-- <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> --> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <!-- <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> --> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> diff --git a/MediaBrowser.Common/Net/HttpRequestOptions.cs b/MediaBrowser.Common/Net/HttpRequestOptions.cs index 51962001e..38274a80e 100644 --- a/MediaBrowser.Common/Net/HttpRequestOptions.cs +++ b/MediaBrowser.Common/Net/HttpRequestOptions.cs @@ -20,7 +20,7 @@ namespace MediaBrowser.Common.Net RequestHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); CacheMode = CacheMode.None; - DecompressionMethod = CompressionMethod.Deflate; + DecompressionMethod = CompressionMethods.Deflate; } /// <summary> @@ -29,7 +29,7 @@ namespace MediaBrowser.Common.Net /// <value>The URL.</value> public string Url { get; set; } - public CompressionMethod DecompressionMethod { get; set; } + public CompressionMethods DecompressionMethod { get; set; } /// <summary> /// Gets or sets the accept header. @@ -83,8 +83,6 @@ namespace MediaBrowser.Common.Net public string RequestContent { get; set; } - public byte[] RequestContentBytes { get; set; } - public bool BufferContent { get; set; } public bool LogErrorResponseBody { get; set; } @@ -112,7 +110,7 @@ namespace MediaBrowser.Common.Net } [Flags] - public enum CompressionMethod + public enum CompressionMethods { None = 0b00000001, Deflate = 0b00000010, diff --git a/MediaBrowser.Common/Net/HttpResponseInfo.cs b/MediaBrowser.Common/Net/HttpResponseInfo.cs index d7f7a5622..d4fee6c78 100644 --- a/MediaBrowser.Common/Net/HttpResponseInfo.cs +++ b/MediaBrowser.Common/Net/HttpResponseInfo.cs @@ -8,7 +8,7 @@ namespace MediaBrowser.Common.Net /// <summary> /// Class HttpResponseInfo. /// </summary> - public class HttpResponseInfo : IDisposable + public sealed class HttpResponseInfo : IDisposable { #pragma warning disable CS1591 public HttpResponseInfo() @@ -22,7 +22,6 @@ namespace MediaBrowser.Common.Net } #pragma warning restore CS1591 -#pragma warning restore SA1600 /// <summary> /// Gets or sets the type of the content. diff --git a/MediaBrowser.Common/System/OperatingSystem.cs b/MediaBrowser.Common/System/OperatingSystem.cs index 7d38ddb6e..5f673d320 100644 --- a/MediaBrowser.Common/System/OperatingSystem.cs +++ b/MediaBrowser.Common/System/OperatingSystem.cs @@ -35,7 +35,7 @@ namespace MediaBrowser.Common.System case OperatingSystemId.Linux: return "Linux"; case OperatingSystemId.Darwin: return "macOS"; case OperatingSystemId.Windows: return "Windows"; - default: throw new Exception($"Unknown OS {Id}"); + default: throw new PlatformNotSupportedException($"Unknown OS {Id}"); } } } @@ -53,20 +53,20 @@ namespace MediaBrowser.Common.System default: { string osDescription = RuntimeInformation.OSDescription; - if (osDescription.IndexOf("linux", StringComparison.OrdinalIgnoreCase) != -1) + if (osDescription.Contains("linux", StringComparison.OrdinalIgnoreCase)) { return OperatingSystemId.Linux; } - else if (osDescription.IndexOf("darwin", StringComparison.OrdinalIgnoreCase) != -1) + else if (osDescription.Contains("darwin", StringComparison.OrdinalIgnoreCase)) { return OperatingSystemId.Darwin; } - else if (osDescription.IndexOf("bsd", StringComparison.OrdinalIgnoreCase) != -1) + else if (osDescription.Contains("bsd", StringComparison.OrdinalIgnoreCase)) { return OperatingSystemId.BSD; } - throw new Exception($"Can't resolve OS with description: '{osDescription}'"); + throw new PlatformNotSupportedException($"Can't resolve OS with description: '{osDescription}'"); } } } diff --git a/MediaBrowser.Common/Updates/IInstallationManager.cs b/MediaBrowser.Common/Updates/IInstallationManager.cs index 8ea492261..93f330e5b 100644 --- a/MediaBrowser.Common/Updates/IInstallationManager.cs +++ b/MediaBrowser.Common/Updates/IInstallationManager.cs @@ -92,7 +92,7 @@ namespace MediaBrowser.Common.Updates /// </summary> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The available plugin updates.</returns> - IAsyncEnumerable<PackageVersionInfo> GetAvailablePluginUpdates(CancellationToken cancellationToken = default); + Task<IEnumerable<PackageVersionInfo>> GetAvailablePluginUpdates(CancellationToken cancellationToken = default); /// <summary> /// Installs the package. diff --git a/MediaBrowser.Controller/Channels/IChannel.cs b/MediaBrowser.Controller/Channels/IChannel.cs index f8ed98a45..c44e20d1a 100644 --- a/MediaBrowser.Controller/Channels/IChannel.cs +++ b/MediaBrowser.Controller/Channels/IChannel.cs @@ -64,7 +64,7 @@ namespace MediaBrowser.Controller.Channels /// </summary> /// <param name="type">The type.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{DynamicImageInfo}.</returns> + /// <returns>Task{DynamicImageResponse}.</returns> Task<DynamicImageResponse> GetChannelImage(ImageType type, CancellationToken cancellationToken); /// <summary> diff --git a/MediaBrowser.Controller/Chapters/IChapterManager.cs b/MediaBrowser.Controller/Chapters/IChapterManager.cs index d061898a1..f82e5b41a 100644 --- a/MediaBrowser.Controller/Chapters/IChapterManager.cs +++ b/MediaBrowser.Controller/Chapters/IChapterManager.cs @@ -1,16 +1,17 @@ +using System; using System.Collections.Generic; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Chapters { /// <summary> - /// Interface IChapterManager + /// Interface IChapterManager. /// </summary> public interface IChapterManager { /// <summary> /// Saves the chapters. /// </summary> - void SaveChapters(string itemId, List<ChapterInfo> chapters); + void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters); } } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 5e4217cd5..7ed8fa767 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -15,7 +15,6 @@ using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; -using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -178,6 +177,7 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public int? TotalBitrate { get; set; } + [JsonIgnore] public ExtraType? ExtraType { get; set; } @@ -549,7 +549,6 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public DateTime DateModified { get; set; } - [JsonIgnore] public DateTime DateLastSaved { get; set; } [JsonIgnore] @@ -1326,8 +1325,9 @@ namespace MediaBrowser.Controller.Entities } // Use some hackery to get the extra type based on foldername - Enum.TryParse(extraFolderName.Replace(" ", ""), true, out ExtraType extraType); - item.ExtraType = extraType; + item.ExtraType = Enum.TryParse(extraFolderName.Replace(" ", string.Empty), true, out ExtraType extraType) + ? extraType + : Model.Entities.ExtraType.Unknown; return item; @@ -2190,13 +2190,9 @@ namespace MediaBrowser.Controller.Entities /// <summary> /// Do whatever refreshing is necessary when the filesystem pertaining to this item has changed. /// </summary> - /// <returns>Task.</returns> public virtual void ChangedExternally() { - ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(FileSystem)) - { - - }, RefreshPriority.High); + ProviderManager.QueueRefresh(Id, new MetadataRefreshOptions(new DirectoryService(FileSystem)), RefreshPriority.High); } /// <summary> @@ -2227,7 +2223,6 @@ namespace MediaBrowser.Controller.Entities existingImage.Width = image.Width; existingImage.Height = image.Height; } - else { var current = ImageInfos; @@ -2270,7 +2265,6 @@ namespace MediaBrowser.Controller.Entities /// </summary> /// <param name="type">The type.</param> /// <param name="index">The index.</param> - /// <returns>Task.</returns> public void DeleteImage(ImageType type, int index) { var info = GetImageInfo(type, index); @@ -2308,7 +2302,7 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// Validates that images within the item are still on the file system + /// Validates that images within the item are still on the filesystem. /// </summary> public bool ValidateImages(IDirectoryService directoryService) { @@ -2602,7 +2596,7 @@ namespace MediaBrowser.Controller.Entities } /// <summary> - /// This is called before any metadata refresh and returns true or false indicating if changes were made + /// This is called before any metadata refresh and returns true if changes were made. /// </summary> public virtual bool BeforeMetadataRefresh(bool replaceAllMetdata) { @@ -2662,36 +2656,43 @@ namespace MediaBrowser.Controller.Entities newOptions.ForceSave = true; ownedItem.Genres = item.Genres; } + if (!item.Studios.SequenceEqual(ownedItem.Studios, StringComparer.Ordinal)) { newOptions.ForceSave = true; ownedItem.Studios = item.Studios; } + if (!item.ProductionLocations.SequenceEqual(ownedItem.ProductionLocations, StringComparer.Ordinal)) { newOptions.ForceSave = true; ownedItem.ProductionLocations = item.ProductionLocations; } + if (item.CommunityRating != ownedItem.CommunityRating) { ownedItem.CommunityRating = item.CommunityRating; newOptions.ForceSave = true; } + if (item.CriticRating != ownedItem.CriticRating) { ownedItem.CriticRating = item.CriticRating; newOptions.ForceSave = true; } + if (!string.Equals(item.Overview, ownedItem.Overview, StringComparison.Ordinal)) { ownedItem.Overview = item.Overview; newOptions.ForceSave = true; } + if (!string.Equals(item.OfficialRating, ownedItem.OfficialRating, StringComparison.Ordinal)) { ownedItem.OfficialRating = item.OfficialRating; newOptions.ForceSave = true; } + if (!string.Equals(item.CustomRating, ownedItem.CustomRating, StringComparison.Ordinal)) { ownedItem.CustomRating = item.CustomRating; @@ -2739,7 +2740,7 @@ namespace MediaBrowser.Controller.Entities { var list = GetEtagValues(user); - return string.Join("|", list.ToArray()).GetMD5().ToString("N", CultureInfo.InvariantCulture); + return string.Join("|", list).GetMD5().ToString("N", CultureInfo.InvariantCulture); } protected virtual List<string> GetEtagValues(User user) @@ -2782,8 +2783,7 @@ namespace MediaBrowser.Controller.Entities return true; } - var view = this as IHasCollectionType; - if (view != null) + if (this is IHasCollectionType view) { if (string.Equals(view.CollectionType, CollectionType.LiveTv, StringComparison.OrdinalIgnoreCase)) { @@ -2876,14 +2876,29 @@ namespace MediaBrowser.Controller.Entities /// <value>The remote trailers.</value> public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; } + /// <summary> + /// Get all extras associated with this item, sorted by <see cref="SortName"/>. + /// </summary> + /// <returns>An enumerable containing the items.</returns> public IEnumerable<BaseItem> GetExtras() { - return ExtraIds.Select(LibraryManager.GetItemById).Where(i => i != null).OrderBy(i => i.SortName); + return ExtraIds + .Select(LibraryManager.GetItemById) + .Where(i => i != null) + .OrderBy(i => i.SortName); } + /// <summary> + /// Get all extras with specific types that are associated with this item. + /// </summary> + /// <param name="extraTypes">The types of extras to retrieve.</param> + /// <returns>An enumerable containing the extras.</returns> public IEnumerable<BaseItem> GetExtras(IReadOnlyCollection<ExtraType> extraTypes) { - return ExtraIds.Select(LibraryManager.GetItemById).Where(i => i != null && extraTypes.Contains(i.ExtraType.Value)); + return ExtraIds + .Select(LibraryManager.GetItemById) + .Where(i => i != null) + .Where(i => i.ExtraType.HasValue && extraTypes.Contains(i.ExtraType.Value)); } public IEnumerable<BaseItem> GetTrailers() @@ -2894,24 +2909,36 @@ namespace MediaBrowser.Controller.Entities return Array.Empty<BaseItem>(); } - public IEnumerable<BaseItem> GetDisplayExtras() - { - return GetExtras(DisplayExtraTypes); - } - public virtual bool IsHD => Height >= 720; + public bool IsShortcut { get; set; } + public string ShortcutPath { get; set; } + public int Width { get; set; } + public int Height { get; set; } + public Guid[] ExtraIds { get; set; } + public virtual long GetRunTimeTicksForPlayState() { return RunTimeTicks ?? 0; } - // Possible types of extra videos - public static readonly IReadOnlyCollection<ExtraType> DisplayExtraTypes = new[] { Model.Entities.ExtraType.BehindTheScenes, Model.Entities.ExtraType.Clip, Model.Entities.ExtraType.DeletedScene, Model.Entities.ExtraType.Interview, Model.Entities.ExtraType.Sample, Model.Entities.ExtraType.Scene }; + /// <summary> + /// Extra types that should be counted and displayed as "Special Features" in the UI. + /// </summary> + public static readonly IReadOnlyCollection<ExtraType> DisplayExtraTypes = new HashSet<ExtraType> + { + Model.Entities.ExtraType.Unknown, + Model.Entities.ExtraType.BehindTheScenes, + Model.Entities.ExtraType.Clip, + Model.Entities.ExtraType.DeletedScene, + Model.Entities.ExtraType.Interview, + Model.Entities.ExtraType.Sample, + Model.Entities.ExtraType.Scene + }; public virtual bool SupportsExternalTransfer => false; diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs index 44c35374d..dcad2554b 100644 --- a/MediaBrowser.Controller/Entities/Book.cs +++ b/MediaBrowser.Controller/Entities/Book.cs @@ -13,8 +13,10 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public string SeriesPresentationUniqueKey { get; set; } + [JsonIgnore] public string SeriesName { get; set; } + [JsonIgnore] public Guid SeriesId { get; set; } @@ -22,10 +24,12 @@ namespace MediaBrowser.Controller.Entities { return SeriesName; } + public string FindSeriesName() { return SeriesName; } + public string FindSeriesPresentationUniqueKey() { return SeriesPresentationUniqueKey; diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index c72bd487e..a468e0c35 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -28,7 +30,6 @@ namespace MediaBrowser.Controller.Entities /// </summary> public class Folder : BaseItem { - public static IUserManager UserManager { get; set; } public static IUserViewManager UserViewManager { get; set; } /// <summary> @@ -620,7 +621,6 @@ namespace MediaBrowser.Controller.Entities { EnableImages = false } - }).TotalRecordCount; } @@ -864,7 +864,7 @@ namespace MediaBrowser.Controller.Entities return SortItemsByRequest(query, result); } - return result.ToArray(); + return result; } return GetItemsInternal(query).Items; @@ -1713,7 +1713,6 @@ namespace MediaBrowser.Controller.Entities { EnableImages = false } - }); double unplayedCount = unplayedQueryResult.TotalRecordCount; diff --git a/MediaBrowser.Controller/Entities/IItemByName.cs b/MediaBrowser.Controller/Entities/IItemByName.cs index 89b5dfee3..8ef5c8d96 100644 --- a/MediaBrowser.Controller/Entities/IItemByName.cs +++ b/MediaBrowser.Controller/Entities/IItemByName.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace MediaBrowser.Controller.Entities { /// <summary> - /// Marker interface + /// Marker interface. /// </summary> public interface IItemByName { diff --git a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs index 1613531b5..011975dd2 100644 --- a/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs +++ b/MediaBrowser.Controller/Entities/InternalPeopleQuery.cs @@ -4,11 +4,21 @@ namespace MediaBrowser.Controller.Entities { public class InternalPeopleQuery { + /// <summary> + /// Gets or sets the maximum number of items the query should return. + /// </summary> + public int Limit { get; set; } + public Guid ItemId { get; set; } + public string[] PersonTypes { get; set; } + public string[] ExcludePersonTypes { get; set; } + public int? MaxListOrder { get; set; } + public Guid AppearsInItemId { get; set; } + public string NameContains { get; set; } public InternalPeopleQuery() diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs index 64e216e69..9e4f9d47e 100644 --- a/MediaBrowser.Controller/Entities/Person.cs +++ b/MediaBrowser.Controller/Entities/Person.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Text.Json.Serialization; using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities diff --git a/MediaBrowser.Controller/Entities/PersonInfo.cs b/MediaBrowser.Controller/Entities/PersonInfo.cs index e90c55a8a..f3ec73b32 100644 --- a/MediaBrowser.Controller/Entities/PersonInfo.cs +++ b/MediaBrowser.Controller/Entities/PersonInfo.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Collections.Generic; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs index 48316499a..c0043c0ef 100644 --- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs +++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.Extensions.Configuration; namespace MediaBrowser.Controller.Extensions @@ -8,6 +9,11 @@ namespace MediaBrowser.Controller.Extensions public static class ConfigurationExtensions { /// <summary> + /// The key for a setting that indicates whether the application should host web client content. + /// </summary> + public const string HostWebClientKey = "hostwebclient"; + + /// <summary> /// The key for the FFmpeg probe size option. /// </summary> public const string FfmpegProbeSizeKey = "FFmpeg:probesize"; @@ -23,6 +29,15 @@ namespace MediaBrowser.Controller.Extensions public const string PlaylistsAllowDuplicatesKey = "playlists:allowDuplicates"; /// <summary> + /// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>. + /// </summary> + /// <param name="configuration">The configuration to retrieve the value from.</param> + /// <returns>The parsed config value.</returns> + /// <exception cref="FormatException">The config value is not a valid bool string. See <see cref="bool.Parse(string)"/>.</exception> + public static bool HostWebClient(this IConfiguration configuration) + => configuration.GetValue<bool>(HostWebClientKey); + + /// <summary> /// Gets the FFmpeg probe size from the <see cref="IConfiguration" />. /// </summary> /// <param name="configuration">The configuration to read the setting from.</param> diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 25f0905eb..608ffc61c 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -82,6 +82,11 @@ namespace MediaBrowser.Controller /// <returns>The local API URL.</returns> string GetLocalApiUrl(IPAddress address); + /// <summary> + /// Open a URL in an external browser window. + /// </summary> + /// <param name="url">The URL to open.</param> + /// <exception cref="NotSupportedException"><see cref="CanLaunchWebBrowser"/> is false.</exception> void LaunchUrl(string url); void EnableLoopback(string appName); diff --git a/MediaBrowser.Controller/Library/IMediaSourceProvider.cs b/MediaBrowser.Controller/Library/IMediaSourceProvider.cs index ec7798551..5bf4acebb 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceProvider.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceProvider.cs @@ -1,5 +1,4 @@ #pragma warning disable CS1591 -#pragma warning disable SA1600 using System.Collections.Generic; using System.Threading; diff --git a/MediaBrowser.Controller/Library/NameExtensions.cs b/MediaBrowser.Controller/Library/NameExtensions.cs index 6b0b7e53a..24d0347e9 100644 --- a/MediaBrowser.Controller/Library/NameExtensions.cs +++ b/MediaBrowser.Controller/Library/NameExtensions.cs @@ -1,6 +1,6 @@ using System; -using System.Linq; using System.Collections.Generic; +using System.Linq; using MediaBrowser.Controller.Extensions; namespace MediaBrowser.Controller.Library diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index bcca9e4a1..662ab2535 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -8,8 +8,8 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.1" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.1" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.3" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.3" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 342c76414..8fefdd770 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -78,8 +78,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(hwType) && encodingOptions.EnableHardwareEncoding - && codecMap.ContainsKey(hwType) - && CheckVaapi(state, hwType, encodingOptions)) + && codecMap.ContainsKey(hwType)) { var preferredEncoder = codecMap[hwType]; @@ -93,23 +92,6 @@ namespace MediaBrowser.Controller.MediaEncoding return defaultEncoder; } - private bool CheckVaapi(EncodingJobInfo state, string hwType, EncodingOptions encodingOptions) - { - if (!string.Equals(hwType, "vaapi", StringComparison.OrdinalIgnoreCase)) - { - // No vaapi requested, return OK. - return true; - } - - if (string.IsNullOrEmpty(encodingOptions.VaapiDevice)) - { - // No device specified, return OK. - return true; - } - - return IsVaapiSupported(state); - } - private bool IsVaapiSupported(EncodingJobInfo state) { var videoStream = state.VideoStream; @@ -424,7 +406,13 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase)) { - return "aac -strict experimental"; + // Use libfdk_aac for better audio quality if using custom build of FFmpeg which has fdk_aac support + if (_mediaEncoder.SupportsEncoder("libfdk_aac")) + { + return "libfdk_aac"; + } + + return "aac"; } if (string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase)) @@ -460,16 +448,7 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.IsVideoRequest && string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) { - var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; - var hwOutputFormat = "vaapi"; - - if (hasGraphicalSubs) - { - hwOutputFormat = "yuv420p"; - } - - arg.Append("-hwaccel vaapi -hwaccel_output_format ") - .Append(hwOutputFormat) + arg.Append("-hwaccel vaapi -hwaccel_output_format vaapi") .Append(" -vaapi_device ") .Append(encodingOptions.VaapiDevice) .Append(' '); @@ -480,20 +459,26 @@ namespace MediaBrowser.Controller.MediaEncoding { var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions); var outputVideoCodec = GetVideoEncoder(state, encodingOptions); + + var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; - if (encodingOptions.EnableHardwareEncoding && outputVideoCodec.Contains("qsv", StringComparison.OrdinalIgnoreCase)) + if (!hasTextSubs) { - if (!string.IsNullOrEmpty(videoDecoder) && videoDecoder.Contains("qsv", StringComparison.OrdinalIgnoreCase)) + // While using QSV encoder + if ((outputVideoCodec ?? string.Empty).IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1) { - arg.Append("-hwaccel qsv "); - } - else - { - arg.Append("-init_hw_device qsv=hw -filter_hw_device hw "); + // While using QSV decoder + if ((videoDecoder ?? string.Empty).IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1) + { + arg.Append("-hwaccel qsv "); + } + // While using SW decoder + else + { + arg.Append("-init_hw_device qsv=hw -filter_hw_device hw "); + } } } - - arg.Append(videoDecoder + " "); } arg.Append("-i ") @@ -503,17 +488,6 @@ namespace MediaBrowser.Controller.MediaEncoding && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream) { - if (state.VideoStream != null && state.VideoStream.Width.HasValue) - { - // This is hacky but not sure how to get the exact subtitle resolution - int height = Convert.ToInt32(state.VideoStream.Width.Value / 16.0 * 9.0); - - arg.Append(" -canvas_size ") - .Append(state.VideoStream.Width.Value.ToString(CultureInfo.InvariantCulture)) - .Append(':') - .Append(height.ToString(CultureInfo.InvariantCulture)); - } - var subtitlePath = state.SubtitleStream.Path; if (string.Equals(Path.GetExtension(subtitlePath), ".sub", StringComparison.OrdinalIgnoreCase)) @@ -1546,9 +1520,12 @@ namespace MediaBrowser.Controller.MediaEncoding } /// <summary> - /// Gets the internal graphical subtitle param. + /// Gets the graphical subtitle param. /// </summary> - public string GetGraphicalSubtitleParam(EncodingJobInfo state, EncodingOptions options, string outputVideoCodec) + public string GetGraphicalSubtitleParam( + EncodingJobInfo state, + EncodingOptions options, + string outputVideoCodec) { var outputSizeParam = string.Empty; @@ -1562,53 +1539,77 @@ namespace MediaBrowser.Controller.MediaEncoding { outputSizeParam = GetOutputSizeParam(state, options, outputVideoCodec).TrimEnd('"'); - if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + var index = outputSizeParam.IndexOf("hwdownload", StringComparison.OrdinalIgnoreCase); + if (index != -1) { - var index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase); - if (index != -1) - { - outputSizeParam = "," + outputSizeParam.Substring(index); - } + outputSizeParam = "," + outputSizeParam.Substring(index); } else { - var index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase); + index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase); if (index != -1) { outputSizeParam = "," + outputSizeParam.Substring(index); } - } - } - - if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) - && outputSizeParam.Length == 0) - { - outputSizeParam = ",format=nv12|vaapi,hwupload"; - - // Add parameters to use VAAPI with burn-in subttiles (GH issue #642) - if (state.SubtitleStream != null - && state.SubtitleStream.IsTextSubtitleStream - && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) { - outputSizeParam += ",hwmap=mode=read+write+direct"; + else + { + index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase); + if (index != -1) + { + outputSizeParam = "," + outputSizeParam.Substring(index); + } + else + { + index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase); + if (index != -1) + { + outputSizeParam = "," + outputSizeParam.Substring(index); + } + } + } } } var videoSizeParam = string.Empty; + var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options); // Setup subtitle scaling if (state.VideoStream != null && state.VideoStream.Width.HasValue && state.VideoStream.Height.HasValue) { + // force_original_aspect_ratio=decrease + // Enable decreasing output video width or height if necessary to keep the original aspect ratio videoSizeParam = string.Format( CultureInfo.InvariantCulture, - "scale={0}:{1}", + "scale={0}:{1}:force_original_aspect_ratio=decrease", state.VideoStream.Width.Value, state.VideoStream.Height.Value); - //For QSV, feed it into hardware encoder now + // For QSV, feed it into hardware encoder now if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) { videoSizeParam += ",hwupload=extra_hw_frames=64"; } + + // For VAAPI and CUVID decoder + // these encoders cannot automatically adjust the size of graphical subtitles to fit the output video, + // thus needs to be manually adjusted. + if ((IsVaapiSupported(state) && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) + || (videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1) + { + var videoStream = state.VideoStream; + var inputWidth = videoStream?.Width; + var inputHeight = videoStream?.Height; + var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight); + + if (width.HasValue && height.HasValue) + { + videoSizeParam = string.Format( + CultureInfo.InvariantCulture, + "scale={0}:{1}:force_original_aspect_ratio=decrease", + width.Value, + height.Value); + } + } } var mapPrefix = state.SubtitleStream.IsExternal ? @@ -1619,12 +1620,35 @@ namespace MediaBrowser.Controller.MediaEncoding ? 0 : state.SubtitleStream.Index; - var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options); - // Setup default filtergraph utilizing FFMpeg overlay() and FFMpeg scale() (see the return of this function for index reference) var retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay{3}\""; - if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + // When the input may or may not be hardware VAAPI decodable + if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + { + /* + [base]: HW scaling video to OutputSize + [sub]: SW scaling subtitle to FixedOutputSize + [base][sub]: SW overlay + */ + outputSizeParam = outputSizeParam.TrimStart(','); + retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3},hwdownload[base];[base][sub]overlay,format=nv12,hwupload\""; + } + + // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first + else if (IsVaapiSupported(state) && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) + && string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase)) + { + /* + [base]: SW scaling video to OutputSize + [sub]: SW scaling subtitle to FixedOutputSize + [base][sub]: SW overlay + */ + outputSizeParam = outputSizeParam.TrimStart(','); + retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\""; + } + + else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) { /* QSV in FFMpeg can now setup hardware overlay for transcodes. @@ -1688,7 +1712,8 @@ namespace MediaBrowser.Controller.MediaEncoding return (Convert.ToInt32(outputWidth), Convert.ToInt32(outputHeight)); } - public List<string> GetScalingFilters(int? videoWidth, + public List<string> GetScalingFilters(EncodingJobInfo state, + int? videoWidth, int? videoHeight, Video3DFormat? threedFormat, string videoDecoder, @@ -1707,7 +1732,9 @@ namespace MediaBrowser.Controller.MediaEncoding requestedMaxWidth, requestedMaxHeight); - if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) || string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + + if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) || (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) && !hasTextSubs) && width.HasValue && height.HasValue) { @@ -1737,7 +1764,7 @@ namespace MediaBrowser.Controller.MediaEncoding filters.Add(string.Format(CultureInfo.InvariantCulture, "scale_{0}=format=nv12", vaapi_or_qsv)); } } - else if ((videoDecoder ?? string.Empty).IndexOf("_cuvid", StringComparison.OrdinalIgnoreCase) != -1 + else if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1 && width.HasValue && height.HasValue) { @@ -1941,8 +1968,7 @@ namespace MediaBrowser.Controller.MediaEncoding public string GetOutputSizeParam( EncodingJobInfo state, EncodingOptions options, - string outputVideoCodec, - bool allowTimeStampCopy = true) + string outputVideoCodec) { // http://sonnati.wordpress.com/2012/10/19/ffmpeg-the-swiss-army-knife-of-internet-streaming-part-vi/ @@ -1951,42 +1977,61 @@ namespace MediaBrowser.Controller.MediaEncoding var videoStream = state.VideoStream; var filters = new List<string>(); - // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first - var hwType = options.HardwareAccelerationType ?? string.Empty; - if (string.Equals(hwType, "vaapi", StringComparison.OrdinalIgnoreCase) && !options.EnableHardwareEncoding ) - { - filters.Add("hwdownload"); + var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options); + var inputWidth = videoStream?.Width; + var inputHeight = videoStream?.Height; + var threeDFormat = state.MediaSource.Video3DFormat; - // If transcoding from 10 bit, transform colour spaces too - if (!string.IsNullOrEmpty(videoStream.PixelFormat) - && videoStream.PixelFormat.IndexOf("p10", StringComparison.OrdinalIgnoreCase) != -1 - && string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase)) - { - filters.Add("format=p010le"); - filters.Add("format=nv12"); - } - else - { - filters.Add("format=nv12"); - } - } + var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + // When the input may or may not be hardware VAAPI decodable if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) { filters.Add("format=nv12|vaapi"); filters.Add("hwupload"); } - var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options); - - // If we are software decoding, and hardware encoding - if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase) - && (string.IsNullOrEmpty(videoDecoder) || !videoDecoder.Contains("qsv", StringComparison.OrdinalIgnoreCase))) + // When the input may or may not be hardware QSV decodable + else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) { - filters.Add("format=nv12|qsv"); - filters.Add("hwupload=extra_hw_frames=64"); + if (!hasTextSubs) + { + filters.Add("format=nv12|qsv"); + filters.Add("hwupload=extra_hw_frames=64"); + } } + // If we're hardware VAAPI decoding and software encoding, download frames from the decoder first + else if (IsVaapiSupported(state) && string.Equals(options.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase) + && string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase)) + { + var codec = videoStream.Codec.ToLowerInvariant(); + var isColorDepth10 = !string.IsNullOrEmpty(videoStream.Profile) && (videoStream.Profile.Contains("Main 10", StringComparison.OrdinalIgnoreCase) + || videoStream.Profile.Contains("High 10", StringComparison.OrdinalIgnoreCase)); + + // Assert 10-bit hardware VAAPI decodable + if (isColorDepth10 && (string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))) + { + /* + Download data from GPU to CPU as p010le format. + Colorspace conversion is unnecessary here as libx264 will handle it. + If this step is missing, it will fail on AMD but not on intel. + */ + filters.Add("hwdownload"); + filters.Add("format=p010le"); + } + + // Assert 8-bit hardware VAAPI decodable + else if (!isColorDepth10) + { + filters.Add("hwdownload"); + filters.Add("format=nv12"); + } + } + + // Add hardware deinterlace filter before scaling filter if (state.DeInterlace("h264", true)) { if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) @@ -1995,17 +2040,23 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) { - filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_qsv")); + if (!hasTextSubs) + { + filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_qsv")); + } } } - if ((state.DeInterlace("h264", true) || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true)) - && !string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + // Add software deinterlace filter before scaling filter + if (((state.DeInterlace("h264", true) || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true)) + && !string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) + && !string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + || (hasTextSubs && state.DeInterlace("h264", true) && string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))) { var inputFramerate = videoStream?.RealFrameRate; // If it is already 60fps then it will create an output framerate that is much too high for roku and others to handle - if (string.Equals(options.DeinterlaceMethod, "bobandweave", StringComparison.OrdinalIgnoreCase) && (inputFramerate ?? 60) <= 30) + if (string.Equals(options.DeinterlaceMethod, "yadif_bob", StringComparison.OrdinalIgnoreCase) && (inputFramerate ?? 60) <= 30) { filters.Add("yadif=1:-1:0"); } @@ -2015,11 +2066,21 @@ namespace MediaBrowser.Controller.MediaEncoding } } - var inputWidth = videoStream?.Width; - var inputHeight = videoStream?.Height; - var threeDFormat = state.MediaSource.Video3DFormat; + // Add scaling filter: scale_*=format=nv12 or scale_*=w=*:h=*:format=nv12 or scale=expr + filters.AddRange(GetScalingFilters(state, inputWidth, inputHeight, threeDFormat, videoDecoder, outputVideoCodec, request.Width, request.Height, request.MaxWidth, request.MaxHeight)); - filters.AddRange(GetScalingFilters(inputWidth, inputHeight, threeDFormat, videoDecoder, outputVideoCodec, request.Width, request.Height, request.MaxWidth, request.MaxHeight)); + // Add parameters to use VAAPI with burn-in text subttiles (GH issue #642) + if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + { + if (state.SubtitleStream != null + && state.SubtitleStream.IsTextSubtitleStream + && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) + { + // Test passed on Intel and AMD gfx + filters.Add("hwmap=mode=read+write"); + filters.Add("format=nv12"); + } + } var output = string.Empty; @@ -2037,11 +2098,6 @@ namespace MediaBrowser.Controller.MediaEncoding { filters.Add("hwmap"); } - - if (allowTimeStampCopy) - { - output += " -copyts"; - } } if (filters.Count > 0) @@ -2218,7 +2274,7 @@ namespace MediaBrowser.Controller.MediaEncoding { inputModifier += " " + videoDecoder; - if ((videoDecoder ?? string.Empty).IndexOf("_cuvid", StringComparison.OrdinalIgnoreCase) != -1) + if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1) { var videoStream = state.VideoStream; var inputWidth = videoStream?.Width; @@ -2227,7 +2283,7 @@ namespace MediaBrowser.Controller.MediaEncoding var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight); - if ((videoDecoder ?? string.Empty).IndexOf("_cuvid", StringComparison.OrdinalIgnoreCase) != -1 + if ((videoDecoder ?? string.Empty).IndexOf("cuvid", StringComparison.OrdinalIgnoreCase) != -1 && width.HasValue && height.HasValue) { @@ -2525,6 +2581,12 @@ namespace MediaBrowser.Controller.MediaEncoding case "h264": if (_mediaEncoder.SupportsDecoder("h264_cuvid") && encodingOptions.HardwareDecodingCodecs.Contains("h264", StringComparer.OrdinalIgnoreCase)) { + // cuvid decoder does not support 10-bit input + if ((videoStream.BitDepth ?? 8) > 8) + { + encodingOptions.HardwareDecodingCodecs = Array.Empty<string>(); + return null; + } return "-c:v h264_cuvid "; } break; @@ -2637,7 +2699,7 @@ namespace MediaBrowser.Controller.MediaEncoding { if (Environment.OSVersion.Platform == PlatformID.Win32NT) { - if(Environment.OSVersion.Version.Major > 6 || (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor > 1)) + if (Environment.OSVersion.Version.Major > 6 || (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor > 1)) return "-hwaccel d3d11va"; else return "-hwaccel dxva2"; @@ -2772,14 +2834,27 @@ namespace MediaBrowser.Controller.MediaEncoding var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasCopyTs = false; + // Add resolution params, if specified if (!hasGraphicalSubs) { var outputSizeParam = GetOutputSizeParam(state, encodingOptions, videoCodec); + args += outputSizeParam; + hasCopyTs = outputSizeParam.IndexOf("copyts", StringComparison.OrdinalIgnoreCase) != -1; } + // This is for graphical subs + if (hasGraphicalSubs) + { + var graphicalSubtitleParam = GetGraphicalSubtitleParam(state, encodingOptions, videoCodec); + + args += graphicalSubtitleParam; + + hasCopyTs = graphicalSubtitleParam.IndexOf("copyts", StringComparison.OrdinalIgnoreCase) != -1; + } + if (state.RunTimeTicks.HasValue && state.BaseRequest.CopyTimestamps) { if (!hasCopyTs) @@ -2787,13 +2862,12 @@ namespace MediaBrowser.Controller.MediaEncoding args += " -copyts"; } - args += " -avoid_negative_ts disabled -start_at_zero"; - } + args += " -avoid_negative_ts disabled"; - // This is for internal graphical subs - if (hasGraphicalSubs) - { - args += GetGraphicalSubtitleParam(state, encodingOptions, videoCodec); + if (!(state.SubtitleStream != null && state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream)) + { + args += " -start_at_zero"; + } } var qualityParam = GetVideoQualityParam(state, videoCodec, encodingOptions, defaultPreset); @@ -2899,6 +2973,5 @@ namespace MediaBrowser.Controller.MediaEncoding string.Empty, string.Empty).Trim(); } - } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs index 38ef33caf..1127a08de 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobInfo.cs @@ -9,8 +9,8 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Session; using MediaBrowser.Model.Net; +using MediaBrowser.Model.Session; namespace MediaBrowser.Controller.MediaEncoding { diff --git a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs index e560999e8..15a2580af 100644 --- a/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs +++ b/MediaBrowser.Controller/MediaEncoding/IEncodingManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -12,6 +14,6 @@ namespace MediaBrowser.Controller.MediaEncoding /// <summary> /// Refreshes the chapter images. /// </summary> - Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, List<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken); + Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs index 46933c046..806478864 100644 --- a/MediaBrowser.Controller/Net/IHttpServer.cs +++ b/MediaBrowser.Controller/Net/IHttpServer.cs @@ -32,7 +32,7 @@ namespace MediaBrowser.Controller.Net /// <summary> /// Inits this instance. /// </summary> - void Init(IEnumerable<IService> services, IEnumerable<IWebSocketListener> listener, IEnumerable<string> urlPrefixes); + void Init(IEnumerable<Type> serviceTypes, IEnumerable<IWebSocketListener> listener, IEnumerable<string> urlPrefixes); /// <summary> /// If set, all requests will respond with this message diff --git a/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs b/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs index 4424e044b..c2dcb66d7 100644 --- a/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs +++ b/MediaBrowser.Controller/Persistence/IDisplayPreferencesRepository.cs @@ -6,30 +6,34 @@ using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Persistence { /// <summary> - /// Interface IDisplayPreferencesRepository + /// Interface IDisplayPreferencesRepository. /// </summary> public interface IDisplayPreferencesRepository : IRepository { /// <summary> - /// Saves display preferences for an item + /// Saves display preferences for an item. /// </summary> /// <param name="displayPreferences">The display preferences.</param> /// <param name="userId">The user id.</param> /// <param name="client">The client.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, - CancellationToken cancellationToken); + void SaveDisplayPreferences( + DisplayPreferences displayPreferences, + string userId, + string client, + CancellationToken cancellationToken); /// <summary> - /// Saves all display preferences for a user + /// Saves all display preferences for a user. /// </summary> /// <param name="displayPreferences">The display preferences.</param> /// <param name="userId">The user id.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - void SaveAllDisplayPreferences(IEnumerable<DisplayPreferences> displayPreferences, Guid userId, - CancellationToken cancellationToken); + void SaveAllDisplayPreferences( + IEnumerable<DisplayPreferences> displayPreferences, + Guid userId, + CancellationToken cancellationToken); + /// <summary> /// Gets the display preferences. /// </summary> diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index 5a5b7f58f..75fc43a04 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -24,8 +24,7 @@ namespace MediaBrowser.Controller.Persistence /// Deletes the item. /// </summary> /// <param name="id">The identifier.</param> - /// <param name="cancellationToken">The cancellation token.</param> - void DeleteItem(Guid id, CancellationToken cancellationToken); + void DeleteItem(Guid id); /// <summary> /// Saves the items. @@ -61,7 +60,7 @@ namespace MediaBrowser.Controller.Persistence /// <summary> /// Saves the chapters. /// </summary> - void SaveChapters(Guid id, List<ChapterInfo> chapters); + void SaveChapters(Guid id, IReadOnlyList<ChapterInfo> chapters); /// <summary> /// Gets the media streams. @@ -169,4 +168,3 @@ namespace MediaBrowser.Controller.Persistence List<string> GetAllArtistNames(); } } - diff --git a/MediaBrowser.Controller/Providers/AlbumInfo.cs b/MediaBrowser.Controller/Providers/AlbumInfo.cs index ac6b86c1d..dbda4843f 100644 --- a/MediaBrowser.Controller/Providers/AlbumInfo.cs +++ b/MediaBrowser.Controller/Providers/AlbumInfo.cs @@ -16,6 +16,7 @@ namespace MediaBrowser.Controller.Providers /// </summary> /// <value>The artist provider ids.</value> public Dictionary<string, string> ArtistProviderIds { get; set; } + public List<SongInfo> SongInfos { get; set; } public AlbumInfo() diff --git a/MediaBrowser.Controller/Providers/BoxSetInfo.cs b/MediaBrowser.Controller/Providers/BoxSetInfo.cs index 4cbe2d6ef..d23f2b9bf 100644 --- a/MediaBrowser.Controller/Providers/BoxSetInfo.cs +++ b/MediaBrowser.Controller/Providers/BoxSetInfo.cs @@ -2,6 +2,5 @@ namespace MediaBrowser.Controller.Providers { public class BoxSetInfo : ItemLookupInfo { - } } diff --git a/MediaBrowser.Controller/Providers/DirectoryService.cs b/MediaBrowser.Controller/Providers/DirectoryService.cs index 303b03a21..b7640c205 100644 --- a/MediaBrowser.Controller/Providers/DirectoryService.cs +++ b/MediaBrowser.Controller/Providers/DirectoryService.cs @@ -26,7 +26,6 @@ namespace MediaBrowser.Controller.Providers { entries = _fileSystem.GetFileSystemEntries(path).ToArray(); - //_cache.TryAdd(path, entries); _cache[path] = entries; } @@ -56,7 +55,6 @@ namespace MediaBrowser.Controller.Providers if (file != null && file.Exists) { - //_fileCache.TryAdd(path, file); _fileCache[path] = file; } else @@ -66,15 +64,12 @@ namespace MediaBrowser.Controller.Providers } return file; - //return _fileSystem.GetFileInfo(path); } - public List<string> GetFilePaths(string path) - { - return GetFilePaths(path, false); - } + public IReadOnlyList<string> GetFilePaths(string path) + => GetFilePaths(path, false); - public List<string> GetFilePaths(string path, bool clearCache) + public IReadOnlyList<string> GetFilePaths(string path, bool clearCache) { if (clearCache || !_filePathCache.TryGetValue(path, out List<string> result)) { diff --git a/MediaBrowser.Controller/Providers/DynamicImageInfo.cs b/MediaBrowser.Controller/Providers/DynamicImageInfo.cs deleted file mode 100644 index 0791783c6..000000000 --- a/MediaBrowser.Controller/Providers/DynamicImageInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Controller.Providers -{ - public class DynamicImageInfo - { - public string ImageId { get; set; } - public ImageType Type { get; set; } - } -} diff --git a/MediaBrowser.Controller/Providers/DynamicImageResponse.cs b/MediaBrowser.Controller/Providers/DynamicImageResponse.cs index 11c7ccbe5..7c1371702 100644 --- a/MediaBrowser.Controller/Providers/DynamicImageResponse.cs +++ b/MediaBrowser.Controller/Providers/DynamicImageResponse.cs @@ -8,9 +8,13 @@ namespace MediaBrowser.Controller.Providers public class DynamicImageResponse { public string Path { get; set; } + public MediaProtocol Protocol { get; set; } + public Stream Stream { get; set; } + public ImageFormat Format { get; set; } + public bool HasImage { get; set; } public void SetFormatFromMimeType(string mimeType) diff --git a/MediaBrowser.Controller/Providers/EpisodeInfo.cs b/MediaBrowser.Controller/Providers/EpisodeInfo.cs index 6ecf4edc5..55c41ff82 100644 --- a/MediaBrowser.Controller/Providers/EpisodeInfo.cs +++ b/MediaBrowser.Controller/Providers/EpisodeInfo.cs @@ -10,6 +10,7 @@ namespace MediaBrowser.Controller.Providers public int? IndexNumberEnd { get; set; } public bool IsMissingEpisode { get; set; } + public string SeriesDisplayOrder { get; set; } public EpisodeInfo() diff --git a/MediaBrowser.Controller/Providers/ExtraInfo.cs b/MediaBrowser.Controller/Providers/ExtraInfo.cs deleted file mode 100644 index 413ff2e78..000000000 --- a/MediaBrowser.Controller/Providers/ExtraInfo.cs +++ /dev/null @@ -1,15 +0,0 @@ -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Controller.Providers -{ - public class ExtraInfo - { - public string Path { get; set; } - - public LocationType LocationType { get; set; } - - public bool IsDownloadable { get; set; } - - public ExtraType ExtraType { get; set; } - } -} diff --git a/MediaBrowser.Controller/Providers/ExtraSource.cs b/MediaBrowser.Controller/Providers/ExtraSource.cs deleted file mode 100644 index 46b246fbc..000000000 --- a/MediaBrowser.Controller/Providers/ExtraSource.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MediaBrowser.Controller.Providers -{ - public enum ExtraSource - { - Local = 1, - Metadata = 2, - Remote = 3 - } -} diff --git a/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs b/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs index 2d5a0b364..6b4c9feb5 100644 --- a/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs +++ b/MediaBrowser.Controller/Providers/ICustomMetadataProvider.cs @@ -13,7 +13,7 @@ namespace MediaBrowser.Controller.Providers where TItemType : BaseItem { /// <summary> - /// Fetches the asynchronous. + /// Fetches the metadata asynchronously. /// </summary> /// <param name="item">The item.</param> /// <param name="options">The options.</param> diff --git a/MediaBrowser.Controller/Providers/IDirectoryService.cs b/MediaBrowser.Controller/Providers/IDirectoryService.cs index 8059d2bd1..949a17740 100644 --- a/MediaBrowser.Controller/Providers/IDirectoryService.cs +++ b/MediaBrowser.Controller/Providers/IDirectoryService.cs @@ -6,10 +6,13 @@ namespace MediaBrowser.Controller.Providers public interface IDirectoryService { FileSystemMetadata[] GetFileSystemEntries(string path); + List<FileSystemMetadata> GetFiles(string path); + FileSystemMetadata GetFile(string path); - List<string> GetFilePaths(string path); - List<string> GetFilePaths(string path, bool clearCache); + IReadOnlyList<string> GetFilePaths(string path); + + IReadOnlyList<string> GetFilePaths(string path, bool clearCache); } } diff --git a/MediaBrowser.Controller/Providers/IExtrasProvider.cs b/MediaBrowser.Controller/Providers/IExtrasProvider.cs deleted file mode 100644 index fa31635cb..000000000 --- a/MediaBrowser.Controller/Providers/IExtrasProvider.cs +++ /dev/null @@ -1,20 +0,0 @@ -using MediaBrowser.Controller.Entities; - -namespace MediaBrowser.Controller.Providers -{ - public interface IExtrasProvider - { - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> - string Name { get; } - - /// <summary> - /// Supportses the specified item. - /// </summary> - /// <param name="item">The item.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns> - bool Supports(BaseItem item); - } -} diff --git a/MediaBrowser.Controller/Providers/IForcedProvider.cs b/MediaBrowser.Controller/Providers/IForcedProvider.cs index 35fa29d94..5ae4a56ef 100644 --- a/MediaBrowser.Controller/Providers/IForcedProvider.cs +++ b/MediaBrowser.Controller/Providers/IForcedProvider.cs @@ -1,7 +1,7 @@ namespace MediaBrowser.Controller.Providers { /// <summary> - /// This is a marker interface that will cause a provider to run even if IsLocked=true + /// This is a marker interface that will cause a provider to run even if an item is locked from changes. /// </summary> public interface IForcedProvider { diff --git a/MediaBrowser.Controller/Providers/IImageProvider.cs b/MediaBrowser.Controller/Providers/IImageProvider.cs index 2df3d5ff8..29ab323f8 100644 --- a/MediaBrowser.Controller/Providers/IImageProvider.cs +++ b/MediaBrowser.Controller/Providers/IImageProvider.cs @@ -3,7 +3,7 @@ using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Providers { /// <summary> - /// Interface IImageProvider + /// Interface IImageProvider. /// </summary> public interface IImageProvider { @@ -14,10 +14,10 @@ namespace MediaBrowser.Controller.Providers string Name { get; } /// <summary> - /// Supportses the specified item. + /// Supports the specified item. /// </summary> /// <param name="item">The item.</param> - /// <returns><c>true</c> if XXXX, <c>false</c> otherwise</returns> + /// <returns><c>true</c> if the provider supports the item.</returns> bool Supports(BaseItem item); } } diff --git a/MediaBrowser.Controller/Providers/ILocalImageFileProvider.cs b/MediaBrowser.Controller/Providers/ILocalImageFileProvider.cs deleted file mode 100644 index 72bc56390..000000000 --- a/MediaBrowser.Controller/Providers/ILocalImageFileProvider.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; -using MediaBrowser.Controller.Entities; - -namespace MediaBrowser.Controller.Providers -{ - public interface ILocalImageFileProvider : ILocalImageProvider - { - List<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService); - } -} diff --git a/MediaBrowser.Controller/Providers/ILocalImageProvider.cs b/MediaBrowser.Controller/Providers/ILocalImageProvider.cs index 09aaab3de..463c81376 100644 --- a/MediaBrowser.Controller/Providers/ILocalImageProvider.cs +++ b/MediaBrowser.Controller/Providers/ILocalImageProvider.cs @@ -1,9 +1,13 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; + namespace MediaBrowser.Controller.Providers { /// <summary> - /// This is just a marker interface + /// This is just a marker interface. /// </summary> public interface ILocalImageProvider : IImageProvider { + List<LocalImageInfo> GetImages(BaseItem item, IDirectoryService directoryService); } } diff --git a/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs b/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs index 2a2c379f6..44fb1b394 100644 --- a/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs +++ b/MediaBrowser.Controller/Providers/ILocalMetadataProvider.cs @@ -17,8 +17,9 @@ namespace MediaBrowser.Controller.Providers /// <param name="info">The information.</param> /// <param name="directoryService">The directory service.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task{MetadataResult{`0}}.</returns> - Task<MetadataResult<TItemType>> GetMetadata(ItemInfo info, + /// <returns>Task{MetadataResult{0}}.</returns> + Task<MetadataResult<TItemType>> GetMetadata( + ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken); } diff --git a/MediaBrowser.Controller/Providers/IMetadataService.cs b/MediaBrowser.Controller/Providers/IMetadataService.cs index 49f6a7830..21204e6d3 100644 --- a/MediaBrowser.Controller/Providers/IMetadataService.cs +++ b/MediaBrowser.Controller/Providers/IMetadataService.cs @@ -12,8 +12,9 @@ namespace MediaBrowser.Controller.Providers /// Determines whether this instance can refresh the specified item. /// </summary> /// <param name="item">The item.</param> - /// <returns><c>true</c> if this instance can refresh the specified item; otherwise, <c>false</c>.</returns> + /// <returns><c>true</c> if this instance can refresh the specified item.</returns> bool CanRefresh(BaseItem item); + bool CanRefreshPrimary(Type type); /// <summary> diff --git a/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs b/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs index 058010e1a..28da27ae7 100644 --- a/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs +++ b/MediaBrowser.Controller/Providers/IPreRefreshProvider.cs @@ -2,6 +2,5 @@ namespace MediaBrowser.Controller.Providers { public interface IPreRefreshProvider : ICustomMetadataProvider { - } } diff --git a/MediaBrowser.Controller/Providers/IProviderManager.cs b/MediaBrowser.Controller/Providers/IProviderManager.cs index 925ace895..254b27460 100644 --- a/MediaBrowser.Controller/Providers/IProviderManager.cs +++ b/MediaBrowser.Controller/Providers/IProviderManager.cs @@ -14,7 +14,7 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Controller.Providers { /// <summary> - /// Interface IProviderManager + /// Interface IProviderManager. /// </summary> public interface IProviderManager { @@ -159,13 +159,17 @@ namespace MediaBrowser.Controller.Providers Dictionary<Guid, Guid> GetRefreshQueue(); void OnRefreshStart(BaseItem item); + void OnRefreshProgress(BaseItem item, double progress); + void OnRefreshComplete(BaseItem item); double? GetRefreshProgress(Guid id); event EventHandler<GenericEventArgs<BaseItem>> RefreshStarted; + event EventHandler<GenericEventArgs<BaseItem>> RefreshCompleted; + event EventHandler<GenericEventArgs<Tuple<BaseItem, double>>> RefreshProgress; } diff --git a/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs b/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs index e56bba3e3..68a968f90 100644 --- a/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs +++ b/MediaBrowser.Controller/Providers/IRemoteImageProvider.cs @@ -9,7 +9,7 @@ using MediaBrowser.Model.Providers; namespace MediaBrowser.Controller.Providers { /// <summary> - /// Interface IImageProvider + /// Interface IImageProvider. /// </summary> public interface IRemoteImageProvider : IImageProvider { diff --git a/MediaBrowser.Controller/Providers/ItemInfo.cs b/MediaBrowser.Controller/Providers/ItemInfo.cs index f29a8aa70..d61153dfa 100644 --- a/MediaBrowser.Controller/Providers/ItemInfo.cs +++ b/MediaBrowser.Controller/Providers/ItemInfo.cs @@ -23,10 +23,15 @@ namespace MediaBrowser.Controller.Providers } public Type ItemType { get; set; } + public string Path { get; set; } + public string ContainingFolderPath { get; set; } + public VideoType VideoType { get; set; } + public bool IsInMixedFolder { get; set; } + public bool IsPlaceHolder { get; set; } } } diff --git a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs index 0aaab9a94..4707b0c7f 100644 --- a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs +++ b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs @@ -11,29 +11,37 @@ namespace MediaBrowser.Controller.Providers /// </summary> /// <value>The name.</value> public string Name { get; set; } + /// <summary> /// Gets or sets the metadata language. /// </summary> /// <value>The metadata language.</value> public string MetadataLanguage { get; set; } + /// <summary> /// Gets or sets the metadata country code. /// </summary> /// <value>The metadata country code.</value> public string MetadataCountryCode { get; set; } + /// <summary> /// Gets or sets the provider ids. /// </summary> /// <value>The provider ids.</value> public Dictionary<string, string> ProviderIds { get; set; } + /// <summary> /// Gets or sets the year. /// </summary> /// <value>The year.</value> public int? Year { get; set; } + public int? IndexNumber { get; set; } + public int? ParentIndexNumber { get; set; } + public DateTime? PremiereDate { get; set; } + public bool IsAutomated { get; set; } public ItemLookupInfo() diff --git a/MediaBrowser.Controller/Providers/LocalImageInfo.cs b/MediaBrowser.Controller/Providers/LocalImageInfo.cs index 24cded79b..184281025 100644 --- a/MediaBrowser.Controller/Providers/LocalImageInfo.cs +++ b/MediaBrowser.Controller/Providers/LocalImageInfo.cs @@ -6,6 +6,7 @@ namespace MediaBrowser.Controller.Providers public class LocalImageInfo { public FileSystemMetadata FileInfo { get; set; } + public ImageType Type { get; set; } } } diff --git a/MediaBrowser.Controller/Providers/MetadataProviderPriority.cs b/MediaBrowser.Controller/Providers/MetadataProviderPriority.cs deleted file mode 100644 index 0076bb972..000000000 --- a/MediaBrowser.Controller/Providers/MetadataProviderPriority.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace MediaBrowser.Controller.Providers -{ - /// <summary> - /// Determines when a provider should execute, relative to others - /// </summary> - public enum MetadataProviderPriority - { - // Run this provider at the beginning - /// <summary> - /// The first - /// </summary> - First = 1, - - // Run this provider after all first priority providers - /// <summary> - /// The second - /// </summary> - Second = 2, - - // Run this provider after all second priority providers - /// <summary> - /// The third - /// </summary> - Third = 3, - - /// <summary> - /// The fourth - /// </summary> - Fourth = 4, - - Fifth = 5, - - // Run this provider last - /// <summary> - /// The last - /// </summary> - Last = 999 - } -} diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs index b3eb8cdd1..0a473b80c 100644 --- a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs +++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs @@ -13,11 +13,13 @@ namespace MediaBrowser.Controller.Providers public bool ReplaceAllMetadata { get; set; } public MetadataRefreshMode MetadataRefreshMode { get; set; } + public RemoteSearchResult SearchResult { get; set; } public string[] RefreshPaths { get; set; } public bool ForceSave { get; set; } + public bool EnableRemoteContentProbe { get; set; } public MetadataRefreshOptions(IDirectoryService directoryService) diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs index ebff81b7f..59adaedfa 100644 --- a/MediaBrowser.Controller/Providers/MetadataResult.cs +++ b/MediaBrowser.Controller/Providers/MetadataResult.cs @@ -8,6 +8,7 @@ namespace MediaBrowser.Controller.Providers public class MetadataResult<T> { public List<LocalImageInfo> Images { get; set; } + public List<UserItemData> UserDataList { get; set; } public MetadataResult() @@ -19,10 +20,15 @@ namespace MediaBrowser.Controller.Providers public List<PersonInfo> People { get; set; } public bool HasMetadata { get; set; } + public T Item { get; set; } + public string ResultLanguage { get; set; } + public string Provider { get; set; } + public bool QueriedById { get; set; } + public void AddPerson(PersonInfo p) { if (People == null) diff --git a/MediaBrowser.Controller/Providers/MovieInfo.cs b/MediaBrowser.Controller/Providers/MovieInfo.cs index c9a766fe7..5b2c3ed03 100644 --- a/MediaBrowser.Controller/Providers/MovieInfo.cs +++ b/MediaBrowser.Controller/Providers/MovieInfo.cs @@ -2,6 +2,5 @@ namespace MediaBrowser.Controller.Providers { public class MovieInfo : ItemLookupInfo { - } } diff --git a/MediaBrowser.Controller/Providers/PersonLookupInfo.cs b/MediaBrowser.Controller/Providers/PersonLookupInfo.cs index 3fa6e50b7..a6218c039 100644 --- a/MediaBrowser.Controller/Providers/PersonLookupInfo.cs +++ b/MediaBrowser.Controller/Providers/PersonLookupInfo.cs @@ -2,6 +2,5 @@ namespace MediaBrowser.Controller.Providers { public class PersonLookupInfo : ItemLookupInfo { - } } diff --git a/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs b/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs index 078125673..a2ac6c9ae 100644 --- a/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs +++ b/MediaBrowser.Controller/Providers/RemoteSearchQuery.cs @@ -10,14 +10,14 @@ namespace MediaBrowser.Controller.Providers public Guid ItemId { get; set; } /// <summary> - /// If set will only search within the given provider + /// Will only search within the given provider when set. /// </summary> public string SearchProviderName { get; set; } /// <summary> - /// Gets or sets a value indicating whether [include disabled providers]. + /// Gets or sets a value indicating whether disabled providers should be included. /// </summary> - /// <value><c>true</c> if [include disabled providers]; otherwise, <c>false</c>.</value> + /// <value><c>true</c> if disabled providers should be included.</value> public bool IncludeDisabledProviders { get; set; } } } diff --git a/MediaBrowser.Controller/Sorting/SortExtensions.cs b/MediaBrowser.Controller/Sorting/SortExtensions.cs index f5ee574a2..2a68f4678 100644 --- a/MediaBrowser.Controller/Sorting/SortExtensions.cs +++ b/MediaBrowser.Controller/Sorting/SortExtensions.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; namespace MediaBrowser.Controller.Sorting { diff --git a/MediaBrowser.LocalMetadata/Images/CollectionFolderImageProvider.cs b/MediaBrowser.LocalMetadata/Images/CollectionFolderImageProvider.cs index 206e761bb..3bab1243c 100644 --- a/MediaBrowser.LocalMetadata/Images/CollectionFolderImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/CollectionFolderImageProvider.cs @@ -5,7 +5,7 @@ using MediaBrowser.Model.IO; namespace MediaBrowser.LocalMetadata.Images { - public class CollectionFolderLocalImageProvider : ILocalImageFileProvider, IHasOrder + public class CollectionFolderLocalImageProvider : ILocalImageProvider, IHasOrder { private readonly IFileSystem _fileSystem; diff --git a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs index 443f3fbb5..2f4cca5ff 100644 --- a/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/EpisodeLocalImageProvider.cs @@ -10,7 +10,7 @@ using MediaBrowser.Model.IO; namespace MediaBrowser.LocalMetadata.Images { - public class EpisodeLocalLocalImageProvider : ILocalImageFileProvider, IHasOrder + public class EpisodeLocalLocalImageProvider : ILocalImageProvider, IHasOrder { private readonly IFileSystem _fileSystem; diff --git a/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs b/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs index 25a8ad596..795933ce9 100644 --- a/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/InternalMetadataFolderImageProvider.cs @@ -9,7 +9,7 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.LocalMetadata.Images { - public class InternalMetadataFolderImageProvider : ILocalImageFileProvider, IHasOrder + public class InternalMetadataFolderImageProvider : ILocalImageProvider, IHasOrder { private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; diff --git a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs index 7c330ad86..16807f5a4 100644 --- a/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs +++ b/MediaBrowser.LocalMetadata/Images/LocalImageProvider.cs @@ -13,7 +13,7 @@ using MediaBrowser.Model.IO; namespace MediaBrowser.LocalMetadata.Images { - public class LocalImageProvider : ILocalImageFileProvider, IHasOrder + public class LocalImageProvider : ILocalImageProvider, IHasOrder { private readonly IFileSystem _fileSystem; @@ -30,7 +30,7 @@ namespace MediaBrowser.LocalMetadata.Images { if (item.SupportsLocalMetadata) { - // Episode has it's own provider + // Episode has its own provider if (item is Episode || item is Audio || item is Photo) { return false; diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs index c530c9fd8..3f177a9fa 100644 --- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs +++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs @@ -1,6 +1,6 @@ using System; -using System.Diagnostics; using System.Collections.Concurrent; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -155,47 +155,44 @@ namespace MediaBrowser.MediaEncoding.Attachments inputPath, attachmentStreamIndex, outputPath); - var startInfo = new ProcessStartInfo - { - Arguments = processArgs, - FileName = _mediaEncoder.EncoderPath, - UseShellExecute = false, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Hidden, - ErrorDialog = false - }; - var process = new Process - { - StartInfo = startInfo - }; - _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); + int exitCode; - process.Start(); + using (var process = new Process + { + StartInfo = new ProcessStartInfo + { + Arguments = processArgs, + FileName = _mediaEncoder.EncoderPath, + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + }, + EnableRaisingEvents = true + }) + { + _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); - var processTcs = new TaskCompletionSource<bool>(); - process.EnableRaisingEvents = true; - process.Exited += (sender, args) => processTcs.TrySetResult(true); - var unregister = cancellationToken.Register(() => processTcs.TrySetResult(process.HasExited)); - var ranToCompletion = await processTcs.Task.ConfigureAwait(false); - unregister.Dispose(); + process.Start(); - if (!ranToCompletion) - { - try - { - _logger.LogWarning("Killing ffmpeg attachment extraction process"); - process.Kill(); - } - catch (Exception ex) + var ranToCompletion = await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); + + if (!ranToCompletion) { - _logger.LogError(ex, "Error killing attachment extraction process"); + try + { + _logger.LogWarning("Killing ffmpeg attachment extraction process"); + process.Kill(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error killing attachment extraction process"); + } } - } - var exitCode = ranToCompletion ? process.ExitCode : -1; - - process.Dispose(); + exitCode = ranToCompletion ? process.ExitCode : -1; + } var failed = false; diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index f5decdc0d..6e036d24c 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -42,6 +42,7 @@ namespace MediaBrowser.MediaEncoding.Encoder "libvpx", "libvpx-vp9", "aac", + "libfdk_aac", "libmp3lame", "libopus", "libvorbis", diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 4123f0203..359330449 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -13,15 +13,15 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.MediaEncoding.Probing; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Diagnostics; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.System; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using System.Diagnostics; namespace MediaBrowser.MediaEncoding.Encoder { @@ -38,7 +38,6 @@ namespace MediaBrowser.MediaEncoding.Encoder private readonly ILogger _logger; private readonly IServerConfigurationManager _configurationManager; private readonly IFileSystem _fileSystem; - private readonly IProcessFactory _processFactory; private readonly ILocalizationManager _localization; private readonly Func<ISubtitleEncoder> _subtitleEncoder; private readonly IConfiguration _configuration; @@ -58,7 +57,6 @@ namespace MediaBrowser.MediaEncoding.Encoder ILogger<MediaEncoder> logger, IServerConfigurationManager configurationManager, IFileSystem fileSystem, - IProcessFactory processFactory, ILocalizationManager localization, Func<ISubtitleEncoder> subtitleEncoder, IConfiguration configuration, @@ -67,7 +65,6 @@ namespace MediaBrowser.MediaEncoding.Encoder _logger = logger; _configurationManager = configurationManager; _fileSystem = fileSystem; - _processFactory = processFactory; _localization = localization; _startupOptionFFmpegPath = startupOptionsFFmpegPath; _subtitleEncoder = subtitleEncoder; @@ -362,30 +359,33 @@ namespace MediaBrowser.MediaEncoding.Encoder : "{0} -i {1} -threads 0 -v warning -print_format json -show_streams -show_format"; args = string.Format(args, probeSizeArgument, inputPath).Trim(); - var process = _processFactory.Create(new ProcessOptions + var process = new Process { - CreateNoWindow = true, - UseShellExecute = false, + StartInfo = new ProcessStartInfo + { + CreateNoWindow = true, + UseShellExecute = false, - // Must consume both or ffmpeg may hang due to deadlocks. See comments below. - RedirectStandardOutput = true, + // Must consume both or ffmpeg may hang due to deadlocks. See comments below. + RedirectStandardOutput = true, - FileName = _ffprobePath, - Arguments = args, + FileName = _ffprobePath, + Arguments = args, - IsHidden = true, - ErrorDialog = false, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false, + }, EnableRaisingEvents = true - }); + }; if (forceEnableLogging) { - _logger.LogInformation("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); + _logger.LogInformation("{ProcessFileName} {ProcessArgs}", process.StartInfo.FileName, process.StartInfo.Arguments); } else { - _logger.LogDebug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); + _logger.LogDebug("{ProcessFileName} {ProcessArgs}", process.StartInfo.FileName, process.StartInfo.Arguments); } using (var processWrapper = new ProcessWrapper(process, this)) @@ -429,7 +429,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - return new ProbeResultNormalizer(_logger, _fileSystem, _localization).GetMediaInfo(result, videoType, isAudio, primaryPath, protocol); + return new ProbeResultNormalizer(_logger, _localization).GetMediaInfo(result, videoType, isAudio, primaryPath, protocol); } } @@ -478,7 +478,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } catch (Exception ex) { - _logger.LogError(ex, "I-frame image extraction failed, will attempt standard way. Input: {arguments}", inputArgument); + _logger.LogError(ex, "I-frame image extraction failed, will attempt standard way. Input: {Arguments}", inputArgument); } } @@ -571,18 +571,21 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - var process = _processFactory.Create(new ProcessOptions + var process = new Process { - CreateNoWindow = true, - UseShellExecute = false, - FileName = _ffmpegPath, - Arguments = args, - IsHidden = true, - ErrorDialog = false, + StartInfo = new ProcessStartInfo + { + CreateNoWindow = true, + UseShellExecute = false, + FileName = _ffmpegPath, + Arguments = args, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false, + }, EnableRaisingEvents = true - }); + }; - _logger.LogDebug("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); + _logger.LogDebug("{ProcessFileName} {ProcessArguments}", process.StartInfo.FileName, process.StartInfo.Arguments); using (var processWrapper = new ProcessWrapper(process, this)) { @@ -599,7 +602,7 @@ namespace MediaBrowser.MediaEncoding.Encoder timeoutMs = DefaultImageExtractionTimeout; } - ranToCompletion = await process.WaitForExitAsync(timeoutMs).ConfigureAwait(false); + ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false); if (!ranToCompletion) { @@ -700,23 +703,27 @@ namespace MediaBrowser.MediaEncoding.Encoder } } - var process = _processFactory.Create(new ProcessOptions + var processStartInfo = new ProcessStartInfo { CreateNoWindow = true, UseShellExecute = false, FileName = _ffmpegPath, Arguments = args, - IsHidden = true, - ErrorDialog = false, - EnableRaisingEvents = true - }); + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + }; - _logger.LogInformation(process.StartInfo.FileName + " " + process.StartInfo.Arguments); + _logger.LogInformation(processStartInfo.FileName + " " + processStartInfo.Arguments); await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false); bool ranToCompletion = false; + var process = new Process + { + StartInfo = processStartInfo, + EnableRaisingEvents = true + }; using (var processWrapper = new ProcessWrapper(process, this)) { try @@ -732,7 +739,7 @@ namespace MediaBrowser.MediaEncoding.Encoder while (isResponsive) { - if (await process.WaitForExitAsync(30000).ConfigureAwait(false)) + if (await process.WaitForExitAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false)) { ranToCompletion = true; break; @@ -949,22 +956,22 @@ namespace MediaBrowser.MediaEncoding.Encoder private bool _disposed = false; - public ProcessWrapper(IProcess process, MediaEncoder mediaEncoder) + public ProcessWrapper(Process process, MediaEncoder mediaEncoder) { Process = process; _mediaEncoder = mediaEncoder; Process.Exited += OnProcessExited; } - public IProcess Process { get; } + public Process Process { get; } public bool HasExited { get; private set; } public int? ExitCode { get; private set; } - void OnProcessExited(object sender, EventArgs e) + private void OnProcessExited(object sender, EventArgs e) { - var process = (IProcess)sender; + var process = (Process)sender; HasExited = true; @@ -979,7 +986,7 @@ namespace MediaBrowser.MediaEncoding.Encoder DisposeProcess(process); } - private void DisposeProcess(IProcess process) + private void DisposeProcess(Process process) { lock (_mediaEncoder._runningProcessesLock) { diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index f8047af42..b24d97f4e 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -9,7 +9,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; @@ -19,13 +18,11 @@ namespace MediaBrowser.MediaEncoding.Probing { private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly ILogger _logger; - private readonly IFileSystem _fileSystem; private readonly ILocalizationManager _localization; - public ProbeResultNormalizer(ILogger logger, IFileSystem fileSystem, ILocalizationManager localization) + public ProbeResultNormalizer(ILogger logger, ILocalizationManager localization) { _logger = logger; - _fileSystem = fileSystem; _localization = localization; } @@ -40,7 +37,7 @@ namespace MediaBrowser.MediaEncoding.Probing FFProbeHelpers.NormalizeFFProbeResult(data); SetSize(data, info); - var internalStreams = data.Streams ?? new MediaStreamInfo[] { }; + var internalStreams = data.Streams ?? Array.Empty<MediaStreamInfo>(); info.MediaStreams = internalStreams.Select(s => GetMediaStream(isAudio, s, data.Format)) .Where(i => i != null) @@ -539,7 +536,7 @@ namespace MediaBrowser.MediaEncoding.Probing if (!string.IsNullOrWhiteSpace(streamInfo.CodecTagString)) { - attachment.CodecTag = streamInfo.CodecTagString; + attachment.CodecTag = streamInfo.CodecTagString; } if (streamInfo.Tags != null) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 72db56974..ba171295e 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -12,7 +13,6 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Model.Diagnostics; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; @@ -31,7 +31,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles private readonly IMediaEncoder _mediaEncoder; private readonly IHttpClient _httpClient; private readonly IMediaSourceManager _mediaSourceManager; - private readonly IProcessFactory _processFactory; public SubtitleEncoder( ILibraryManager libraryManager, @@ -40,8 +39,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles IFileSystem fileSystem, IMediaEncoder mediaEncoder, IHttpClient httpClient, - IMediaSourceManager mediaSourceManager, - IProcessFactory processFactory) + IMediaSourceManager mediaSourceManager) { _libraryManager = libraryManager; _logger = logger; @@ -50,7 +48,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles _mediaEncoder = mediaEncoder; _httpClient = httpClient; _mediaSourceManager = mediaSourceManager; - _processFactory = processFactory; } private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles"); @@ -183,9 +180,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles private async Task<Stream> GetSubtitleStream(string path, MediaProtocol protocol, bool requiresCharset, CancellationToken cancellationToken) { - using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false)) + if (requiresCharset) { - if (requiresCharset) + using (var stream = await GetStream(path, protocol, cancellationToken).ConfigureAwait(false)) { var result = CharsetDetector.DetectFromStream(stream).Detected; stream.Position = 0; @@ -200,9 +197,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles return new MemoryStream(Encoding.UTF8.GetBytes(text)); } } - - return stream; } + + return File.OpenRead(path); } private async Task<SubtitleInfo> GetReadableFile( @@ -429,49 +426,53 @@ namespace MediaBrowser.MediaEncoding.Subtitles encodingParam = " -sub_charenc " + encodingParam; } - var process = _processFactory.Create(new ProcessOptions - { - CreateNoWindow = true, - UseShellExecute = false, - FileName = _mediaEncoder.EncoderPath, - Arguments = string.Format("{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath), - EnableRaisingEvents = true, - IsHidden = true, - ErrorDialog = false - }); - - _logger.LogInformation("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); + int exitCode; - try - { - process.Start(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error starting ffmpeg"); - - throw; - } - - var ranToCompletion = await process.WaitForExitAsync(300000).ConfigureAwait(false); + using (var process = new Process + { + StartInfo = new ProcessStartInfo + { + CreateNoWindow = true, + UseShellExecute = false, + FileName = _mediaEncoder.EncoderPath, + Arguments = string.Format("{0} -i \"{1}\" -c:s srt \"{2}\"", encodingParam, inputPath, outputPath), + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + }, + EnableRaisingEvents = true + }) + { + _logger.LogInformation("{0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments); - if (!ranToCompletion) - { try { - _logger.LogInformation("Killing ffmpeg subtitle conversion process"); - - process.Kill(); + process.Start(); } catch (Exception ex) { - _logger.LogError(ex, "Error killing subtitle conversion process"); + _logger.LogError(ex, "Error starting ffmpeg"); + + throw; } - } - var exitCode = ranToCompletion ? process.ExitCode : -1; + var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(5)).ConfigureAwait(false); + + if (!ranToCompletion) + { + try + { + _logger.LogInformation("Killing ffmpeg subtitle conversion process"); + + process.Kill(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error killing subtitle conversion process"); + } + } - process.Dispose(); + exitCode = ranToCompletion ? process.ExitCode : -1; + } var failed = false; @@ -578,49 +579,53 @@ namespace MediaBrowser.MediaEncoding.Subtitles outputCodec, outputPath); - var process = _processFactory.Create(new ProcessOptions - { - CreateNoWindow = true, - UseShellExecute = false, - EnableRaisingEvents = true, - FileName = _mediaEncoder.EncoderPath, - Arguments = processArgs, - IsHidden = true, - ErrorDialog = false - }); - - _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); - - try - { - process.Start(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error starting ffmpeg"); + int exitCode; - throw; - } - - var ranToCompletion = await process.WaitForExitAsync(300000).ConfigureAwait(false); + using (var process = new Process + { + StartInfo = new ProcessStartInfo + { + CreateNoWindow = true, + UseShellExecute = false, + FileName = _mediaEncoder.EncoderPath, + Arguments = processArgs, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false + }, + EnableRaisingEvents = true + }) + { + _logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments); - if (!ranToCompletion) - { try { - _logger.LogWarning("Killing ffmpeg subtitle extraction process"); - - process.Kill(); + process.Start(); } catch (Exception ex) { - _logger.LogError(ex, "Error killing subtitle extraction process"); + _logger.LogError(ex, "Error starting ffmpeg"); + + throw; } - } - var exitCode = ranToCompletion ? process.ExitCode : -1; + var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(5)).ConfigureAwait(false); + + if (!ranToCompletion) + { + try + { + _logger.LogWarning("Killing ffmpeg subtitle extraction process"); + + process.Kill(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error killing subtitle extraction process"); + } + } - process.Dispose(); + exitCode = ranToCompletion ? process.ExitCode : -1; + } var failed = false; @@ -731,6 +736,14 @@ namespace MediaBrowser.MediaEncoding.Subtitles { var charset = CharsetDetector.DetectFromStream(stream).Detected?.EncodingName; + // UTF16 is automatically converted to UTF8 by FFmpeg, do not specify a character encoding + if ((path.EndsWith(".ass") || path.EndsWith(".ssa")) + && (string.Equals(charset, "utf-16le", StringComparison.OrdinalIgnoreCase) + || string.Equals(charset, "utf-16be", StringComparison.OrdinalIgnoreCase))) + { + charset = ""; + } + _logger.LogDebug("charset {0} detected for {Path}", charset ?? "null", path); return charset; @@ -753,10 +766,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles return _httpClient.Get(opts); - case MediaProtocol.File: - return Task.FromResult<Stream>(File.OpenRead(path)); - default: - throw new ArgumentOutOfRangeException(nameof(protocol)); + case MediaProtocol.File: + return Task.FromResult<Stream>(File.OpenRead(path)); + default: + throw new ArgumentOutOfRangeException(nameof(protocol)); } } } diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index eaf9c4ecb..648568fd7 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -40,6 +40,7 @@ namespace MediaBrowser.Model.Configuration VaapiDevice = "/dev/dri/renderD128"; H264Crf = 23; H265Crf = 28; + DeinterlaceMethod = "yadif"; EnableHardwareEncoding = true; EnableSubtitleExtraction = true; HardwareDecodingCodecs = new string[] { "h264", "vc1" }; diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index dfb563180..b5e8d5589 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -15,9 +15,8 @@ namespace MediaBrowser.Model.Configuration private string _baseUrl; /// <summary> - /// Gets or sets a value indicating whether [enable u pn p]. + /// Gets or sets a value indicating whether to enable automatic port forwarding. /// </summary> - /// <value><c>true</c> if [enable u pn p]; otherwise, <c>false</c>.</value> public bool EnableUPnP { get; set; } /// <summary> @@ -148,9 +147,9 @@ namespace MediaBrowser.Model.Configuration public bool EnableDashboardResponseCaching { get; set; } /// <summary> - /// Allows the dashboard to be served from a custom path. + /// Gets or sets a custom path to serve the dashboard from. /// </summary> - /// <value>The dashboard source path.</value> + /// <value>The dashboard source path, or null if the default path should be used.</value> public string DashboardSourcePath { get; set; } /// <summary> @@ -254,7 +253,7 @@ namespace MediaBrowser.Model.Configuration AutoRunWebApp = true; EnableRemoteAccess = true; - EnableUPnP = true; + EnableUPnP = false; MinResumePct = 5; MaxResumePct = 90; diff --git a/MediaBrowser.Model/Diagnostics/IProcess.cs b/MediaBrowser.Model/Diagnostics/IProcess.cs deleted file mode 100644 index 514d1e737..000000000 --- a/MediaBrowser.Model/Diagnostics/IProcess.cs +++ /dev/null @@ -1,23 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.IO; -using System.Threading.Tasks; - -namespace MediaBrowser.Model.Diagnostics -{ - public interface IProcess : IDisposable - { - event EventHandler Exited; - - void Kill(); - bool WaitForExit(int timeMs); - Task<bool> WaitForExitAsync(int timeMs); - int ExitCode { get; } - void Start(); - StreamWriter StandardInput { get; } - StreamReader StandardError { get; } - StreamReader StandardOutput { get; } - ProcessOptions StartInfo { get; } - } -} diff --git a/MediaBrowser.Model/Diagnostics/IProcessFactory.cs b/MediaBrowser.Model/Diagnostics/IProcessFactory.cs deleted file mode 100644 index 57082acc5..000000000 --- a/MediaBrowser.Model/Diagnostics/IProcessFactory.cs +++ /dev/null @@ -1,24 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Diagnostics -{ - public interface IProcessFactory - { - IProcess Create(ProcessOptions options); - } - - public class ProcessOptions - { - public string FileName { get; set; } - public string Arguments { get; set; } - public string WorkingDirectory { get; set; } - public bool CreateNoWindow { get; set; } - public bool UseShellExecute { get; set; } - public bool EnableRaisingEvents { get; set; } - public bool ErrorDialog { get; set; } - public bool RedirectStandardError { get; set; } - public bool RedirectStandardInput { get; set; } - public bool RedirectStandardOutput { get; set; } - public bool IsHidden { get; set; } - } -} diff --git a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs index a5947bbf4..b43f8633e 100644 --- a/MediaBrowser.Model/Dlna/DirectPlayProfile.cs +++ b/MediaBrowser.Model/Dlna/DirectPlayProfile.cs @@ -25,12 +25,12 @@ namespace MediaBrowser.Model.Dlna public bool SupportsVideoCodec(string codec) { - return ContainerProfile.ContainsContainer(VideoCodec, codec); + return Type == DlnaProfileType.Video && ContainerProfile.ContainsContainer(VideoCodec, codec); } public bool SupportsAudioCodec(string codec) { - return ContainerProfile.ContainsContainer(AudioCodec, codec); + return (Type == DlnaProfileType.Audio || Type == DlnaProfileType.Video) && ContainerProfile.ContainsContainer(AudioCodec, codec); } } } diff --git a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs index 6a58b4adc..8235b72d1 100644 --- a/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs +++ b/MediaBrowser.Model/Dlna/ResolutionNormalizer.cs @@ -1,7 +1,6 @@ #pragma warning disable CS1591 using System; -using MediaBrowser.Model.Extensions; namespace MediaBrowser.Model.Dlna { diff --git a/MediaBrowser.Model/Dlna/SearchCriteria.cs b/MediaBrowser.Model/Dlna/SearchCriteria.cs index dc6d201ae..394fb9af9 100644 --- a/MediaBrowser.Model/Dlna/SearchCriteria.cs +++ b/MediaBrowser.Model/Dlna/SearchCriteria.cs @@ -2,7 +2,6 @@ using System; using System.Text.RegularExpressions; -using MediaBrowser.Model.Extensions; namespace MediaBrowser.Model.Dlna { diff --git a/MediaBrowser.Model/Entities/ChapterInfo.cs b/MediaBrowser.Model/Entities/ChapterInfo.cs index 2903ef61b..bea7ec1db 100644 --- a/MediaBrowser.Model/Entities/ChapterInfo.cs +++ b/MediaBrowser.Model/Entities/ChapterInfo.cs @@ -26,6 +26,7 @@ namespace MediaBrowser.Model.Entities /// </summary> /// <value>The image path.</value> public string ImagePath { get; set; } + public DateTime ImageDateModified { get; set; } public string ImageTag { get; set; } diff --git a/MediaBrowser.Model/Entities/DisplayPreferences.cs b/MediaBrowser.Model/Entities/DisplayPreferences.cs index 499baa058..2cd8bd306 100644 --- a/MediaBrowser.Model/Entities/DisplayPreferences.cs +++ b/MediaBrowser.Model/Entities/DisplayPreferences.cs @@ -101,7 +101,7 @@ namespace MediaBrowser.Model.Entities /// </summary> /// <value><c>true</c> if [show sidebar]; otherwise, <c>false</c>.</value> public bool ShowSidebar { get; set; } - + /// <summary> /// Gets or sets the client /// </summary> diff --git a/MediaBrowser.Model/Entities/ExtraType.cs b/MediaBrowser.Model/Entities/ExtraType.cs index 857e92adb..aca4bd282 100644 --- a/MediaBrowser.Model/Entities/ExtraType.cs +++ b/MediaBrowser.Model/Entities/ExtraType.cs @@ -4,6 +4,7 @@ namespace MediaBrowser.Model.Entities { public enum ExtraType { + Unknown = 0, Clip = 1, Trailer = 2, BehindTheScenes = 3, diff --git a/MediaBrowser.Model/Entities/ImageType.cs b/MediaBrowser.Model/Entities/ImageType.cs index 0f8208090..d89a4b3ad 100644 --- a/MediaBrowser.Model/Entities/ImageType.cs +++ b/MediaBrowser.Model/Entities/ImageType.cs @@ -44,6 +44,7 @@ namespace MediaBrowser.Model.Entities /// The box. /// </summary> Box = 7, + /// <summary> /// The screenshot. /// </summary> diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 37f9d7c1a..e7e8d7cec 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -69,9 +69,9 @@ namespace MediaBrowser.Model.Entities } } - public string localizedUndefined { get; set; } - public string localizedDefault { get; set; } - public string localizedForced { get; set; } + public string localizedUndefined { get; set; } + public string localizedDefault { get; set; } + public string localizedForced { get; set; } public string DisplayTitle { diff --git a/MediaBrowser.Model/Entities/SeriesStatus.cs b/MediaBrowser.Model/Entities/SeriesStatus.cs index 51351c135..c77c4a8ad 100644 --- a/MediaBrowser.Model/Entities/SeriesStatus.cs +++ b/MediaBrowser.Model/Entities/SeriesStatus.cs @@ -9,7 +9,7 @@ namespace MediaBrowser.Model.Entities /// The continuing. /// </summary> Continuing, - + /// <summary> /// The ended. /// </summary> diff --git a/MediaBrowser.Model/Entities/SortOrder.cs b/MediaBrowser.Model/Entities/SortOrder.cs index e6cb6fd09..f3abc06f3 100644 --- a/MediaBrowser.Model/Entities/SortOrder.cs +++ b/MediaBrowser.Model/Entities/SortOrder.cs @@ -9,7 +9,7 @@ namespace MediaBrowser.Model.Entities /// The ascending. /// </summary> Ascending, - + /// <summary> /// The descending. /// </summary> diff --git a/MediaBrowser.Model/Extensions/StringHelper.cs b/MediaBrowser.Model/Extensions/StringHelper.cs index f97a07096..f819a295c 100644 --- a/MediaBrowser.Model/Extensions/StringHelper.cs +++ b/MediaBrowser.Model/Extensions/StringHelper.cs @@ -22,6 +22,11 @@ namespace MediaBrowser.Model.Extensions return str; } +#if NETSTANDARD2_0 + char[] a = str.ToCharArray(); + a[0] = char.ToUpperInvariant(a[0]); + return new string(a); +#else return string.Create( str.Length, str, @@ -33,6 +38,7 @@ namespace MediaBrowser.Model.Extensions chars[i] = buf[i]; } }); +#endif } } } diff --git a/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs b/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs index e30dd84dc..29f417489 100644 --- a/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs +++ b/MediaBrowser.Model/LiveTv/SeriesTimerInfoDto.cs @@ -14,7 +14,7 @@ namespace MediaBrowser.Model.LiveTv public SeriesTimerInfoDto() { ImageTags = new Dictionary<ImageType, string>(); - Days = new DayOfWeek[] { }; + Days = Array.Empty<DayOfWeek>(); Type = "SeriesTimer"; } diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 657665766..27486c68f 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -1,4 +1,4 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <Authors>Jellyfin Contributors</Authors> @@ -8,17 +8,17 @@ </PropertyGroup> <PropertyGroup> - <TargetFramework>netstandard2.1</TargetFramework> + <TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> - <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' " >true</TreatWarningsAsErrors> + <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' ">true</TreatWarningsAsErrors> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.1" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.3" /> <PackageReference Include="System.Globalization" Version="4.3.0" /> - <PackageReference Include="System.Text.Json" Version="4.7.0" /> + <PackageReference Include="System.Text.Json" Version="4.7.1" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs index 1fd2b7425..06efedb30 100644 --- a/MediaBrowser.Model/Net/MimeTypes.cs +++ b/MediaBrowser.Model/Net/MimeTypes.cs @@ -64,6 +64,7 @@ namespace MediaBrowser.Model.Net { ".m3u8", "application/x-mpegURL" }, { ".mobi", "application/x-mobipocket-ebook" }, { ".xml", "application/xml" }, + { ".wasm", "application/wasm" }, // Type image { ".jpg", "image/jpeg" }, @@ -105,6 +106,7 @@ namespace MediaBrowser.Model.Net { ".3g2", "video/3gpp2" }, { ".mpd", "video/vnd.mpeg.dash.mpd" }, { ".ts", "video/mp2t" }, + { ".mpegts", "video/mp2t" }, // Type audio { ".mp3", "audio/mpeg" }, @@ -122,6 +124,8 @@ namespace MediaBrowser.Model.Net { ".xsp", "audio/xsp" }, { ".dsp", "audio/dsp" }, { ".flac", "audio/flac" }, + { ".ape", "audio/x-ape" }, + { ".wv", "audio/x-wavpack" }, }; private static readonly Dictionary<string, string> _extensionLookup = CreateExtensionLookup(); diff --git a/MediaBrowser.Providers/Chapters/ChapterManager.cs b/MediaBrowser.Providers/Chapters/ChapterManager.cs index 45e87f137..3cbfe7d4d 100644 --- a/MediaBrowser.Providers/Chapters/ChapterManager.cs +++ b/MediaBrowser.Providers/Chapters/ChapterManager.cs @@ -1,36 +1,26 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Controller.Chapters; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; -using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.Chapters { public class ChapterManager : IChapterManager { - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; - private readonly IServerConfigurationManager _config; private readonly IItemRepository _itemRepo; - public ChapterManager( - ILibraryManager libraryManager, - ILoggerFactory loggerFactory, - IServerConfigurationManager config, - IItemRepository itemRepo) + public ChapterManager(IItemRepository itemRepo) { - _libraryManager = libraryManager; - _logger = loggerFactory.CreateLogger(nameof(ChapterManager)); - _config = config; _itemRepo = itemRepo; } - public void SaveChapters(string itemId, List<ChapterInfo> chapters) + /// <inheritdoc /> + public void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters) { - _itemRepo.SaveChapters(new Guid(itemId), chapters); + _itemRepo.SaveChapters(itemId, chapters); } } } diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index 01c950260..6ef0e44a2 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -39,7 +39,7 @@ namespace MediaBrowser.Providers.Manager if (!(item is Photo)) { - var images = providers.OfType<ILocalImageFileProvider>() + var images = providers.OfType<ILocalImageProvider>() .SelectMany(i => i.GetImages(item, directoryService)) .ToList(); diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index e6cb923e3..c49aa407a 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -606,7 +606,6 @@ namespace MediaBrowser.Providers.Manager // Run custom refresh providers if they report a change or any remote providers change return anyRemoteProvidersChanged || providersWithChanges.Contains(i); - }).ToList(); } } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index e7b349f67..7125f34c5 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -1,5 +1,9 @@ +#pragma warning disable CS1591 + using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading; @@ -897,7 +901,10 @@ namespace MediaBrowser.Providers.Manager return new ExternalUrl { Name = i.Name, - Url = string.Format(i.UrlFormatString, value) + Url = string.Format( + CultureInfo.InvariantCulture, + i.UrlFormatString, + value) }; }).Where(i => i != null).Concat(item.GetRelatedUrls()); @@ -911,11 +918,10 @@ namespace MediaBrowser.Providers.Manager Name = i.Name, Key = i.Key, UrlFormatString = i.UrlFormatString - }); } - private Dictionary<Guid, double> _activeRefreshes = new Dictionary<Guid, double>(); + private ConcurrentDictionary<Guid, double> _activeRefreshes = new ConcurrentDictionary<Guid, double>(); public Dictionary<Guid, Guid> GetRefreshQueue() { @@ -927,66 +933,54 @@ namespace MediaBrowser.Providers.Manager { dict[item.Item1] = item.Item1; } + return dict; } } public void OnRefreshStart(BaseItem item) { - //_logger.LogInformation("OnRefreshStart {0}", item.Id.ToString("N", CultureInfo.InvariantCulture)); - var id = item.Id; - - lock (_activeRefreshes) - { - _activeRefreshes[id] = 0; - } - + _logger.LogInformation("OnRefreshStart {0}", item.Id.ToString("N", CultureInfo.InvariantCulture)); + _activeRefreshes[item.Id] = 0; RefreshStarted?.Invoke(this, new GenericEventArgs<BaseItem>(item)); } public void OnRefreshComplete(BaseItem item) { - //_logger.LogInformation("OnRefreshComplete {0}", item.Id.ToString("N", CultureInfo.InvariantCulture)); - lock (_activeRefreshes) - { - _activeRefreshes.Remove(item.Id); - } + _logger.LogInformation("OnRefreshComplete {0}", item.Id.ToString("N", CultureInfo.InvariantCulture)); + + _activeRefreshes.Remove(item.Id, out _); RefreshCompleted?.Invoke(this, new GenericEventArgs<BaseItem>(item)); } public double? GetRefreshProgress(Guid id) { - lock (_activeRefreshes) + if (_activeRefreshes.TryGetValue(id, out double value)) { - if (_activeRefreshes.TryGetValue(id, out double value)) - { - return value; - } - - return null; + return value; } + + return null; } public void OnRefreshProgress(BaseItem item, double progress) { - //_logger.LogInformation("OnRefreshProgress {0} {1}", item.Id.ToString("N", CultureInfo.InvariantCulture), progress); var id = item.Id; - - lock (_activeRefreshes) - { - if (_activeRefreshes.ContainsKey(id)) - { - _activeRefreshes[id] = progress; - - RefreshProgress?.Invoke(this, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(item, progress))); - } - else - { - // TODO: Need to hunt down the conditions for this happening - //throw new Exception(string.Format("Refresh for item {0} {1} is not in progress", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture))); - } - } + _logger.LogInformation("OnRefreshProgress {0} {1}", id.ToString("N", CultureInfo.InvariantCulture), progress); + + // TODO: Need to hunt down the conditions for this happening + _activeRefreshes.AddOrUpdate( + id, + (_) => throw new Exception( + string.Format( + CultureInfo.InvariantCulture, + "Cannot update refresh progress of item '{0}' ({1}) because a refresh for this item is not running", + item.GetType().Name, + item.Id.ToString("N", CultureInfo.InvariantCulture))), + (_, __) => progress); + + RefreshProgress?.Invoke(this, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(item, progress))); } private readonly SimplePriorityQueue<Tuple<Guid, MetadataRefreshOptions>> _refreshQueue = @@ -1040,10 +1034,9 @@ namespace MediaBrowser.Providers.Manager // Try to throttle this a little bit. await Task.Delay(100).ConfigureAwait(false); - var artist = item as MusicArtist; - var task = artist == null - ? RefreshItem(item, refreshItem.Item2, cancellationToken) - : RefreshArtist(artist, refreshItem.Item2, cancellationToken); + var task = item is MusicArtist artist + ? RefreshArtist(artist, refreshItem.Item2, cancellationToken) + : RefreshItem(item, refreshItem.Item2, cancellationToken); await task.ConfigureAwait(false); } @@ -1125,8 +1118,7 @@ namespace MediaBrowser.Providers.Manager } } - public Task RefreshFullItem(BaseItem item, MetadataRefreshOptions options, - CancellationToken cancellationToken) + public Task RefreshFullItem(BaseItem item, MetadataRefreshOptions options, CancellationToken cancellationToken) { return RefreshItem(item, options, cancellationToken); } diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index dfe3eb2ef..330a4d1e5 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -11,8 +11,8 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.1" /> - <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.1" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.3" /> + <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.3" /> <PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" /> <PackageReference Include="PlaylistsNET" Version="1.0.4" /> <PackageReference Include="TvDbSharper" Version="3.0.1" /> @@ -24,6 +24,18 @@ <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> + <!-- Code Analyzers--> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" /> + <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> + </ItemGroup> + + <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + <ItemGroup> <None Remove="Plugins\AudioDb\Configuration\config.html" /> <EmbeddedResource Include="Plugins\AudioDb\Configuration\config.html" /> diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs index db6e49634..6982568eb 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeProvider.cs @@ -46,7 +46,6 @@ namespace MediaBrowser.Providers.MediaInfo private readonly IApplicationPaths _appPaths; private readonly IJsonSerializer _json; private readonly IEncodingManager _encodingManager; - private readonly IFileSystem _fileSystem; private readonly IServerConfigurationManager _config; private readonly ISubtitleManager _subtitleManager; private readonly IChapterManager _chapterManager; @@ -134,7 +133,6 @@ namespace MediaBrowser.Providers.MediaInfo IApplicationPaths appPaths, IJsonSerializer json, IEncodingManager encodingManager, - IFileSystem fileSystem, IServerConfigurationManager config, ISubtitleManager subtitleManager, IChapterManager chapterManager, @@ -149,7 +147,6 @@ namespace MediaBrowser.Providers.MediaInfo _appPaths = appPaths; _json = json; _encodingManager = encodingManager; - _fileSystem = fileSystem; _config = config; _subtitleManager = subtitleManager; _chapterManager = chapterManager; @@ -157,7 +154,7 @@ namespace MediaBrowser.Providers.MediaInfo _channelManager = channelManager; _mediaSourceManager = mediaSourceManager; - _subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager, fileSystem); + _subtitleResolver = new SubtitleResolver(BaseItem.LocalizationManager); } private readonly Task<ItemUpdateType> _cachedTask = Task.FromResult(ItemUpdateType.None); @@ -194,7 +191,18 @@ namespace MediaBrowser.Providers.MediaInfo FetchShortcutInfo(item); } - var prober = new FFProbeVideoInfo(_logger, _mediaSourceManager, _isoManager, _mediaEncoder, _itemRepo, _blurayExaminer, _localization, _appPaths, _json, _encodingManager, _fileSystem, _config, _subtitleManager, _chapterManager, _libraryManager); + var prober = new FFProbeVideoInfo( + _logger, + _mediaSourceManager, + _mediaEncoder, + _itemRepo, + _blurayExaminer, + _localization, + _encodingManager, + _config, + _subtitleManager, + _chapterManager, + _libraryManager); return prober.ProbeVideo(item, options, cancellationToken); } diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 2b178d4d4..89496622f 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -25,7 +27,6 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.MediaInfo @@ -33,33 +34,38 @@ namespace MediaBrowser.Providers.MediaInfo public class FFProbeVideoInfo { private readonly ILogger _logger; - private readonly IIsoManager _isoManager; private readonly IMediaEncoder _mediaEncoder; private readonly IItemRepository _itemRepo; private readonly IBlurayExaminer _blurayExaminer; private readonly ILocalizationManager _localization; - private readonly IApplicationPaths _appPaths; - private readonly IJsonSerializer _json; private readonly IEncodingManager _encodingManager; - private readonly IFileSystem _fileSystem; private readonly IServerConfigurationManager _config; private readonly ISubtitleManager _subtitleManager; private readonly IChapterManager _chapterManager; private readonly ILibraryManager _libraryManager; private readonly IMediaSourceManager _mediaSourceManager; - public FFProbeVideoInfo(ILogger logger, IMediaSourceManager mediaSourceManager, IIsoManager isoManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, IBlurayExaminer blurayExaminer, ILocalizationManager localization, IApplicationPaths appPaths, IJsonSerializer json, IEncodingManager encodingManager, IFileSystem fileSystem, IServerConfigurationManager config, ISubtitleManager subtitleManager, IChapterManager chapterManager, ILibraryManager libraryManager) + private readonly long _dummyChapterDuration = TimeSpan.FromMinutes(5).Ticks; + + public FFProbeVideoInfo( + ILogger logger, + IMediaSourceManager mediaSourceManager, + IMediaEncoder mediaEncoder, + IItemRepository itemRepo, + IBlurayExaminer blurayExaminer, + ILocalizationManager localization, + IEncodingManager encodingManager, + IServerConfigurationManager config, + ISubtitleManager subtitleManager, + IChapterManager chapterManager, + ILibraryManager libraryManager) { _logger = logger; - _isoManager = isoManager; _mediaEncoder = mediaEncoder; _itemRepo = itemRepo; _blurayExaminer = blurayExaminer; _localization = localization; - _appPaths = appPaths; - _json = json; _encodingManager = encodingManager; - _fileSystem = fileSystem; _config = config; _subtitleManager = subtitleManager; _chapterManager = chapterManager; @@ -67,7 +73,8 @@ namespace MediaBrowser.Providers.MediaInfo _mediaSourceManager = mediaSourceManager; } - public async Task<ItemUpdateType> ProbeVideo<T>(T item, + public async Task<ItemUpdateType> ProbeVideo<T>( + T item, MetadataRefreshOptions options, CancellationToken cancellationToken) where T : Video @@ -90,7 +97,6 @@ namespace MediaBrowser.Providers.MediaInfo return ItemUpdateType.MetadataImport; } } - else if (item.VideoType == VideoType.BluRay) { var inputPath = item.Path; @@ -121,7 +127,8 @@ namespace MediaBrowser.Providers.MediaInfo return ItemUpdateType.MetadataImport; } - private Task<Model.MediaInfo.MediaInfo> GetMediaInfo(Video item, + private Task<Model.MediaInfo.MediaInfo> GetMediaInfo( + Video item, string[] streamFileNames, CancellationToken cancellationToken) { @@ -136,22 +143,24 @@ namespace MediaBrowser.Providers.MediaInfo protocol = _mediaSourceManager.GetPathProtocol(path); } - return _mediaEncoder.GetMediaInfo(new MediaInfoRequest - { - PlayableStreamFileNames = streamFileNames, - ExtractChapters = true, - MediaType = DlnaProfileType.Video, - MediaSource = new MediaSourceInfo + return _mediaEncoder.GetMediaInfo( + new MediaInfoRequest { - Path = path, - Protocol = protocol, - VideoType = item.VideoType - } - - }, cancellationToken); + PlayableStreamFileNames = streamFileNames, + ExtractChapters = true, + MediaType = DlnaProfileType.Video, + MediaSource = new MediaSourceInfo + { + Path = path, + Protocol = protocol, + VideoType = item.VideoType + } + }, + cancellationToken); } - protected async Task Fetch(Video video, + protected async Task Fetch( + Video video, CancellationToken cancellationToken, Model.MediaInfo.MediaInfo mediaInfo, BlurayDiscInfo blurayInfo, @@ -159,7 +168,7 @@ namespace MediaBrowser.Providers.MediaInfo { List<MediaStream> mediaStreams; IReadOnlyList<MediaAttachment> mediaAttachments; - List<ChapterInfo> chapters; + ChapterInfo[] chapters; if (mediaInfo != null) { @@ -177,6 +186,7 @@ namespace MediaBrowser.Providers.MediaInfo { video.RunTimeTicks = mediaInfo.RunTimeTicks; } + video.Size = mediaInfo.Size; if (video.VideoType == VideoType.VideoFile) @@ -189,19 +199,20 @@ namespace MediaBrowser.Providers.MediaInfo { video.Container = null; } + video.Container = mediaInfo.Container; - chapters = mediaInfo.Chapters == null ? new List<ChapterInfo>() : mediaInfo.Chapters.ToList(); + chapters = mediaInfo.Chapters == null ? Array.Empty<ChapterInfo>() : mediaInfo.Chapters; if (blurayInfo != null) { - FetchBdInfo(video, chapters, mediaStreams, blurayInfo); + FetchBdInfo(video, ref chapters, mediaStreams, blurayInfo); } } else { mediaStreams = new List<MediaStream>(); mediaAttachments = Array.Empty<MediaAttachment>(); - chapters = new List<ChapterInfo>(); + chapters = Array.Empty<ChapterInfo>(); } await AddExternalSubtitles(video, mediaStreams, options, cancellationToken).ConfigureAwait(false); @@ -231,9 +242,9 @@ namespace MediaBrowser.Providers.MediaInfo if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || options.MetadataRefreshMode == MetadataRefreshMode.Default) { - if (chapters.Count == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video)) + if (chapters.Length == 0 && mediaStreams.Any(i => i.Type == MediaStreamType.Video)) { - AddDummyChapters(video, chapters); + chapters = CreateDummyChapters(video); } NormalizeChapterNames(chapters); @@ -246,28 +257,29 @@ namespace MediaBrowser.Providers.MediaInfo await _encodingManager.RefreshChapterImages(video, options.DirectoryService, chapters, extractDuringScan, false, cancellationToken).ConfigureAwait(false); - _chapterManager.SaveChapters(video.Id.ToString(), chapters); + _chapterManager.SaveChapters(video.Id, chapters); } } - private void NormalizeChapterNames(List<ChapterInfo> chapters) + private void NormalizeChapterNames(ChapterInfo[] chapters) { - var index = 1; - - foreach (var chapter in chapters) + for (int i = 0; i < chapters.Length; i++) { + string name = chapters[i].Name; // Check if the name is empty and/or if the name is a time // Some ripping programs do that. - if (string.IsNullOrWhiteSpace(chapter.Name) || - TimeSpan.TryParse(chapter.Name, out var time)) + if (string.IsNullOrWhiteSpace(name) || + TimeSpan.TryParse(name, out _)) { - chapter.Name = string.Format(_localization.GetLocalizedString("ChapterNameValue"), index.ToString(CultureInfo.InvariantCulture)); + chapters[i].Name = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("ChapterNameValue"), + (i + 1).ToString(CultureInfo.InvariantCulture)); } - index++; } } - private void FetchBdInfo(BaseItem item, List<ChapterInfo> chapters, List<MediaStream> mediaStreams, BlurayDiscInfo blurayInfo) + private void FetchBdInfo(BaseItem item, ref ChapterInfo[] chapters, List<MediaStream> mediaStreams, BlurayDiscInfo blurayInfo) { var video = (Video)item; @@ -301,13 +313,15 @@ namespace MediaBrowser.Providers.MediaInfo if (blurayInfo.Chapters != null) { - chapters.Clear(); - - chapters.AddRange(blurayInfo.Chapters.Select(c => new ChapterInfo + double[] brChapter = blurayInfo.Chapters; + chapters = new ChapterInfo[brChapter.Length]; + for (int i = 0; i < brChapter.Length; i++) { - StartPositionTicks = TimeSpan.FromSeconds(c).Ticks - - })); + chapters[i] = new ChapterInfo + { + StartPositionTicks = TimeSpan.FromSeconds(brChapter[i]).Ticks + }; + } } videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video); @@ -477,12 +491,13 @@ namespace MediaBrowser.Providers.MediaInfo /// <param name="options">The refreshOptions.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task.</returns> - private async Task AddExternalSubtitles(Video video, + private async Task AddExternalSubtitles( + Video video, List<MediaStream> currentStreams, MetadataRefreshOptions options, CancellationToken cancellationToken) { - var subtitleResolver = new SubtitleResolver(_localization, _fileSystem); + var subtitleResolver = new SubtitleResolver(_localization); var startIndex = currentStreams.Count == 0 ? 0 : (currentStreams.Select(i => i.Index).Max() + 1); var externalSubtitleStreams = subtitleResolver.GetExternalSubtitleStreams(video, startIndex, options.DirectoryService, false); @@ -495,17 +510,17 @@ namespace MediaBrowser.Providers.MediaInfo var libraryOptions = _libraryManager.GetLibraryOptions(video); string[] subtitleDownloadLanguages; - bool SkipIfEmbeddedSubtitlesPresent; - bool SkipIfAudioTrackMatches; - bool RequirePerfectMatch; + bool skipIfEmbeddedSubtitlesPresent; + bool skipIfAudioTrackMatches; + bool requirePerfectMatch; bool enabled; if (libraryOptions.SubtitleDownloadLanguages == null) { subtitleDownloadLanguages = subtitleOptions.DownloadLanguages; - SkipIfEmbeddedSubtitlesPresent = subtitleOptions.SkipIfEmbeddedSubtitlesPresent; - SkipIfAudioTrackMatches = subtitleOptions.SkipIfAudioTrackMatches; - RequirePerfectMatch = subtitleOptions.RequirePerfectMatch; + skipIfEmbeddedSubtitlesPresent = subtitleOptions.SkipIfEmbeddedSubtitlesPresent; + skipIfAudioTrackMatches = subtitleOptions.SkipIfAudioTrackMatches; + requirePerfectMatch = subtitleOptions.RequirePerfectMatch; enabled = (subtitleOptions.DownloadEpisodeSubtitles && video is Episode) || (subtitleOptions.DownloadMovieSubtitles && @@ -514,9 +529,9 @@ namespace MediaBrowser.Providers.MediaInfo else { subtitleDownloadLanguages = libraryOptions.SubtitleDownloadLanguages; - SkipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent; - SkipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches; - RequirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch; + skipIfEmbeddedSubtitlesPresent = libraryOptions.SkipSubtitlesIfEmbeddedSubtitlesPresent; + skipIfAudioTrackMatches = libraryOptions.SkipSubtitlesIfAudioTrackMatches; + requirePerfectMatch = libraryOptions.RequirePerfectSubtitleMatch; enabled = true; } @@ -526,9 +541,9 @@ namespace MediaBrowser.Providers.MediaInfo _subtitleManager) .DownloadSubtitles(video, currentStreams.Concat(externalSubtitleStreams).ToList(), - SkipIfEmbeddedSubtitlesPresent, - SkipIfAudioTrackMatches, - RequirePerfectMatch, + skipIfEmbeddedSubtitlesPresent, + skipIfAudioTrackMatches, + requirePerfectMatch, subtitleDownloadLanguages, libraryOptions.DisabledSubtitleFetchers, libraryOptions.SubtitleFetcherOrder, @@ -547,49 +562,51 @@ namespace MediaBrowser.Providers.MediaInfo } /// <summary> - /// The dummy chapter duration - /// </summary> - private readonly long _dummyChapterDuration = TimeSpan.FromMinutes(5).Ticks; - - /// <summary> - /// Adds the dummy chapters. + /// Creates dummy chapters. /// </summary> /// <param name="video">The video.</param> - /// <param name="chapters">The chapters.</param> - private void AddDummyChapters(Video video, List<ChapterInfo> chapters) + /// <return>An array of dummy chapters.</returns> + private ChapterInfo[] CreateDummyChapters(Video video) { var runtime = video.RunTimeTicks ?? 0; if (runtime < 0) { - throw new ArgumentException(string.Format("{0} has invalid runtime of {1}", video.Name, runtime)); + throw new ArgumentException( + string.Format( + CultureInfo.InvariantCulture, + "{0} has invalid runtime of {1}", + video.Name, + runtime)); } if (runtime < _dummyChapterDuration) { - return; + return Array.Empty<ChapterInfo>(); } - long currentChapterTicks = 0; - var index = 1; - // Limit to 100 chapters just in case there's some incorrect metadata here - while (currentChapterTicks < runtime && index < 100) + int chapterCount = (int)Math.Min(runtime / _dummyChapterDuration, 100); + var chapters = new ChapterInfo[chapterCount]; + + long currentChapterTicks = 0; + for (int i = 0; i < chapterCount; i++) { - chapters.Add(new ChapterInfo + chapters[i] = new ChapterInfo { StartPositionTicks = currentChapterTicks - }); + }; - index++; currentChapterTicks += _dummyChapterDuration; } + + return chapters; } private string[] FetchFromDvdLib(Video item) { var path = item.Path; - var dvd = new Dvd(path, _fileSystem); + var dvd = new Dvd(path); var primaryTitle = dvd.Titles.OrderByDescending(GetRuntime).FirstOrDefault(); diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs index 7ebbb9e23..2bbe8a968 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleResolver.cs @@ -13,7 +13,6 @@ namespace MediaBrowser.Providers.MediaInfo public class SubtitleResolver { private readonly ILocalizationManager _localization; - private readonly IFileSystem _fileSystem; private static readonly HashSet<string> SubtitleExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { @@ -26,16 +25,16 @@ namespace MediaBrowser.Providers.MediaInfo ".vtt" }; - public SubtitleResolver(ILocalizationManager localization, IFileSystem fileSystem) + public SubtitleResolver(ILocalizationManager localization) { _localization = localization; - _fileSystem = fileSystem; } - public List<MediaStream> GetExternalSubtitleStreams(Video video, - int startIndex, - IDirectoryService directoryService, - bool clearCache) + public List<MediaStream> GetExternalSubtitleStreams( + Video video, + int startIndex, + IDirectoryService directoryService, + bool clearCache) { var streams = new List<MediaStream>(); diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs index 3a936632a..2615f2dbb 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs @@ -14,6 +14,7 @@ using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; +using MediaBrowser.Model.Globalization; namespace MediaBrowser.Providers.MediaInfo { @@ -25,6 +26,7 @@ namespace MediaBrowser.Providers.MediaInfo private readonly IMediaSourceManager _mediaSourceManager; private readonly ILogger _logger; private readonly IJsonSerializer _json; + private readonly ILocalizationManager _localization; public SubtitleScheduledTask( ILibraryManager libraryManager, @@ -32,7 +34,8 @@ namespace MediaBrowser.Providers.MediaInfo IServerConfigurationManager config, ISubtitleManager subtitleManager, ILogger<SubtitleScheduledTask> logger, - IMediaSourceManager mediaSourceManager) + IMediaSourceManager mediaSourceManager, + ILocalizationManager localization) { _libraryManager = libraryManager; _config = config; @@ -40,6 +43,7 @@ namespace MediaBrowser.Providers.MediaInfo _logger = logger; _mediaSourceManager = mediaSourceManager; _json = json; + _localization = localization; } private SubtitleOptions GetOptions() @@ -204,11 +208,11 @@ namespace MediaBrowser.Providers.MediaInfo }; } - public string Name => "Download missing subtitles"; + public string Name => _localization.GetLocalizedString("TaskDownloadMissingSubtitles"); - public string Description => "Searches the internet for missing subtitles based on metadata configuration."; + public string Description => _localization.GetLocalizedString("TaskDownloadMissingSubtitlesDescription"); - public string Category => "Library"; + public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); public string Key => "DownloadSubtitles"; diff --git a/MediaBrowser.Providers/Music/ArtistMetadataService.cs b/MediaBrowser.Providers/Music/ArtistMetadataService.cs index f90a631c6..5a30260a5 100644 --- a/MediaBrowser.Providers/Music/ArtistMetadataService.cs +++ b/MediaBrowser.Providers/Music/ArtistMetadataService.cs @@ -31,10 +31,10 @@ namespace MediaBrowser.Providers.Music { return item.IsAccessedByName ? item.GetTaggedItems(new InternalItemsQuery - { - Recursive = true, - IsFolder = false - }) + { + Recursive = true, + IsFolder = false + }) : item.GetRecursiveChildren(i => i is IHasArtist && !i.IsFolder); } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs index bc973dee5..31cdaf616 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/AlbumProvider.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net; +using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -775,7 +776,7 @@ namespace MediaBrowser.Providers.Music _logger.LogDebug("GetMusicBrainzResponse: Time since previous request: {0} ms", _stopWatchMusicBrainz.ElapsedMilliseconds); _stopWatchMusicBrainz.Restart(); - response = await _httpClient.SendAsync(options, "GET").ConfigureAwait(false); + response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false); // We retry a finite number of times, and only whilst MB is indicating 503 (throttling) } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs index 6910b4bb4..5843b0c7d 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Configuration/PluginConfiguration.cs @@ -32,7 +32,11 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz { if (value < Plugin.DefaultRateLimit && _server == Plugin.DefaultServer) { - RateLimit = Plugin.DefaultRateLimit; + _rateLimit = Plugin.DefaultRateLimit; + } + else + { + _rateLimit = value; } } } diff --git a/MediaBrowser.Providers/TV/Omdb/OmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs index dee3030af..37160dd2c 100644 --- a/MediaBrowser.Providers/TV/Omdb/OmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbEpisodeProvider.cs @@ -11,10 +11,9 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Omdb; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.TV.Omdb +namespace MediaBrowser.Providers.Plugins.Omdb { public class OmdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, diff --git a/MediaBrowser.Providers/Omdb/OmdbImageProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs index 1e0bcacf2..a450c2a6d 100644 --- a/MediaBrowser.Providers/Omdb/OmdbImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbImageProvider.cs @@ -13,7 +13,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; -namespace MediaBrowser.Providers.Omdb +namespace MediaBrowser.Providers.Plugins.Omdb { public class OmdbImageProvider : IRemoteImageProvider, IHasOrder { diff --git a/MediaBrowser.Providers/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs index 44b9dcca1..3aadda5d0 100644 --- a/MediaBrowser.Providers/Omdb/OmdbItemProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs @@ -19,7 +19,7 @@ using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.Omdb +namespace MediaBrowser.Providers.Plugins.Omdb { public class OmdbItemProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IRemoteMetadataProvider<Movie, MovieInfo>, IRemoteMetadataProvider<Trailer, TrailerInfo>, IHasOrder diff --git a/MediaBrowser.Providers/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index fbf6ae135..fbdd293ed 100644 --- a/MediaBrowser.Providers/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -16,7 +16,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; -namespace MediaBrowser.Providers.Omdb +namespace MediaBrowser.Providers.Plugins.Omdb { public class OmdbProvider { diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvDbClientManager.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs index 4abe6a943..b73834155 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvDbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbClientManager.cs @@ -10,9 +10,9 @@ using Microsoft.Extensions.Caching.Memory; using TvDbSharper; using TvDbSharper.Dto; -namespace MediaBrowser.Providers.TV.TheTVDB +namespace MediaBrowser.Providers.Plugins.TheTvdb { - public class TvDbClientManager + public class TvdbClientManager { private const string DefaultLanguage = "en"; @@ -21,7 +21,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB private readonly TvDbClient _tvDbClient; private DateTime _tokenCreatedAt; - public TvDbClientManager(IMemoryCache memoryCache) + public TvdbClientManager(IMemoryCache memoryCache) { _cache = memoryCache; _tvDbClient = new TvDbClient(); @@ -60,21 +60,21 @@ namespace MediaBrowser.Providers.TV.TheTVDB CancellationToken cancellationToken) { var cacheKey = GenerateKey("series", name, language); - return TryGetValue(cacheKey, language,() => TvDbClient.Search.SearchSeriesByNameAsync(name, cancellationToken)); + return TryGetValue(cacheKey, language, () => TvDbClient.Search.SearchSeriesByNameAsync(name, cancellationToken)); } public Task<TvDbResponse<Series>> GetSeriesByIdAsync(int tvdbId, string language, CancellationToken cancellationToken) { var cacheKey = GenerateKey("series", tvdbId, language); - return TryGetValue(cacheKey, language,() => TvDbClient.Series.GetAsync(tvdbId, cancellationToken)); + return TryGetValue(cacheKey, language, () => TvDbClient.Series.GetAsync(tvdbId, cancellationToken)); } public Task<TvDbResponse<EpisodeRecord>> GetEpisodesAsync(int episodeTvdbId, string language, CancellationToken cancellationToken) { var cacheKey = GenerateKey("episode", episodeTvdbId, language); - return TryGetValue(cacheKey, language,() => TvDbClient.Episodes.GetAsync(episodeTvdbId, cancellationToken)); + return TryGetValue(cacheKey, language, () => TvDbClient.Episodes.GetAsync(episodeTvdbId, cancellationToken)); } public async Task<List<EpisodeRecord>> GetAllEpisodesAsync(int tvdbId, string language, @@ -109,7 +109,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB CancellationToken cancellationToken) { var cacheKey = GenerateKey("series", imdbId, language); - return TryGetValue(cacheKey, language,() => TvDbClient.Search.SearchSeriesByImdbIdAsync(imdbId, cancellationToken)); + return TryGetValue(cacheKey, language, () => TvDbClient.Search.SearchSeriesByImdbIdAsync(imdbId, cancellationToken)); } public Task<TvDbResponse<SeriesSearchResult[]>> GetSeriesByZap2ItIdAsync( diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs index fc7f12b1a..6118a9c53 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeImageProvider.cs @@ -12,19 +12,19 @@ using Microsoft.Extensions.Logging; using TvDbSharper; using TvDbSharper.Dto; -namespace MediaBrowser.Providers.TV.TheTVDB +namespace MediaBrowser.Providers.Plugins.TheTvdb { public class TvdbEpisodeImageProvider : IRemoteImageProvider { private readonly IHttpClient _httpClient; private readonly ILogger _logger; - private readonly TvDbClientManager _tvDbClientManager; + private readonly TvdbClientManager _tvdbClientManager; - public TvdbEpisodeImageProvider(IHttpClient httpClient, ILogger<TvdbEpisodeImageProvider> logger, TvDbClientManager tvDbClientManager) + public TvdbEpisodeImageProvider(IHttpClient httpClient, ILogger<TvdbEpisodeImageProvider> logger, TvdbClientManager tvdbClientManager) { _httpClient = httpClient; _logger = logger; - _tvDbClientManager = tvDbClientManager; + _tvdbClientManager = tvdbClientManager; } public string Name => "TheTVDB"; @@ -60,7 +60,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB SeriesProviderIds = series.ProviderIds, SeriesDisplayOrder = series.DisplayOrder }; - string episodeTvdbId = await _tvDbClientManager + string episodeTvdbId = await _tvdbClientManager .GetEpisodeTvdbId(episodeInfo, language, cancellationToken).ConfigureAwait(false); if (string.IsNullOrEmpty(episodeTvdbId)) { @@ -73,7 +73,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB } var episodeResult = - await _tvDbClientManager + await _tvdbClientManager .GetEpisodesAsync(Convert.ToInt32(episodeTvdbId), language, cancellationToken) .ConfigureAwait(false); diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs index 4269d3420..08c2a74d2 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs @@ -12,7 +12,7 @@ using Microsoft.Extensions.Logging; using TvDbSharper; using TvDbSharper.Dto; -namespace MediaBrowser.Providers.TV.TheTVDB +namespace MediaBrowser.Providers.Plugins.TheTvdb { /// <summary> @@ -22,13 +22,13 @@ namespace MediaBrowser.Providers.TV.TheTVDB { private readonly IHttpClient _httpClient; private readonly ILogger _logger; - private readonly TvDbClientManager _tvDbClientManager; + private readonly TvdbClientManager _tvdbClientManager; - public TvdbEpisodeProvider(IHttpClient httpClient, ILogger<TvdbEpisodeProvider> logger, TvDbClientManager tvDbClientManager) + public TvdbEpisodeProvider(IHttpClient httpClient, ILogger<TvdbEpisodeProvider> logger, TvdbClientManager tvdbClientManager) { _httpClient = httpClient; _logger = logger; - _tvDbClientManager = tvDbClientManager; + _tvdbClientManager = tvdbClientManager; } public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken) @@ -99,7 +99,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB string episodeTvdbId = null; try { - episodeTvdbId = await _tvDbClientManager + episodeTvdbId = await _tvdbClientManager .GetEpisodeTvdbId(searchInfo, searchInfo.MetadataLanguage, cancellationToken) .ConfigureAwait(false); if (string.IsNullOrEmpty(episodeTvdbId)) @@ -109,7 +109,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB return result; } - var episodeResult = await _tvDbClientManager.GetEpisodesAsync( + var episodeResult = await _tvdbClientManager.GetEpisodesAsync( Convert.ToInt32(episodeTvdbId), searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); @@ -201,7 +201,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB continue; } - var roles = new List<string> {currentActor.Substring(roleStartIndex + 1)}; + var roles = new List<string> { currentActor.Substring(roleStartIndex + 1) }; // Fetch all roles for (var j = i + 1; j < episode.GuestStars.Length; ++j) diff --git a/MediaBrowser.Providers/People/TvdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs index 50476044b..c1cdc90e9 100644 --- a/MediaBrowser.Providers/People/TvdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbPersonImageProvider.cs @@ -11,25 +11,24 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -using MediaBrowser.Providers.TV.TheTVDB; using Microsoft.Extensions.Logging; using TvDbSharper; -namespace MediaBrowser.Providers.People +namespace MediaBrowser.Providers.Plugins.TheTvdb { public class TvdbPersonImageProvider : IRemoteImageProvider, IHasOrder { private readonly IHttpClient _httpClient; private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; - private readonly TvDbClientManager _tvDbClientManager; + private readonly TvdbClientManager _tvdbClientManager; - public TvdbPersonImageProvider(ILibraryManager libraryManager, IHttpClient httpClient, ILogger<TvdbPersonImageProvider> logger, TvDbClientManager tvDbClientManager) + public TvdbPersonImageProvider(ILibraryManager libraryManager, IHttpClient httpClient, ILogger<TvdbPersonImageProvider> logger, TvdbClientManager tvdbClientManager) { _libraryManager = libraryManager; _httpClient = httpClient; _logger = logger; - _tvDbClientManager = tvDbClientManager; + _tvdbClientManager = tvdbClientManager; } /// <inheritdoc /> @@ -78,7 +77,7 @@ namespace MediaBrowser.Providers.People try { - var actorsResult = await _tvDbClientManager + var actorsResult = await _tvdbClientManager .GetActorsAsync(tvdbId, series.GetPreferredMetadataLanguage(), cancellationToken) .ConfigureAwait(false); var actor = actorsResult.Data.FirstOrDefault(a => diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeasonImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs index 94ca603f2..a5d183df7 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeasonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeasonImageProvider.cs @@ -14,19 +14,19 @@ using TvDbSharper; using TvDbSharper.Dto; using RatingType = MediaBrowser.Model.Dto.RatingType; -namespace MediaBrowser.Providers.TV.TheTVDB +namespace MediaBrowser.Providers.Plugins.TheTvdb { public class TvdbSeasonImageProvider : IRemoteImageProvider, IHasOrder { private readonly IHttpClient _httpClient; private readonly ILogger _logger; - private readonly TvDbClientManager _tvDbClientManager; + private readonly TvdbClientManager _tvdbClientManager; - public TvdbSeasonImageProvider(IHttpClient httpClient, ILogger<TvdbSeasonImageProvider> logger, TvDbClientManager tvDbClientManager) + public TvdbSeasonImageProvider(IHttpClient httpClient, ILogger<TvdbSeasonImageProvider> logger, TvdbClientManager tvdbClientManager) { _httpClient = httpClient; _logger = logger; - _tvDbClientManager = tvDbClientManager; + _tvdbClientManager = tvdbClientManager; } public string Name => ProviderName; @@ -73,7 +73,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB }; try { - var imageResults = await _tvDbClientManager + var imageResults = await _tvdbClientManager .GetImagesAsync(tvdbId, imageQuery, language, cancellationToken).ConfigureAwait(false); remoteImages.AddRange(GetImages(imageResults.Data, language)); } @@ -89,7 +89,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB private IEnumerable<RemoteImageInfo> GetImages(Image[] images, string preferredLanguage) { var list = new List<RemoteImageInfo>(); - var languages = _tvDbClientManager.GetLanguagesAsync(CancellationToken.None).Result.Data; + var languages = _tvdbClientManager.GetLanguagesAsync(CancellationToken.None).Result.Data; foreach (Image image in images) { var imageInfo = new RemoteImageInfo diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs index 365f49fb7..1bad60756 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesImageProvider.cs @@ -14,19 +14,19 @@ using TvDbSharper.Dto; using RatingType = MediaBrowser.Model.Dto.RatingType; using Series = MediaBrowser.Controller.Entities.TV.Series; -namespace MediaBrowser.Providers.TV.TheTVDB +namespace MediaBrowser.Providers.Plugins.TheTvdb { public class TvdbSeriesImageProvider : IRemoteImageProvider, IHasOrder { private readonly IHttpClient _httpClient; private readonly ILogger _logger; - private readonly TvDbClientManager _tvDbClientManager; + private readonly TvdbClientManager _tvdbClientManager; - public TvdbSeriesImageProvider(IHttpClient httpClient, ILogger<TvdbSeriesImageProvider> logger, TvDbClientManager tvDbClientManager) + public TvdbSeriesImageProvider(IHttpClient httpClient, ILogger<TvdbSeriesImageProvider> logger, TvdbClientManager tvdbClientManager) { _httpClient = httpClient; _logger = logger; - _tvDbClientManager = tvDbClientManager; + _tvdbClientManager = tvdbClientManager; } public string Name => ProviderName; @@ -68,7 +68,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB try { var imageResults = - await _tvDbClientManager.GetImagesAsync(tvdbId, imageQuery, language, cancellationToken) + await _tvdbClientManager.GetImagesAsync(tvdbId, imageQuery, language, cancellationToken) .ConfigureAwait(false); remoteImages.AddRange(GetImages(imageResults.Data, language)); @@ -85,7 +85,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB private IEnumerable<RemoteImageInfo> GetImages(Image[] images, string preferredLanguage) { var list = new List<RemoteImageInfo>(); - var languages = _tvDbClientManager.GetLanguagesAsync(CancellationToken.None).Result.Data; + var languages = _tvdbClientManager.GetLanguagesAsync(CancellationToken.None).Result.Data; foreach (Image image in images) { diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs index 9e791bd9d..f6cd249f5 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbSeriesProvider.cs @@ -17,7 +17,7 @@ using TvDbSharper; using TvDbSharper.Dto; using Series = MediaBrowser.Controller.Entities.TV.Series; -namespace MediaBrowser.Providers.TV.TheTVDB +namespace MediaBrowser.Providers.Plugins.TheTvdb { public class TvdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder { @@ -26,16 +26,16 @@ namespace MediaBrowser.Providers.TV.TheTVDB private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localizationManager; - private readonly TvDbClientManager _tvDbClientManager; + private readonly TvdbClientManager _tvdbClientManager; - public TvdbSeriesProvider(IHttpClient httpClient, ILogger<TvdbSeriesProvider> logger, ILibraryManager libraryManager, ILocalizationManager localizationManager, TvDbClientManager tvDbClientManager) + public TvdbSeriesProvider(IHttpClient httpClient, ILogger<TvdbSeriesProvider> logger, ILibraryManager libraryManager, ILocalizationManager localizationManager, TvdbClientManager tvdbClientManager) { _httpClient = httpClient; _logger = logger; _libraryManager = libraryManager; _localizationManager = localizationManager; Current = this; - _tvDbClientManager = tvDbClientManager; + _tvdbClientManager = tvdbClientManager; } public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken) @@ -116,7 +116,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB try { var seriesResult = - await _tvDbClientManager + await _tvdbClientManager .GetSeriesByIdAsync(Convert.ToInt32(tvdbId), metadataLanguage, cancellationToken) .ConfigureAwait(false); MapSeriesToResult(result, seriesResult.Data, metadataLanguage); @@ -133,7 +133,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB try { - var actorsResult = await _tvDbClientManager + var actorsResult = await _tvdbClientManager .GetActorsAsync(Convert.ToInt32(tvdbId), metadataLanguage, cancellationToken).ConfigureAwait(false); MapActorsToResult(result, actorsResult.Data); } @@ -152,12 +152,12 @@ namespace MediaBrowser.Providers.TV.TheTVDB { if (string.Equals(idType, MetadataProviders.Zap2It.ToString(), StringComparison.OrdinalIgnoreCase)) { - result = await _tvDbClientManager.GetSeriesByZap2ItIdAsync(id, language, cancellationToken) + result = await _tvdbClientManager.GetSeriesByZap2ItIdAsync(id, language, cancellationToken) .ConfigureAwait(false); } else { - result = await _tvDbClientManager.GetSeriesByImdbIdAsync(id, language, cancellationToken) + result = await _tvdbClientManager.GetSeriesByImdbIdAsync(id, language, cancellationToken) .ConfigureAwait(false); } } @@ -223,7 +223,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB TvDbResponse<SeriesSearchResult[]> result; try { - result = await _tvDbClientManager.GetSeriesByNameAsync(comparableName, language, cancellationToken) + result = await _tvdbClientManager.GetSeriesByNameAsync(comparableName, language, cancellationToken) .ConfigureAwait(false); } catch (TvDbServerException e) @@ -252,7 +252,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB try { var seriesSesult = - await _tvDbClientManager.GetSeriesByIdAsync(seriesSearchResult.Id, language, cancellationToken) + await _tvdbClientManager.GetSeriesByIdAsync(seriesSearchResult.Id, language, cancellationToken) .ConfigureAwait(false); remoteSearchResult.SetProviderId(MetadataProviders.Imdb, seriesSesult.Data.ImdbId); remoteSearchResult.SetProviderId(MetadataProviders.Zap2It, seriesSesult.Data.Zap2itId); @@ -344,7 +344,11 @@ namespace MediaBrowser.Providers.TV.TheTVDB series.ProductionYear = date.Year; } - series.RunTimeTicks = TimeSpan.FromMinutes(Convert.ToDouble(tvdbSeries.Runtime)).Ticks; + if (!string.IsNullOrEmpty(tvdbSeries.Runtime) && double.TryParse(tvdbSeries.Runtime, out double runtime)) + { + series.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks; + } + foreach (var genre in tvdbSeries.Genre) { series.AddGenre(genre); @@ -359,7 +363,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB { try { - var episodeSummary = _tvDbClientManager + var episodeSummary = _tvdbClientManager .GetSeriesEpisodeSummaryAsync(tvdbSeries.Id, metadataLanguage, CancellationToken.None).Result.Data; var maxSeasonNumber = episodeSummary.AiredSeasons.Select(s => Convert.ToInt32(s)).Max(); var episodeQuery = new EpisodeQuery @@ -367,7 +371,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB AiredSeason = maxSeasonNumber }; var episodesPage = - _tvDbClientManager.GetEpisodesPageAsync(tvdbSeries.Id, episodeQuery, metadataLanguage, CancellationToken.None).Result.Data; + _tvdbClientManager.GetEpisodesPageAsync(tvdbSeries.Id, episodeQuery, metadataLanguage, CancellationToken.None).Result.Data; result.Item.EndDate = episodesPage.Select(e => { DateTime.TryParse(e.FirstAired, out var firstAired); diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbUtils.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs index dd5ebf270..79d879aa1 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbUtils.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbUtils.cs @@ -1,6 +1,7 @@ using System; using MediaBrowser.Model.Entities; -namespace MediaBrowser.Providers.TV.TheTVDB + +namespace MediaBrowser.Providers.Plugins.TheTvdb { public static class TvdbUtils { diff --git a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs b/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs index e72df50de..0721c4bb4 100644 --- a/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs +++ b/MediaBrowser.Providers/TV/MissingEpisodeProvider.cs @@ -12,7 +12,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; -using MediaBrowser.Providers.TV.TheTVDB; +using MediaBrowser.Providers.Plugins.TheTvdb; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.TV @@ -26,7 +26,7 @@ namespace MediaBrowser.Providers.TV private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; private readonly IFileSystem _fileSystem; - private readonly TvDbClientManager _tvDbClientManager; + private readonly TvdbClientManager _tvdbClientManager; public MissingEpisodeProvider( ILogger logger, @@ -34,14 +34,14 @@ namespace MediaBrowser.Providers.TV ILibraryManager libraryManager, ILocalizationManager localization, IFileSystem fileSystem, - TvDbClientManager tvDbClientManager) + TvdbClientManager tvdbClientManager) { _logger = logger; _config = config; _libraryManager = libraryManager; _localization = localization; _fileSystem = fileSystem; - _tvDbClientManager = tvDbClientManager; + _tvdbClientManager = tvdbClientManager; } public async Task<bool> Run(Series series, bool addNewItems, CancellationToken cancellationToken) @@ -52,7 +52,7 @@ namespace MediaBrowser.Providers.TV return false; } - var episodes = await _tvDbClientManager.GetAllEpisodesAsync(Convert.ToInt32(tvdbId), series.GetPreferredMetadataLanguage(), cancellationToken); + var episodes = await _tvdbClientManager.GetAllEpisodesAsync(Convert.ToInt32(tvdbId), series.GetPreferredMetadataLanguage(), cancellationToken); var episodeLookup = episodes .Select(i => diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index 7bc5a87d8..5e75a8125 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -9,7 +9,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Providers.Manager; -using MediaBrowser.Providers.TV.TheTVDB; +using MediaBrowser.Providers.Plugins.TheTvdb; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.TV @@ -17,7 +17,7 @@ namespace MediaBrowser.Providers.TV public class SeriesMetadataService : MetadataService<Series, SeriesInfo> { private readonly ILocalizationManager _localization; - private readonly TvDbClientManager _tvDbClientManager; + private readonly TvdbClientManager _tvdbClientManager; public SeriesMetadataService( IServerConfigurationManager serverConfigurationManager, @@ -26,11 +26,11 @@ namespace MediaBrowser.Providers.TV IFileSystem fileSystem, ILibraryManager libraryManager, ILocalizationManager localization, - TvDbClientManager tvDbClientManager) + TvdbClientManager tvdbClientManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager) { _localization = localization; - _tvDbClientManager = tvDbClientManager; + _tvdbClientManager = tvdbClientManager; } /// <inheritdoc /> @@ -47,7 +47,7 @@ namespace MediaBrowser.Providers.TV LibraryManager, _localization, FileSystem, - _tvDbClientManager); + _tvdbClientManager); try { diff --git a/MediaBrowser.Providers/TV/TvExternalIds.cs b/MediaBrowser.Providers/TV/TvExternalIds.cs index 646dae3e0..baf854285 100644 --- a/MediaBrowser.Providers/TV/TvExternalIds.cs +++ b/MediaBrowser.Providers/TV/TvExternalIds.cs @@ -1,7 +1,7 @@ using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Providers.TV.TheTVDB; +using MediaBrowser.Providers.Plugins.TheTvdb; namespace MediaBrowser.Providers.TV { diff --git a/MediaBrowser.Providers/Tmdb/Models/General/Profile.cs b/MediaBrowser.Providers/Tmdb/Models/General/Profile.cs index 73a049c73..f87d14850 100644 --- a/MediaBrowser.Providers/Tmdb/Models/General/Profile.cs +++ b/MediaBrowser.Providers/Tmdb/Models/General/Profile.cs @@ -2,10 +2,10 @@ namespace MediaBrowser.Providers.Tmdb.Models.General { public class Profile { - public string File_Path { get; set; } - public int Width { get; set; } - public int Height { get; set; } - public object Iso_639_1 { get; set; } - public double Aspect_Ratio { get; set; } + public string File_Path { get; set; } + public int Width { get; set; } + public int Height { get; set; } + public object Iso_639_1 { get; set; } + public double Aspect_Ratio { get; set; } } } diff --git a/MediaBrowser.Providers/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Tmdb/Movies/TmdbMovieProvider.cs index fbb87d25d..e2fd5b9e3 100644 --- a/MediaBrowser.Providers/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Tmdb/Movies/TmdbMovieProvider.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Net; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common; @@ -36,7 +37,6 @@ namespace MediaBrowser.Providers.Tmdb.Movies private readonly IFileSystem _fileSystem; private readonly IServerConfigurationManager _configurationManager; private readonly ILogger _logger; - private readonly ILocalizationManager _localization; private readonly ILibraryManager _libraryManager; private readonly IApplicationHost _appHost; @@ -48,7 +48,6 @@ namespace MediaBrowser.Providers.Tmdb.Movies IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILogger<TmdbMovieProvider> logger, - ILocalizationManager localization, ILibraryManager libraryManager, IApplicationHost appHost) { @@ -57,7 +56,6 @@ namespace MediaBrowser.Providers.Tmdb.Movies _fileSystem = fileSystem; _configurationManager = configurationManager; _logger = logger; - _localization = localization; _libraryManager = libraryManager; _appHost = appHost; Current = this; @@ -409,15 +407,15 @@ namespace MediaBrowser.Providers.Tmdb.Movies private static long _lastRequestTicks; // The limit is 40 requests per 10 seconds - private static int requestIntervalMs = 300; + private const int RequestIntervalMs = 300; /// <summary> /// Gets the movie db response. /// </summary> internal async Task<HttpResponseInfo> GetMovieDbResponse(HttpRequestOptions options) { - var delayTicks = (requestIntervalMs * 10000) - (DateTime.UtcNow.Ticks - _lastRequestTicks); - var delayMs = Math.Min(delayTicks / 10000, requestIntervalMs); + var delayTicks = (RequestIntervalMs * 10000) - (DateTime.UtcNow.Ticks - _lastRequestTicks); + var delayMs = Math.Min(delayTicks / 10000, RequestIntervalMs); if (delayMs > 0) { @@ -430,11 +428,13 @@ namespace MediaBrowser.Providers.Tmdb.Movies options.BufferContent = true; options.UserAgent = _appHost.ApplicationUserAgent; - return await _httpClient.SendAsync(options, "GET").ConfigureAwait(false); + return await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false); } + /// <inheritdoc /> public int Order => 1; + /// <inheritdoc /> public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClient.GetResponse(new HttpRequestOptions diff --git a/MediaBrowser.WebDashboard/Api/DashboardService.cs b/MediaBrowser.WebDashboard/Api/DashboardService.cs index 99d8d044f..133a35527 100644 --- a/MediaBrowser.WebDashboard/Api/DashboardService.cs +++ b/MediaBrowser.WebDashboard/Api/DashboardService.cs @@ -12,12 +12,14 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Plugins; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Services; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace MediaBrowser.WebDashboard.Api @@ -102,6 +104,7 @@ namespace MediaBrowser.WebDashboard.Api /// <value>The HTTP result factory.</value> private readonly IHttpResultFactory _resultFactory; private readonly IServerApplicationHost _appHost; + private readonly IConfiguration _appConfig; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IFileSystem _fileSystem; private readonly IResourceFileManager _resourceFileManager; @@ -111,6 +114,7 @@ namespace MediaBrowser.WebDashboard.Api /// </summary> /// <param name="logger">The logger.</param> /// <param name="appHost">The application host.</param> + /// <param name="appConfig">The application configuration.</param> /// <param name="resourceFileManager">The resource file manager.</param> /// <param name="serverConfigurationManager">The server configuration manager.</param> /// <param name="fileSystem">The file system.</param> @@ -118,6 +122,7 @@ namespace MediaBrowser.WebDashboard.Api public DashboardService( ILogger<DashboardService> logger, IServerApplicationHost appHost, + IConfiguration appConfig, IResourceFileManager resourceFileManager, IServerConfigurationManager serverConfigurationManager, IFileSystem fileSystem, @@ -125,6 +130,7 @@ namespace MediaBrowser.WebDashboard.Api { _logger = logger; _appHost = appHost; + _appConfig = appConfig; _resourceFileManager = resourceFileManager; _serverConfigurationManager = serverConfigurationManager; _fileSystem = fileSystem; @@ -138,20 +144,30 @@ namespace MediaBrowser.WebDashboard.Api public IRequest Request { get; set; } /// <summary> - /// Gets the path for the web interface. + /// Gets the path of the directory containing the static web interface content, or null if the server is not + /// hosting the web client. /// </summary> - /// <value>The path for the web interface.</value> - public string DashboardUIPath + public string DashboardUIPath => GetDashboardUIPath(_appConfig, _serverConfigurationManager); + + /// <summary> + /// Gets the path of the directory containing the static web interface content. + /// </summary> + /// <param name="appConfig">The app configuration.</param> + /// <param name="serverConfigManager">The server configuration manager.</param> + /// <returns>The directory path, or null if the server is not hosting the web client.</returns> + public static string GetDashboardUIPath(IConfiguration appConfig, IServerConfigurationManager serverConfigManager) { - get + if (!appConfig.HostWebClient()) { - if (!string.IsNullOrEmpty(_serverConfigurationManager.Configuration.DashboardSourcePath)) - { - return _serverConfigurationManager.Configuration.DashboardSourcePath; - } + return null; + } - return _serverConfigurationManager.ApplicationPaths.WebPath; + if (!string.IsNullOrEmpty(serverConfigManager.Configuration.DashboardSourcePath)) + { + return serverConfigManager.Configuration.DashboardSourcePath; } + + return serverConfigManager.ApplicationPaths.WebPath; } [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] @@ -209,7 +225,7 @@ namespace MediaBrowser.WebDashboard.Api return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => Task.FromResult(stream)); } - return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => GetPackageCreator(DashboardUIPath).ModifyHtml("dummy.html", stream, null, _appHost.ApplicationVersionString, null)); + return _resultFactory.GetStaticResult(Request, plugin.Version.ToString().GetMD5(), null, null, MimeTypes.GetMimeType("page.html"), () => PackageCreator.ModifyHtml(false, stream, null, _appHost.ApplicationVersionString, null)); } throw new ResourceNotFoundException(); @@ -307,6 +323,11 @@ namespace MediaBrowser.WebDashboard.Api /// <returns>System.Object.</returns> public async Task<object> Get(GetDashboardResource request) { + if (!_appConfig.HostWebClient() || DashboardUIPath == null) + { + throw new ResourceNotFoundException(); + } + var path = request.ResourceName; var contentType = MimeTypes.GetMimeType(path); @@ -378,6 +399,11 @@ namespace MediaBrowser.WebDashboard.Api public async Task<object> Get(GetDashboardPackage request) { + if (!_appConfig.HostWebClient() || DashboardUIPath == null) + { + throw new ResourceNotFoundException(); + } + var mode = request.Mode; var inputPath = string.IsNullOrWhiteSpace(mode) ? diff --git a/MediaBrowser.WebDashboard/Api/PackageCreator.cs b/MediaBrowser.WebDashboard/Api/PackageCreator.cs index ad996c5a9..b7c15a840 100644 --- a/MediaBrowser.WebDashboard/Api/PackageCreator.cs +++ b/MediaBrowser.WebDashboard/Api/PackageCreator.cs @@ -31,7 +31,8 @@ namespace MediaBrowser.WebDashboard.Api if (resourceStream != null && IsCoreHtml(virtualPath)) { - resourceStream = await ModifyHtml(virtualPath, resourceStream, mode, appVersion, localizationCulture).ConfigureAwait(false); + bool isMainIndexPage = string.Equals(virtualPath, "index.html", StringComparison.OrdinalIgnoreCase); + resourceStream = await ModifyHtml(isMainIndexPage, resourceStream, mode, appVersion, localizationCulture).ConfigureAwait(false); } return resourceStream; @@ -47,16 +48,25 @@ namespace MediaBrowser.WebDashboard.Api return string.Equals(Path.GetExtension(path), ".html", StringComparison.OrdinalIgnoreCase); } - // Modifies the HTML by adding common meta tags, css and js. - public async Task<Stream> ModifyHtml( - string path, + /// <summary> + /// Modifies the source HTML stream by adding common meta tags, css and js. + /// </summary> + /// <param name="isMainIndexPage">True if the stream contains content for the main index page.</param> + /// <param name="sourceStream">The stream whose content should be modified.</param> + /// <param name="mode">The client mode ('cordova', 'android', etc).</param> + /// <param name="appVersion">The application version.</param> + /// <param name="localizationCulture">The localization culture.</param> + /// <returns> + /// A task that represents the async operation to read and modify the input stream. + /// The task result contains a stream containing the modified HTML content. + /// </returns> + public static async Task<Stream> ModifyHtml( + bool isMainIndexPage, Stream sourceStream, string mode, string appVersion, string localizationCulture) { - var isMainIndexPage = string.Equals(path, "index.html", StringComparison.OrdinalIgnoreCase); - string html; using (var reader = new StreamReader(sourceStream, Encoding.UTF8)) { diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index 36b9a9c1f..5c8de80f1 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -208,8 +208,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers protected void ParseProviderLinks(T item, string xml) { - // Look for a match for the Regex pattern "tt" followed by 7 digits - var m = Regex.Match(xml, @"tt([0-9]{7})", RegexOptions.IgnoreCase); + // Look for a match for the Regex pattern "tt" followed by 7 or 8 digits + var m = Regex.Match(xml, "tt([0-9]{7,8})", RegexOptions.IgnoreCase); if (m.Success) { item.SetProviderId(MetadataProviders.Imdb, m.Value); @@ -14,7 +14,7 @@ <img alt="Current Release" src="https://img.shields.io/github/release/jellyfin/jellyfin.svg"/> </a> <a href="https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/?utm_source=widget"> -<img src="https://translate.jellyfin.org/widgets/jellyfin/-/jellyfin-core/svg-badge.svg" alt="Translation Status"/> +<img alt="Translation Status" src="https://translate.jellyfin.org/widgets/jellyfin/-/jellyfin-core/svg-badge.svg"/> </a> <a href="https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=1"> <img alt="Azure Builds" src="https://dev.azure.com/jellyfin-project/jellyfin/_apis/build/status/Jellyfin%20CI"/> @@ -38,6 +38,12 @@ <a href="https://www.reddit.com/r/jellyfin"> <img alt="Join our Subreddit" src="https://img.shields.io/badge/reddit-r%2Fjellyfin-%23FF5700.svg"/> </a> +<a href="https://github.com/jellyfin/jellyfin/releases.atom"> +<img alt="Release RSS Feed"" src="https://img.shields.io/badge/rss-releases-ffa500?logo=rss" /> +</a> +<a href="https://github.com/jellyfin/jellyfin/commits/master.atom"> +<img alt="Master Commits RSS Feed"" src="https://img.shields.io/badge/rss-commits-ffa500?logo=rss" /> +</a> </p> --- diff --git a/RSSDP/DisposableManagedObjectBase.cs b/RSSDP/DisposableManagedObjectBase.cs index bb36229c4..39589f022 100644 --- a/RSSDP/DisposableManagedObjectBase.cs +++ b/RSSDP/DisposableManagedObjectBase.cs @@ -72,7 +72,7 @@ namespace Rssdp.Infrastructure /// <para>Sets the <see cref="IsDisposed"/> property to true. Does not explicitly throw an exception if called multiple times, but makes no promises about behaviour of derived classes.</para> /// </remarks> /// <seealso cref="IsDisposed"/> - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification="We do exactly as asked, but CA doesn't seem to like us also setting the IsDisposed property. Too bad, it's a good idea and shouldn't cause an exception or anything likely to interfer with the dispose process.")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly", Justification = "We do exactly as asked, but CA doesn't seem to like us also setting the IsDisposed property. Too bad, it's a good idea and shouldn't cause an exception or anything likely to interfer with the dispose process.")] public void Dispose() { IsDisposed = true; diff --git a/RSSDP/SsdpCommunicationsServer.cs b/RSSDP/SsdpCommunicationsServer.cs index 0aa985a26..18097ef24 100644 --- a/RSSDP/SsdpCommunicationsServer.cs +++ b/RSSDP/SsdpCommunicationsServer.cs @@ -8,9 +8,9 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Net; -using Microsoft.Extensions.Logging; -using MediaBrowser.Model.Net; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.Net; +using Microsoft.Extensions.Logging; namespace Rssdp.Infrastructure { diff --git a/benches/Jellyfin.Common.Benches/Jellyfin.Common.Benches.csproj b/benches/Jellyfin.Common.Benches/Jellyfin.Common.Benches.csproj index bea2e6f0f..47aeed05e 100644 --- a/benches/Jellyfin.Common.Benches/Jellyfin.Common.Benches.csproj +++ b/benches/Jellyfin.Common.Benches/Jellyfin.Common.Benches.csproj @@ -2,7 +2,7 @@ <PropertyGroup> <OutputType>Exe</OutputType> - <TargetFramework>netcoreapp3.0</TargetFramework> + <TargetFramework>netcoreapp3.1</TargetFramework> </PropertyGroup> <ItemGroup> diff --git a/deployment/windows/build-jellyfin.ps1 b/deployment/windows/build-jellyfin.ps1 deleted file mode 100644 index c762137a7..000000000 --- a/deployment/windows/build-jellyfin.ps1 +++ /dev/null @@ -1,190 +0,0 @@ -[CmdletBinding()] -param( - [switch]$MakeNSIS, - [switch]$InstallNSIS, - [switch]$InstallFFMPEG, - [switch]$InstallNSSM, - [switch]$SkipJellyfinBuild, - [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', - [ValidateSet('x64','x86', 'arm', 'arm64')][string]$Architecture = 'x64' -) - -$ProgressPreference = 'SilentlyContinue' # Speedup all downloads by hiding progress bars. - -#PowershellCore and *nix check to make determine which temp dir to use. -if(($PSVersionTable.PSEdition -eq 'Core') -and (-not $IsWindows)){ - $TempDir = mktemp -d -}else{ - $TempDir = $env:Temp -} -#Create staging dir -New-Item -ItemType Directory -Force -Path $InstallLocation -$ResolvedInstallLocation = Resolve-Path $InstallLocation -$ResolvedUXLocation = Resolve-Path $UXLocation - -function Build-JellyFin { - if(($Architecture -eq 'arm64') -and ($WindowsVersion -ne 'win10')){ - Write-Error "arm64 only supported with Windows10 Version" - exit - } - if(($Architecture -eq 'arm') -and ($WindowsVersion -notin @('win10','win81','win8'))){ - Write-Error "arm only supported with Windows 8 or higher" - exit - } - Write-Verbose "windowsversion-Architecture: $windowsversion-$Architecture" - Write-Verbose "InstallLocation: $ResolvedInstallLocation" - Write-Verbose "DotNetVerbosity: $DotNetVerbosity" - dotnet publish --self-contained -c $BuildType --output $ResolvedInstallLocation -v $DotNetVerbosity -p:GenerateDocumentationFile=false -p:DebugSymbols=false -p:DebugType=none --runtime `"$windowsversion-$Architecture`" Jellyfin.Server -} - -function Install-FFMPEG { - param( - [string]$ResolvedInstallLocation, - [string]$Architecture, - [string]$FFMPEGVersionX86 = "ffmpeg-4.2.1-win32-shared" - ) - Write-Verbose "Checking Architecture" - if($Architecture -notin @('x86','x64')){ - Write-Warning "No builds available for your selected architecture of $Architecture" - Write-Warning "FFMPEG will not be installed" - }elseif($Architecture -eq 'x64'){ - Write-Verbose "Downloading 64 bit FFMPEG" - Invoke-WebRequest -Uri https://repo.jellyfin.org/releases/server/windows/ffmpeg/jellyfin-ffmpeg.zip -UseBasicParsing -OutFile "$tempdir/ffmpeg.zip" | Write-Verbose - }else{ - Write-Verbose "Downloading 32 bit FFMPEG" - Invoke-WebRequest -Uri https://ffmpeg.zeranoe.com/builds/win32/shared/$FFMPEGVersionX86.zip -UseBasicParsing -OutFile "$tempdir/ffmpeg.zip" | Write-Verbose - } - - Expand-Archive "$tempdir/ffmpeg.zip" -DestinationPath "$tempdir/ffmpeg/" -Force | Write-Verbose - if($Architecture -eq 'x64'){ - Write-Verbose "Copying Binaries to Jellyfin location" - Get-ChildItem "$tempdir/ffmpeg" | ForEach-Object { - Copy-Item $_.FullName -Destination $installLocation | Write-Verbose - } - }else{ - Write-Verbose "Copying Binaries to Jellyfin location" - Get-ChildItem "$tempdir/ffmpeg/$FFMPEGVersionX86/bin" | ForEach-Object { - Copy-Item $_.FullName -Destination $installLocation | Write-Verbose - } - } - Remove-Item "$tempdir/ffmpeg/" -Recurse -Force -ErrorAction Continue | Write-Verbose - Remove-Item "$tempdir/ffmpeg.zip" -Force -ErrorAction Continue | Write-Verbose -} - -function Install-NSSM { - param( - [string]$ResolvedInstallLocation, - [string]$Architecture - ) - Write-Verbose "Checking Architecture" - if($Architecture -notin @('x86','x64')){ - Write-Warning "No builds available for your selected architecture of $Architecture" - Write-Warning "NSSM will not be installed" - }else{ - Write-Verbose "Downloading NSSM" - # [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 - if($Architecture -eq 'x64'){ - Write-Verbose "Copying Binaries to Jellyfin location" - Get-ChildItem "$tempdir/nssm/nssm-2.24-101-g897c7ad/win64" | ForEach-Object { - Copy-Item $_.FullName -Destination $installLocation | Write-Verbose - } - }else{ - Write-Verbose "Copying Binaries to Jellyfin location" - Get-ChildItem "$tempdir/nssm/nssm-2.24-101-g897c7ad/win32" | ForEach-Object { - Copy-Item $_.FullName -Destination $installLocation | Write-Verbose - } - } - Remove-Item "$tempdir/nssm/" -Recurse -Force -ErrorAction Continue | Write-Verbose - Remove-Item "$tempdir/nssm.zip" -Force -ErrorAction Continue | Write-Verbose -} - -function Make-NSIS { - param( - [string]$ResolvedInstallLocation - ) - - $env:InstallLocation = $ResolvedInstallLocation - if($InstallNSIS.IsPresent -or ($InstallNSIS -eq $true)){ - & "$tempdir/nsis/nsis-3.04/makensis.exe" /D$Architecture /DUXPATH=$ResolvedUXLocation ".\deployment\windows\jellyfin.nsi" - } else { - & "makensis" /D$Architecture /DUXPATH=$ResolvedUXLocation ".\deployment\windows\jellyfin.nsi" - } - Copy-Item .\deployment\windows\jellyfin_*.exe $ResolvedInstallLocation\..\ -} - - -function Install-NSIS { - Write-Verbose "Downloading NSIS" - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - Invoke-WebRequest -Uri https://nchc.dl.sourceforge.net/project/nsis/NSIS%203/3.04/nsis-3.04.zip -UseBasicParsing -OutFile "$tempdir/nsis.zip" | Write-Verbose - - Expand-Archive "$tempdir/nsis.zip" -DestinationPath "$tempdir/nsis/" -Force | Write-Verbose -} - -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 -} -if($InstallFFMPEG.IsPresent -or ($InstallFFMPEG -eq $true)){ - Write-Verbose "Starting FFMPEG Install" - Install-FFMPEG $ResolvedInstallLocation $Architecture -} -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 -if($InstallNSIS.IsPresent -or ($InstallNSIS -eq $true)){ - Write-Verbose "Installing NSIS" - Install-NSIS -} -if($MakeNSIS.IsPresent -or ($MakeNSIS -eq $true)){ - Write-Verbose "Starting NSIS Package creation" - Make-NSIS $ResolvedInstallLocation -} -if($InstallNSIS.IsPresent -or ($InstallNSIS -eq $true)){ - Write-Verbose "Cleanup NSIS" - Cleanup-NSIS -} -if($GenerateZip.IsPresent -or ($GenerateZip -eq $true)){ - Compress-Archive -Path $ResolvedInstallLocation -DestinationPath "$ResolvedInstallLocation/jellyfin.zip" -Force -} -Write-Verbose "Finished" diff --git a/deployment/windows/dependencies.txt b/deployment/windows/dependencies.txt deleted file mode 100644 index 16f77cce7..000000000 --- a/deployment/windows/dependencies.txt +++ /dev/null @@ -1,2 +0,0 @@ -dotnet -nsis diff --git a/deployment/windows/dialogs/confirmation.nsddef b/deployment/windows/dialogs/confirmation.nsddef deleted file mode 100644 index 969ebacd6..000000000 --- a/deployment/windows/dialogs/confirmation.nsddef +++ /dev/null @@ -1,24 +0,0 @@ -<?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="confirmation" Title="Confirmation Page" Subtitle="Please confirm your choices for Jellyfin Server installation" GenerateShowFunction="False"> - <HeaderCustomScript>!include "helpers\StrSlash.nsh"</HeaderCustomScript> - <CreateFunctionCustomScript>${StrSlash} '$0' $INSTDIR - - ${StrSlash} '$1' $_JELLYFINDATADIR_ - - ${NSD_SetText} $hCtl_confirmation_ConfirmRichText "{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1043\viewkind4\uc1 \ - \pard\widctlpar\sa160\sl252\slmult1\b The installer will proceed based on the following inputs gathered on earlier screens.\par \ - Installation Folder:\b0 $0\line\b \ - Service install:\b0 $_INSTALLSERVICE_\line\b \ - Service start:\b0 $_SERVICESTART_\line\b \ - Service account:\b0 $_SERVICEACCOUNTTYPE_\line\b \ - Jellyfin Data Folder:\b0 $1\par \ -\ - \pard\sa200\sl276\slmult1\f1\lang1043\par \ - }"</CreateFunctionCustomScript> - <RichText Name="ConfirmRichText" Location="12, 12" Size="426, 204" TabIndex="0" ExStyle="WS_EX_STATICEDGE" /> -</Dialog> diff --git a/deployment/windows/dialogs/confirmation.nsdinc b/deployment/windows/dialogs/confirmation.nsdinc deleted file mode 100644 index f00e9b43a..000000000 --- a/deployment/windows/dialogs/confirmation.nsdinc +++ /dev/null @@ -1,61 +0,0 @@ -; ========================================================= -; This file was generated by NSISDialogDesigner 1.4.4.0 -; http://coolsoft.altervista.org/nsisdialogdesigner -; -; Do not edit it manually, use NSISDialogDesigner instead! -; Modified by EraYaN (2019-09-01) -; ========================================================= - -; handle variables -Var hCtl_confirmation -Var hCtl_confirmation_ConfirmRichText - -; HeaderCustomScript -!include "helpers\StrSlash.nsh" - - - -; dialog create function -Function fnc_confirmation_Create - - ; === confirmation (type: Dialog) === - nsDialogs::Create 1018 - Pop $hCtl_confirmation - ${If} $hCtl_confirmation == error - Abort - ${EndIf} - !insertmacro MUI_HEADER_TEXT "Confirmation Page" "Please confirm your choices for Jellyfin Server installation" - - ; === ConfirmRichText (type: RichText) === - nsDialogs::CreateControl /NOUNLOAD "RichEdit20A" ${ES_READONLY}|${WS_VISIBLE}|${WS_CHILD}|${WS_TABSTOP}|${WS_VSCROLL}|${ES_MULTILINE}|${ES_WANTRETURN} ${WS_EX_STATICEDGE} 8u 7u 280u 126u "" - Pop $hCtl_confirmation_ConfirmRichText - ${NSD_AddExStyle} $hCtl_confirmation_ConfirmRichText ${WS_EX_STATICEDGE} - - ; CreateFunctionCustomScript - ${StrSlash} '$0' $INSTDIR - - ${StrSlash} '$1' $_JELLYFINDATADIR_ - - ${If} $_INSTALLSERVICE_ == "Yes" - ${NSD_SetText} $hCtl_confirmation_ConfirmRichText "{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1043\viewkind4\uc1 \ - \pard\widctlpar\sa160\sl252\slmult1\b The installer will proceed based on the following inputs gathered on earlier screens.\par \ - Installation Folder:\b0 $0\line\b \ - Service install:\b0 $_INSTALLSERVICE_\line\b \ - Service start:\b0 $_SERVICESTART_\line\b \ - Service account:\b0 $_SERVICEACCOUNTTYPE_\line\b \ - Jellyfin Data Folder:\b0 $1\par \ - \ - \pard\sa200\sl276\slmult1\f1\lang1043\par \ - }" - ${Else} - ${NSD_SetText} $hCtl_confirmation_ConfirmRichText "{\rtf1\ansi\ansicpg1252\deff0\nouicompat\deflang1043\viewkind4\uc1 \ - \pard\widctlpar\sa160\sl252\slmult1\b The installer will proceed based on the following inputs gathered on earlier screens.\par \ - Installation Folder:\b0 $0\line\b \ - Service install:\b0 $_INSTALLSERVICE_\line\b \ - Jellyfin Data Folder:\b0 $1\par \ - \ - \pard\sa200\sl276\slmult1\f1\lang1043\par \ - }" - ${EndIf} - -FunctionEnd diff --git a/deployment/windows/dialogs/service-config.nsddef b/deployment/windows/dialogs/service-config.nsddef deleted file mode 100644 index 3509ada24..000000000 --- a/deployment/windows/dialogs/service-config.nsddef +++ /dev/null @@ -1,13 +0,0 @@ -<?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="service_config" Title="CoOnfigure the service" Subtitle="This controls what type of access the server gets to this system." GenerateShowFunction="False"> - <CheckBox Name="StartServiceAfterInstall" Location="12, 192" Size="426, 24" Text="Start Service after Install" Checked="True" TabIndex="0" /> - <Label Name="LocalSystemAccountLabel" Location="12, 115" Size="426, 46" Text="The Local System account has full access to every resource and file on the system. This can have very real security implications, do not use unless absolutely neseccary." TabIndex="1" /> - <Label Name="NetworkServiceAccountLabel" Location="12, 39" Size="426, 46" Text="The NetworkService account is a predefined local account used by the service control manager. It is the recommended way to install the Jellyfin Server service." TabIndex="2" /> - <RadioButton Name="UseLocalSystemAccount" Location="12, 88" Size="426, 24" Text="Use Local System account" TabIndex="3" /> - <RadioButton Name="UseNetworkServiceAccount" Location="12, 12" Size="426, 24" Text="Use Network Service account (Recommended)" Font="Microsoft Sans Serif, 8.25pt, style=Bold" Checked="True" TabIndex="4" /> -</Dialog>
\ No newline at end of file diff --git a/deployment/windows/dialogs/service-config.nsdinc b/deployment/windows/dialogs/service-config.nsdinc deleted file mode 100644 index 58c350f2e..000000000 --- a/deployment/windows/dialogs/service-config.nsdinc +++ /dev/null @@ -1,56 +0,0 @@ -; ========================================================= -; 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_service_config -Var hCtl_service_config_StartServiceAfterInstall -Var hCtl_service_config_LocalSystemAccountLabel -Var hCtl_service_config_NetworkServiceAccountLabel -Var hCtl_service_config_UseLocalSystemAccount -Var hCtl_service_config_UseNetworkServiceAccount -Var hCtl_service_config_Font1 - - -; dialog create function -Function fnc_service_config_Create - - ; custom font definitions - CreateFont $hCtl_service_config_Font1 "Microsoft Sans Serif" "8.25" "700" - - ; === service_config (type: Dialog) === - nsDialogs::Create 1018 - Pop $hCtl_service_config - ${If} $hCtl_service_config == error - Abort - ${EndIf} - !insertmacro MUI_HEADER_TEXT "Configure the service" "This controls what type of access the server gets to this system." - - ; === StartServiceAfterInstall (type: Checkbox) === - ${NSD_CreateCheckbox} 8u 118u 280u 15u "Start Service after Install" - Pop $hCtl_service_config_StartServiceAfterInstall - ${NSD_Check} $hCtl_service_config_StartServiceAfterInstall - - ; === LocalSystemAccountLabel (type: Label) === - ${NSD_CreateLabel} 8u 71u 280u 28u "The Local System account has full access to every resource and file on the system. This can have very real security implications, do not use unless absolutely neseccary." - Pop $hCtl_service_config_LocalSystemAccountLabel - - ; === NetworkServiceAccountLabel (type: Label) === - ${NSD_CreateLabel} 8u 24u 280u 28u "The NetworkService account is a predefined local account used by the service control manager. It is the recommended way to install the Jellyfin Server service." - Pop $hCtl_service_config_NetworkServiceAccountLabel - - ; === UseLocalSystemAccount (type: RadioButton) === - ${NSD_CreateRadioButton} 8u 54u 280u 15u "Use Local System account" - Pop $hCtl_service_config_UseLocalSystemAccount - ${NSD_AddStyle} $hCtl_service_config_UseLocalSystemAccount ${WS_GROUP} - - ; === UseNetworkServiceAccount (type: RadioButton) === - ${NSD_CreateRadioButton} 8u 7u 280u 15u "Use Network Service account (Recommended)" - Pop $hCtl_service_config_UseNetworkServiceAccount - SendMessage $hCtl_service_config_UseNetworkServiceAccount ${WM_SETFONT} $hCtl_service_config_Font1 0 - ${NSD_Check} $hCtl_service_config_UseNetworkServiceAccount - -FunctionEnd diff --git a/deployment/windows/dialogs/setuptype.nsddef b/deployment/windows/dialogs/setuptype.nsddef deleted file mode 100644 index b55ceeaaa..000000000 --- a/deployment/windows/dialogs/setuptype.nsddef +++ /dev/null @@ -1,12 +0,0 @@ -<?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 deleted file mode 100644 index 8746ad2cc..000000000 --- a/deployment/windows/dialogs/setuptype.nsdinc +++ /dev/null @@ -1,50 +0,0 @@ -; ========================================================= -; 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/helpers/ShowError.nsh b/deployment/windows/helpers/ShowError.nsh deleted file mode 100644 index 6e09b1e40..000000000 --- a/deployment/windows/helpers/ShowError.nsh +++ /dev/null @@ -1,10 +0,0 @@ -; Show error -!macro ShowError TEXT RETRYLABEL - MessageBox MB_ABORTRETRYIGNORE|MB_ICONSTOP "${TEXT}" IDIGNORE +2 IDRETRY ${RETRYLABEL} - Abort -!macroend - -!macro ShowErrorFinal TEXT - MessageBox MB_OK|MB_ICONSTOP "${TEXT}" - Abort -!macroend diff --git a/deployment/windows/helpers/StrSlash.nsh b/deployment/windows/helpers/StrSlash.nsh deleted file mode 100644 index b8aa771aa..000000000 --- a/deployment/windows/helpers/StrSlash.nsh +++ /dev/null @@ -1,47 +0,0 @@ -; Adapted from: https://nsis.sourceforge.io/Another_String_Replace_(and_Slash/BackSlash_Converter) (2019-08-31) - -!macro _StrSlashConstructor out in - Push "${in}" - Push "\" - Call StrSlash - Pop ${out} -!macroend - -!define StrSlash '!insertmacro "_StrSlashConstructor"' - -; Push $filenamestring (e.g. 'c:\this\and\that\filename.htm') -; Push "\" -; Call StrSlash -; Pop $R0 -; ;Now $R0 contains 'c:/this/and/that/filename.htm' -Function StrSlash - Exch $R3 ; $R3 = needle ("\" or "/") - Exch - Exch $R1 ; $R1 = String to replacement in (haystack) - Push $R2 ; Replaced haystack - Push $R4 ; $R4 = not $R3 ("/" or "\") - Push $R6 - Push $R7 ; Scratch reg - StrCpy $R2 "" - StrLen $R6 $R1 - StrCpy $R4 "\" - StrCmp $R3 "/" loop - StrCpy $R4 "/" -loop: - StrCpy $R7 $R1 1 - StrCpy $R1 $R1 $R6 1 - StrCmp $R7 $R3 found - StrCpy $R2 "$R2$R7" - StrCmp $R1 "" done loop -found: - StrCpy $R2 "$R2$R4" - StrCmp $R1 "" done loop -done: - StrCpy $R3 $R2 - Pop $R7 - Pop $R6 - Pop $R4 - Pop $R2 - Pop $R1 - Exch $R3 -FunctionEnd diff --git a/deployment/windows/jellyfin.nsi b/deployment/windows/jellyfin.nsi deleted file mode 100644 index fada62d98..000000000 --- a/deployment/windows/jellyfin.nsi +++ /dev/null @@ -1,575 +0,0 @@ -!verbose 3 -SetCompressor /SOLID bzip2 -ShowInstDetails show -ShowUninstDetails show -Unicode True - -;-------------------------------- -!define SF_USELECTED 0 ; used to check selected options status, rest are inherited from Sections.nsh - - !include "MUI2.nsh" - !include "Sections.nsh" - !include "LogicLib.nsh" - - !include "helpers\ShowError.nsh" - -; 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" - !define NAMESUFFIX "(64 bit)" - !define INSTALL_DIRECTORY "$PROGRAMFILES64\Jellyfin\Server" -!endif - -!ifdef x84 - !define ARCH "x86" - !define NAMESUFFIX "(32 bit)" - !define INSTALL_DIRECTORY "$PROGRAMFILES32\Jellyfin\Server" -!endif - -!ifndef ARCH - !error "Set the Arch with /Dx86 or /Dx64" -!endif - -;-------------------------------- - - !define REG_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\JellyfinServer" ;Registry to show up in Add/Remove Programs - !define REG_CONFIG_KEY "Software\Jellyfin\Server" ;Registry to store all configuration - - !getdllversion "$%InstallLocation%\jellyfin.dll" ver_ ;Align installer version with jellyfin.dll version - - Name "Jellyfin Server ${ver_1}.${ver_2}.${ver_3} ${NAMESUFFIX}" ; This is referred in various header text labels - OutFile "jellyfin_${ver_1}.${ver_2}.${ver_3}_windows-${ARCH}.exe" ; Naming convention jellyfin_{version}_windows-{arch].exe - BrandingText "Jellyfin Server ${ver_1}.${ver_2}.${ver_3} Installer" ; This shows in just over the buttons - -; installer attributes, these show up in details tab on installer properties - VIProductVersion "${ver_1}.${ver_2}.${ver_3}.0" ; VIProductVersion format, should be X.X.X.X - VIFileVersion "${ver_1}.${ver_2}.${ver_3}.0" ; VIFileVersion format, should be X.X.X.X - VIAddVersionKey "ProductName" "Jellyfin Server" - VIAddVersionKey "FileVersion" "${ver_1}.${ver_2}.${ver_3}.0" - VIAddVersionKey "LegalCopyright" "(c) 2019 Jellyfin Contributors. Code released under the GNU General Public License" - VIAddVersionKey "FileDescription" "Jellyfin Server: The Free Software Media System" - -;TODO, check defaults - InstallDir ${INSTALL_DIRECTORY} ;Default installation folder - InstallDirRegKey HKLM "${REG_CONFIG_KEY}" "InstallFolder" ;Read the registry for install folder, - - RequestExecutionLevel admin ; ask it upfront for service control, and installing in priv folders - - CRCCheck on ; make sure the installer wasn't corrupted while downloading - - !define MUI_ABORTWARNING ;Prompts user in case of aborting install - -; TODO: Replace with nice Jellyfin Icons -!ifdef UXPATH - !define MUI_ICON "${UXPATH}\branding\NSIS\modern-install.ico" ; Installer Icon - !define MUI_UNICON "${UXPATH}\branding\NSIS\modern-install.ico" ; Uninstaller Icon - - !define MUI_HEADERIMAGE - !define MUI_HEADERIMAGE_BITMAP "${UXPATH}\branding\NSIS\installer-header.bmp" - !define MUI_WELCOMEFINISHPAGE_BITMAP "${UXPATH}\branding\NSIS\installer-right.bmp" - !define MUI_UNWELCOMEFINISHPAGE_BITMAP "${UXPATH}\branding\NSIS\installer-right.bmp" -!endif - -;-------------------------------- -;Pages - -; Welcome Page - !define MUI_WELCOMEPAGE_TEXT "The installer will ask for details to install Jellyfin Server." - !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 - !insertmacro MUI_PAGE_DIRECTORY - -; Data folder Page - !define MUI_PAGE_CUSTOMFUNCTION_PRE HideDataDirectoryPage ; Controls when to hide / show - !define MUI_PAGE_HEADER_TEXT "Choose Data Location" - !define MUI_PAGE_HEADER_SUBTEXT "Choose the folder in which to install the Jellyfin Server data." - !define MUI_DIRECTORYPAGE_TEXT_TOP "The installer will set the following folder for Jellyfin Server data. To install in a different folder, click Browse and select another folder. Please make sure the folder exists and is accessible. Click Next to continue." - !define MUI_DIRECTORYPAGE_TEXT_DESTINATION "Data folder" - !define MUI_DIRECTORYPAGE_VARIABLE $_JELLYFINDATADIR_ - !insertmacro MUI_PAGE_DIRECTORY - -; Custom Dialogs - !include "dialogs\setuptype.nsdinc" - !include "dialogs\service-config.nsdinc" - !include "dialogs\confirmation.nsdinc" - -; Select service account type - #!define MUI_PAGE_CUSTOMFUNCTION_PRE HideServiceConfigPage ; Controls when to hide / show (This does not work for Page, might need to go PageEx) - #!define MUI_PAGE_CUSTOMFUNCTION_SHOW fnc_service_config_Show - #!define MUI_PAGE_CUSTOMFUNCTION_LEAVE ServiceConfigPage_Config - #!insertmacro MUI_PAGE_CUSTOM ServiceAccountType - Page custom ShowServiceConfigPage ServiceConfigPage_Config - -; Confirmation Page - Page custom ShowConfirmationPage ; just letting the user know what they chose to install - -; Actual Installion Page - !insertmacro MUI_PAGE_INSTFILES - - !insertmacro MUI_UNPAGE_CONFIRM - !insertmacro MUI_UNPAGE_INSTFILES - #!insertmacro MUI_UNPAGE_FINISH - -;-------------------------------- -;Languages; Add more languages later here if needed - !insertmacro MUI_LANGUAGE "English" - -;-------------------------------- -;Installer Sections -Section "!Jellyfin Server (required)" InstallJellyfinServer - SectionIn RO ; Mandatory section, isn't this the whole purpose to run the installer. - - StrCmp "$_EXISTINGINSTALLATION_" "Yes" RunUninstaller CarryOn ; Silently uninstall in case of previous installation - - RunUninstaller: - DetailPrint "Looking for uninstaller at $INSTDIR" - FindFirst $0 $1 "$INSTDIR\Uninstall.exe" - FindClose $0 - StrCmp $1 "" CarryOn ; the registry key was there but uninstaller was not found - - DetailPrint "Silently running the uninstaller at $INSTDIR" - ExecWait '"$INSTDIR\Uninstall.exe" /S _?=$INSTDIR' $0 - DetailPrint "Uninstall finished, $0" - - CarryOn: - ${If} $_EXISTINGSERVICE_ == 'Yes' - ExecWait '"$INSTDIR\nssm.exe" stop JellyfinServer' $0 - ${If} $0 <> 0 - MessageBox MB_OK|MB_ICONSTOP "Could not stop the Jellyfin Server service." - Abort - ${EndIf} - DetailPrint "Stopped Jellyfin Server service, $0" - ${EndIf} - - 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" - WriteRegExpandStr HKLM "${REG_CONFIG_KEY}" "DataFolder" "$_JELLYFINDATADIR_" - WriteRegStr HKLM "${REG_CONFIG_KEY}" "ServiceAccountType" "$_SERVICEACCOUNTTYPE_" - - !getdllversion "$%InstallLocation%\jellyfin.dll" ver_ - StrCpy $_JELLYFINVERSION_ "${ver_1}.${ver_2}.${ver_3}" ; - -; Write the uninstall keys for Windows - WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayName" "Jellyfin Server $_JELLYFINVERSION_ ${NAMESUFFIX}" - 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.org/" - WriteRegStr HKLM "${REG_UNINST_KEY}" "DisplayVersion" "$_JELLYFINVERSION_" - WriteRegDWORD HKLM "${REG_UNINST_KEY}" "NoModify" 1 - WriteRegDWORD HKLM "${REG_UNINST_KEY}" "NoRepair" 1 - -;Create uninstaller - WriteUninstaller "$INSTDIR\Uninstall.exe" -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" --service --datadir \"$_JELLYFINDATADIR_\"' $0 - ${If} $0 <> 0 - !insertmacro ShowError "Could not install the Jellyfin Server service." InstallRetry - ${EndIf} - DetailPrint "Jellyfin Server Service install, $0" - ${Else} - DetailPrint "Jellyfin Server Service exists, updating..." - - ConfigureApplicationRetry: - ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer Application "$INSTDIR\jellyfin.exe"' $0 - ${If} $0 <> 0 - !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureApplicationRetry - ${EndIf} - DetailPrint "Jellyfin Server Service setting (Application), $0" - - ConfigureAppParametersRetry: - 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} - DetailPrint "Jellyfin Server Service setting (AppParameters), $0" - ${EndIf} - - - Sleep 3000 ; Give time for Windows to catchup - ConfigureStartRetry: - ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer Start SERVICE_DELAYED_AUTO_START' $0 - ${If} $0 <> 0 - !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureStartRetry - ${EndIf} - DetailPrint "Jellyfin Server Service setting (Start), $0" - - ConfigureDescriptionRetry: - ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer Description "Jellyfin Server: The Free Software Media System"' $0 - ${If} $0 <> 0 - !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureDescriptionRetry - ${EndIf} - DetailPrint "Jellyfin Server Service setting (Description), $0" - ConfigureDisplayNameRetry: - ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer DisplayName "Jellyfin Server"' $0 - ${If} $0 <> 0 - !insertmacro ShowError "Could not configure the Jellyfin Server service." ConfigureDisplayNameRetry - - ${EndIf} - DetailPrint "Jellyfin Server Service setting (DisplayName), $0" - - Sleep 3000 - ${If} $_SERVICEACCOUNTTYPE_ == "NetworkService" ; the default install using NSSM is Local System - ConfigureNetworkServiceRetry: - ExecWait '"$INSTDIR\nssm.exe" set JellyfinServer Objectname "Network Service"' $0 - ${If} $0 <> 0 - !insertmacro ShowError "Could not configure the Jellyfin Server service account." ConfigureNetworkServiceRetry - ${EndIf} - 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 -${If} $_SERVICESTART_ == "Yes" -${AndIf} $_INSTALLSERVICE_ == "Yes" - StartRetry: - ExecWait '"$INSTDIR\nssm.exe" start JellyfinServer' $0 - ${If} $0 <> 0 - !insertmacro ShowError "Could not start the Jellyfin Server service." StartRetry - ${EndIf} - DetailPrint "Jellyfin Server service start, $0" -${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 - -;Language strings - LangString DESC_InstallJellyfinServer ${LANG_ENGLISH} "Install Jellyfin Server" - LangString DESC_InstallService ${LANG_ENGLISH} "Install As a Service" - -;Assign language strings to sections - !insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN - !insertmacro MUI_DESCRIPTION_TEXT ${InstallJellyfinServer} $(DESC_InstallJellyfinServer) - !insertmacro MUI_DESCRIPTION_TEXT ${InstallService} $(DESC_InstallService) - !insertmacro MUI_FUNCTION_DESCRIPTION_END - -;-------------------------------- -;Uninstaller Section - -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_" - - MessageBox MB_YESNO|MB_ICONINFORMATION "Do you want to retain the Jellyfin Server data folder? The media will not be touched. $\r$\nIf unsure choose YES." /SD IDYES IDYES PreserveData - - RMDir /r /REBOOTOK "$_JELLYFINDATADIR_" - - PreserveData: - - ExecWait '"$INSTDIR\nssm.exe" statuscode JellyfinServer' $0 - DetailPrint "Jellyfin Server service statuscode, $0" - IntCmp $0 0 NoServiceUninstall ; service doesn't exist, may be run from desktop shortcut - - Sleep 3000 ; Give time for Windows to catchup - - UninstallStopRetry: - ExecWait '"$INSTDIR\nssm.exe" stop JellyfinServer' $0 - ${If} $0 <> 0 - !insertmacro ShowError "Could not stop the Jellyfin Server service." UninstallStopRetry - ${EndIf} - DetailPrint "Stopped Jellyfin Server service, $0" - - UninstallRemoveRetry: - ExecWait '"$INSTDIR\nssm.exe" remove JellyfinServer confirm' $0 - ${If} $0 <> 0 - !insertmacro ShowError "Could not remove the Jellyfin Server service." UninstallRemoveRetry - ${EndIf} - DetailPrint "Removed Jellyfin Server service, $0" - - Sleep 3000 ; Give time for Windows to catchup - - 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}" - -SectionEnd - -Function .onInit -; Setting up defaults - StrCpy $_INSTALLSERVICE_ "Yes" - StrCpy $_SERVICESTART_ "Yes" - StrCpy $_SERVICEACCOUNTTYPE_ "NetworkService" - StrCpy $_EXISTINGINSTALLATION_ "No" - StrCpy $_EXISTINGSERVICE_ "No" - StrCpy $_MAKESHORTCUTS_ "No" - - SetShellVarContext current - StrCpy $_JELLYFINDATADIR_ "$%ProgramData%\Jellyfin\Server" - - System::Call 'kernel32::CreateMutex(p 0, i 0, t "JellyfinServerMutex") p .r1 ?e' - Pop $R0 - - StrCmp $R0 0 +3 - !insertmacro ShowErrorFinal "The installer is already running." - -;Detect if Jellyfin is already installed. -; In case it is installed, let the user choose either -; 1. Exit installer -; 2. Upgrade without messing with data -; 2a. Don't ask for any details, uninstall and install afresh with old settings - -; Read Registry for previous installation - ClearErrors - ReadRegStr "$0" HKLM "${REG_CONFIG_KEY}" "InstallFolder" - IfErrors NoExisitingInstall - - DetailPrint "Existing Jellyfin Server detected at: $0" - StrCpy "$INSTDIR" "$0" ; set the location fro registry as new default - - 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 - DetailPrint "Jellyfin Server service statuscode, $0" - IntCmp $0 0 NoService ; service doesn't exist, may be run from desktop shortcut - - ; if service was detected, set defaults going forward. - StrCpy $_EXISTINGSERVICE_ "Yes" - StrCpy $_INSTALLSERVICE_ "Yes" - StrCpy $_SERVICESTART_ "Yes" - StrCpy $_MAKESHORTCUTS_ "No" - SectionSetText ${CreateWinShortcuts} "" - - - 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. \ - $\r$\nClick OK to proceed, Cancel to exit installer." /SD IDOK IDOK ProceedWithUpgrade - Quit ; Quit if the user is not sure about upgrade - - ProceedWithUpgrade: - - NoExisitingInstall: ; by this time, the variables have been correctly set to reflect previous install details - -FunctionEnd - -Function HideInstallDirectoryPage - ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder - Abort - ${EndIf} -FunctionEnd - -Function HideDataDirectoryPage - ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder - Abort - ${EndIf} -FunctionEnd - -Function HideServiceConfigPage - ${If} $_INSTALLSERVICE_ == "No" ; Not running as a service, don't ask for service type - ${OrIf} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder - Abort - ${EndIf} -FunctionEnd - -Function HideConfirmationPage - ${If} $_EXISTINGINSTALLATION_ == "Yes" ; Existing installation detected, so don't ask for InstallFolder - Abort - ${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 - Call fnc_service_config_Create - nsDialogs::Show -FunctionEnd - -; Confirmation dialog show function -Function ShowConfirmationPage - Call HideConfirmationPage - Call fnc_confirmation_Create - nsDialogs::Show -FunctionEnd - -; Declare temp variables to read the options from the custom page. -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 -${If} $StartServiceAfterInstall == 1 - StrCpy $_SERVICESTART_ "Yes" -${Else} - StrCpy $_SERVICESTART_ "No" -${EndIf} -${NSD_GetState} $hCtl_service_config_UseNetworkServiceAccount $UseNetworkServiceAccount -${NSD_GetState} $hCtl_service_config_UseLocalSystemAccount $UseLocalSystemAccount - -${If} $UseNetworkServiceAccount == 1 - StrCpy $_SERVICEACCOUNTTYPE_ "NetworkService" -${ElseIf} $UseLocalSystemAccount == 1 - StrCpy $_SERVICEACCOUNTTYPE_ "LocalSystem" -${Else} - !insertmacro ShowErrorFinal "Service account type not properly configured." -${EndIf} - -FunctionEnd - -; This function handles the choices during component selection -Function .onSelChange - -; If we are not installing service, we don't need to set the NetworkService account or StartService - SectionGetFlags ${InstallService} $0 - ${If} $0 = ${SF_SELECTED} - StrCpy $_INSTALLSERVICE_ "Yes" - ${Else} - StrCpy $_INSTALLSERVICE_ "No" - StrCpy $_SERVICESTART_ "No" - StrCpy $_SERVICEACCOUNTTYPE_ "None" - ${EndIf} -FunctionEnd - -Function .onInstSuccess - #ExecShell "open" "http://localhost:8096" -FunctionEnd diff --git a/deployment/windows/legacy/install-jellyfin.ps1 b/deployment/windows/legacy/install-jellyfin.ps1 deleted file mode 100644 index e909a0468..000000000 --- a/deployment/windows/legacy/install-jellyfin.ps1 +++ /dev/null @@ -1,460 +0,0 @@ -[CmdletBinding()] - -param( - [Switch]$Quiet, - [Switch]$InstallAsService, - [System.Management.Automation.pscredential]$ServiceUser, - [switch]$CreateDesktopShorcut, - [switch]$LaunchJellyfin, - [switch]$MigrateEmbyLibrary, - [string]$InstallLocation, - [string]$EmbyLibraryLocation, - [string]$JellyfinLibraryLocation -) -<# This form was created using POSHGUI.com a free online gui designer for PowerShell -.NAME - Install-Jellyfin -#> - -#This doesn't need to be used by default anymore, but I am keeping it in as a function for future use. -function Elevate-Window { - # Get the ID and security principal of the current user account - $myWindowsID=[System.Security.Principal.WindowsIdentity]::GetCurrent() - $myWindowsPrincipal=new-object System.Security.Principal.WindowsPrincipal($myWindowsID) - - # Get the security principal for the Administrator role - $adminRole=[System.Security.Principal.WindowsBuiltInRole]::Administrator - - # Check to see if we are currently running "as Administrator" - if ($myWindowsPrincipal.IsInRole($adminRole)) - { - # We are running "as Administrator" - so change the title and background color to indicate this - $Host.UI.RawUI.WindowTitle = $myInvocation.MyCommand.Definition + "(Elevated)" - $Host.UI.RawUI.BackgroundColor = "DarkBlue" - clear-host - } - else - { - # We are not running "as Administrator" - so relaunch as administrator - - # Create a new process object that starts PowerShell - $newProcess = new-object System.Diagnostics.ProcessStartInfo "PowerShell"; - - # Specify the current script path and name as a parameter - $newProcess.Arguments = $myInvocation.MyCommand.Definition; - - # Indicate that the process should be elevated - $newProcess.Verb = "runas"; - - # Start the new process - [System.Diagnostics.Process]::Start($newProcess); - - # Exit from the current, unelevated, process - exit - } -} - -#FIXME The install methods should be a function that takes all the params, the quiet flag should be a paramset - -if($Quiet.IsPresent -or $Quiet -eq $true){ - if([string]::IsNullOrEmpty($JellyfinLibraryLocation)){ - $Script:JellyfinDataDir = "$env:LOCALAPPDATA\jellyfin\" - }else{ - $Script:JellyfinDataDir = $JellyfinLibraryLocation - } - if([string]::IsNullOrEmpty($InstallLocation)){ - $Script:DefaultJellyfinInstallDirectory = "$env:Appdata\jellyfin\" - }else{ - $Script:DefaultJellyfinInstallDirectory = $InstallLocation - } - - if([string]::IsNullOrEmpty($EmbyLibraryLocation)){ - $Script:defaultEmbyDataDir = "$env:Appdata\Emby-Server\data\" - }else{ - $Script:defaultEmbyDataDir = $EmbyLibraryLocation - } - - if($InstallAsService.IsPresent -or $InstallAsService -eq $true){ - $Script:InstallAsService = $true - }else{$Script:InstallAsService = $false} - if($null -eq $ServiceUser){ - $Script:InstallServiceAsUser = $false - }else{ - $Script:InstallServiceAsUser = $true - $Script:UserCredentials = $ServiceUser - $Script:JellyfinDataDir = "$env:HOMEDRIVE\Users\$($Script:UserCredentials.UserName)\Appdata\Local\jellyfin\"} - if($CreateDesktopShorcut.IsPresent -or $CreateDesktopShorcut -eq $true) {$Script:CreateShortcut = $true}else{$Script:CreateShortcut = $false} - if($MigrateEmbyLibrary.IsPresent -or $MigrateEmbyLibrary -eq $true){$Script:MigrateLibrary = $true}else{$Script:MigrateLibrary = $false} - if($LaunchJellyfin.IsPresent -or $LaunchJellyfin -eq $true){$Script:StartJellyfin = $true}else{$Script:StartJellyfin = $false} - - if(-not (Test-Path $Script:DefaultJellyfinInstallDirectory)){ - mkdir $Script:DefaultJellyfinInstallDirectory - } - Copy-Item -Path $PSScriptRoot/* -DestinationPath "$Script:DefaultJellyfinInstallDirectory/" -Force -Recurse - if($Script:InstallAsService){ - if($Script:InstallServiceAsUser){ - &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" install Jellyfin `"$Script:DefaultJellyfinInstallDirectory\jellyfin.exe`" --datadir `"$Script:JellyfinDataDir`" - Start-Sleep -Milliseconds 500 - &sc.exe config Jellyfin obj=".\$($Script:UserCredentials.UserName)" password="$($Script:UserCredentials.GetNetworkCredential().Password)" - &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin Start SERVICE_DELAYED_AUTO_START - }else{ - &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" install Jellyfin `"$Script:DefaultJellyfinInstallDirectory\jellyfin.exe`" --datadir `"$Script:JellyfinDataDir`" - Start-Sleep -Milliseconds 500 - #&"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin ObjectName $Script:UserCredentials.UserName $Script:UserCredentials.GetNetworkCredential().Password - #Set-Service -Name Jellyfin -Credential $Script:UserCredentials - &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin Start SERVICE_DELAYED_AUTO_START - } - } - if($Script:MigrateLibrary){ - Copy-Item -Path $Script:defaultEmbyDataDir/config -Destination $Script:JellyfinDataDir -force -Recurse - Copy-Item -Path $Script:defaultEmbyDataDir/cache -Destination $Script:JellyfinDataDir -force -Recurse - Copy-Item -Path $Script:defaultEmbyDataDir/data -Destination $Script:JellyfinDataDir -force -Recurse - Copy-Item -Path $Script:defaultEmbyDataDir/metadata -Destination $Script:JellyfinDataDir -force -Recurse - Copy-Item -Path $Script:defaultEmbyDataDir/root -Destination $Script:JellyfinDataDir -force -Recurse - } - if($Script:CreateShortcut){ - $WshShell = New-Object -comObject WScript.Shell - $Shortcut = $WshShell.CreateShortcut("$Home\Desktop\Jellyfin.lnk") - $Shortcut.TargetPath = "$Script:DefaultJellyfinInstallDirectory\jellyfin.exe" - $Shortcut.Save() - } - if($Script:StartJellyfin){ - if($Script:InstallAsService){ - Get-Service Jellyfin | Start-Service - }else{ - Start-Process -FilePath $Script:DefaultJellyfinInstallDirectory\jellyfin.exe -PassThru - } - } -}else{ - -} -Add-Type -AssemblyName System.Windows.Forms -[System.Windows.Forms.Application]::EnableVisualStyles() - -$Script:JellyFinDataDir = "$env:LOCALAPPDATA\jellyfin\" -$Script:DefaultJellyfinInstallDirectory = "$env:Appdata\jellyfin\" -$Script:defaultEmbyDataDir = "$env:Appdata\Emby-Server\" -$Script:InstallAsService = $False -$Script:InstallServiceAsUser = $false -$Script:CreateShortcut = $false -$Script:MigrateLibrary = $false -$Script:StartJellyfin = $false - -function InstallJellyfin { - Write-Host "Install as service: $Script:InstallAsService" - Write-Host "Install as serviceuser: $Script:InstallServiceAsUser" - Write-Host "Create Shortcut: $Script:CreateShortcut" - Write-Host "MigrateLibrary: $Script:MigrateLibrary" - $GUIElementsCollection | ForEach-Object { - $_.Enabled = $false - } - Write-Host "Making Jellyfin directory" - $ProgressBar.Minimum = 1 - $ProgressBar.Maximum = 100 - $ProgressBar.Value = 1 - if($Script:DefaultJellyfinInstallDirectory -ne $InstallLocationBox.Text){ - Write-Host "Custom Install Location Chosen: $($InstallLocationBox.Text)" - $Script:DefaultJellyfinInstallDirectory = $InstallLocationBox.Text - } - if($Script:JellyfinDataDir -ne $CustomLibraryBox.Text){ - Write-Host "Custom Library Location Chosen: $($CustomLibraryBox.Text)" - $Script:JellyfinDataDir = $CustomLibraryBox.Text - } - if(-not (Test-Path $Script:DefaultJellyfinInstallDirectory)){ - mkdir $Script:DefaultJellyfinInstallDirectory - } - Write-Host "Copying Jellyfin Data" - $progressbar.Value = 10 - Copy-Item -Path $PSScriptRoot/* -Destination $Script:DefaultJellyfinInstallDirectory/ -Force -Recurse - Write-Host "Finished Copying" - $ProgressBar.Value = 50 - if($Script:InstallAsService){ - if($Script:InstallServiceAsUser){ - Write-Host "Installing Service as user $($Script:UserCredentials.UserName)" - &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" install Jellyfin `"$Script:DefaultJellyfinInstallDirectory\jellyfin.exe`" --datadir `"$Script:JellyfinDataDir`" - Start-Sleep -Milliseconds 2000 - &sc.exe config Jellyfin obj=".\$($Script:UserCredentials.UserName)" password="$($Script:UserCredentials.GetNetworkCredential().Password)" - &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin Start SERVICE_DELAYED_AUTO_START - }else{ - Write-Host "Installing Service as LocalSystem" - &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" install Jellyfin `"$Script:DefaultJellyfinInstallDirectory\jellyfin.exe`" --datadir `"$Script:JellyfinDataDir`" - Start-Sleep -Milliseconds 2000 - &"$Script:DefaultJellyfinInstallDirectory\nssm.exe" set Jellyfin Start SERVICE_DELAYED_AUTO_START - } - } - $progressbar.Value = 60 - if($Script:MigrateLibrary){ - if($Script:defaultEmbyDataDir -ne $LibraryLocationBox.Text){ - Write-Host "Custom location defined for emby library: $($LibraryLocationBox.Text)" - $Script:defaultEmbyDataDir = $LibraryLocationBox.Text - } - Write-Host "Copying emby library from $Script:defaultEmbyDataDir to $Script:JellyFinDataDir" - Write-Host "This could take a while depending on the size of your library. Please be patient" - Write-Host "Copying config" - Copy-Item -Path $Script:defaultEmbyDataDir/config -Destination $Script:JellyfinDataDir -force -Recurse - Write-Host "Copying cache" - Copy-Item -Path $Script:defaultEmbyDataDir/cache -Destination $Script:JellyfinDataDir -force -Recurse - Write-Host "Copying data" - Copy-Item -Path $Script:defaultEmbyDataDir/data -Destination $Script:JellyfinDataDir -force -Recurse - Write-Host "Copying metadata" - Copy-Item -Path $Script:defaultEmbyDataDir/metadata -Destination $Script:JellyfinDataDir -force -Recurse - Write-Host "Copying root dir" - Copy-Item -Path $Script:defaultEmbyDataDir/root -Destination $Script:JellyfinDataDir -force -Recurse - } - $progressbar.Value = 80 - if($Script:CreateShortcut){ - Write-Host "Creating Shortcut" - $WshShell = New-Object -comObject WScript.Shell - $Shortcut = $WshShell.CreateShortcut("$Home\Desktop\Jellyfin.lnk") - $Shortcut.TargetPath = "$Script:DefaultJellyfinInstallDirectory\jellyfin.exe" - $Shortcut.Save() - } - $ProgressBar.Value = 90 - if($Script:StartJellyfin){ - if($Script:InstallAsService){ - Write-Host "Starting Jellyfin Service" - Get-Service Jellyfin | Start-Service - }else{ - Write-Host "Starting Jellyfin" - Start-Process -FilePath $Script:DefaultJellyfinInstallDirectory\jellyfin.exe -PassThru - } - } - $progressbar.Value = 100 - Write-Host Finished - $wshell = New-Object -ComObject Wscript.Shell - $wshell.Popup("Operation Completed",0,"Done",0x1) - $InstallForm.Close() -} -function ServiceBoxCheckChanged { - if($InstallAsServiceCheck.Checked){ - $Script:InstallAsService = $true - $ServiceUserLabel.Visible = $true - $ServiceUserLabel.Enabled = $true - $ServiceUserBox.Visible = $true - $ServiceUserBox.Enabled = $true - }else{ - $Script:InstallAsService = $false - $ServiceUserLabel.Visible = $false - $ServiceUserLabel.Enabled = $false - $ServiceUserBox.Visible = $false - $ServiceUserBox.Enabled = $false - } -} -function UserSelect { - if($ServiceUserBox.Text -eq 'Local System') - { - $Script:InstallServiceAsUser = $false - $Script:UserCredentials = $null - $ServiceUserBox.Items.RemoveAt(1) - $ServiceUserBox.Items.Add("Custom User") - }elseif($ServiceUserBox.Text -eq 'Custom User'){ - $Script:InstallServiceAsUser = $true - $Script:UserCredentials = Get-Credential -Message "Please enter the credentials of the user you with to run Jellyfin Service as" -UserName $env:USERNAME - $ServiceUserBox.Items[1] = "$($Script:UserCredentials.UserName)" - } -} -function CreateShortcutBoxCheckChanged { - if($CreateShortcutCheck.Checked){ - $Script:CreateShortcut = $true - }else{ - $Script:CreateShortcut = $False - } -} -function StartJellyFinBoxCheckChanged { - if($StartProgramCheck.Checked){ - $Script:StartJellyfin = $true - }else{ - $Script:StartJellyfin = $false - } -} - -function CustomLibraryCheckChanged { - if($CustomLibraryCheck.Checked){ - $Script:UseCustomLibrary = $true - $CustomLibraryBox.Enabled = $true - }else{ - $Script:UseCustomLibrary = $false - $CustomLibraryBox.Enabled = $false - } -} - -function MigrateLibraryCheckboxChanged { - - if($MigrateLibraryCheck.Checked){ - $Script:MigrateLibrary = $true - $LibraryMigrationLabel.Visible = $true - $LibraryMigrationLabel.Enabled = $true - $LibraryLocationBox.Visible = $true - $LibraryLocationBox.Enabled = $true - }else{ - $Script:MigrateLibrary = $false - $LibraryMigrationLabel.Visible = $false - $LibraryMigrationLabel.Enabled = $false - $LibraryLocationBox.Visible = $false - $LibraryLocationBox.Enabled = $false - } - -} - - -#region begin GUI{ - -$InstallForm = New-Object system.Windows.Forms.Form -$InstallForm.ClientSize = '320,240' -$InstallForm.text = "Terrible Jellyfin Installer" -$InstallForm.TopMost = $false - -$GUIElementsCollection = @() - -$InstallButton = New-Object system.Windows.Forms.Button -$InstallButton.text = "Install" -$InstallButton.width = 60 -$InstallButton.height = 30 -$InstallButton.location = New-Object System.Drawing.Point(5,5) -$InstallButton.Font = 'Microsoft Sans Serif,10' -$GUIElementsCollection += $InstallButton - -$ProgressBar = New-Object system.Windows.Forms.ProgressBar -$ProgressBar.width = 245 -$ProgressBar.height = 30 -$ProgressBar.location = New-Object System.Drawing.Point(70,5) - -$InstallLocationLabel = New-Object system.Windows.Forms.Label -$InstallLocationLabel.text = "Install Location" -$InstallLocationLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft -$InstallLocationLabel.AutoSize = $true -$InstallLocationLabel.width = 100 -$InstallLocationLabel.height = 20 -$InstallLocationLabel.location = New-Object System.Drawing.Point(5,50) -$InstallLocationLabel.Font = 'Microsoft Sans Serif,10' -$GUIElementsCollection += $InstallLocationLabel - -$InstallLocationBox = New-Object system.Windows.Forms.TextBox -$InstallLocationBox.multiline = $false -$InstallLocationBox.width = 205 -$InstallLocationBox.height = 20 -$InstallLocationBox.location = New-Object System.Drawing.Point(110,50) -$InstallLocationBox.Text = $Script:DefaultJellyfinInstallDirectory -$InstallLocationBox.Font = 'Microsoft Sans Serif,10' -$GUIElementsCollection += $InstallLocationBox - -$CustomLibraryCheck = New-Object system.Windows.Forms.CheckBox -$CustomLibraryCheck.text = "Custom Library Location:" -$CustomLibraryCheck.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft -$CustomLibraryCheck.AutoSize = $false -$CustomLibraryCheck.width = 180 -$CustomLibraryCheck.height = 20 -$CustomLibraryCheck.location = New-Object System.Drawing.Point(5,75) -$CustomLibraryCheck.Font = 'Microsoft Sans Serif,10' -$GUIElementsCollection += $CustomLibraryCheck - -$CustomLibraryBox = New-Object system.Windows.Forms.TextBox -$CustomLibraryBox.multiline = $false -$CustomLibraryBox.width = 130 -$CustomLibraryBox.height = 20 -$CustomLibraryBox.location = New-Object System.Drawing.Point(185,75) -$CustomLibraryBox.Text = $Script:JellyFinDataDir -$CustomLibraryBox.Font = 'Microsoft Sans Serif,10' -$CustomLibraryBox.Enabled = $false -$GUIElementsCollection += $CustomLibraryBox - -$InstallAsServiceCheck = New-Object system.Windows.Forms.CheckBox -$InstallAsServiceCheck.text = "Install as Service" -$InstallAsServiceCheck.AutoSize = $false -$InstallAsServiceCheck.width = 140 -$InstallAsServiceCheck.height = 20 -$InstallAsServiceCheck.location = New-Object System.Drawing.Point(5,125) -$InstallAsServiceCheck.Font = 'Microsoft Sans Serif,10' -$GUIElementsCollection += $InstallAsServiceCheck - -$ServiceUserLabel = New-Object system.Windows.Forms.Label -$ServiceUserLabel.text = "Run Service As:" -$ServiceUserLabel.AutoSize = $true -$ServiceUserLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft -$ServiceUserLabel.width = 100 -$ServiceUserLabel.height = 20 -$ServiceUserLabel.location = New-Object System.Drawing.Point(15,145) -$ServiceUserLabel.Font = 'Microsoft Sans Serif,10' -$ServiceUserLabel.Visible = $false -$ServiceUserLabel.Enabled = $false -$GUIElementsCollection += $ServiceUserLabel - -$ServiceUserBox = New-Object system.Windows.Forms.ComboBox -$ServiceUserBox.text = "Run Service As" -$ServiceUserBox.width = 195 -$ServiceUserBox.height = 20 -@('Local System','Custom User') | ForEach-Object {[void] $ServiceUserBox.Items.Add($_)} -$ServiceUserBox.location = New-Object System.Drawing.Point(120,145) -$ServiceUserBox.Font = 'Microsoft Sans Serif,10' -$ServiceUserBox.Visible = $false -$ServiceUserBox.Enabled = $false -$ServiceUserBox.DropDownStyle = [System.Windows.Forms.ComboBoxStyle]::DropDownList -$GUIElementsCollection += $ServiceUserBox - -$MigrateLibraryCheck = New-Object system.Windows.Forms.CheckBox -$MigrateLibraryCheck.text = "Import Emby/Old JF Library" -$MigrateLibraryCheck.AutoSize = $false -$MigrateLibraryCheck.width = 160 -$MigrateLibraryCheck.height = 20 -$MigrateLibraryCheck.location = New-Object System.Drawing.Point(5,170) -$MigrateLibraryCheck.Font = 'Microsoft Sans Serif,10' -$GUIElementsCollection += $MigrateLibraryCheck - -$LibraryMigrationLabel = New-Object system.Windows.Forms.Label -$LibraryMigrationLabel.text = "Emby/Old JF Library Path" -$LibraryMigrationLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleLeft -$LibraryMigrationLabel.AutoSize = $false -$LibraryMigrationLabel.width = 120 -$LibraryMigrationLabel.height = 20 -$LibraryMigrationLabel.location = New-Object System.Drawing.Point(15,190) -$LibraryMigrationLabel.Font = 'Microsoft Sans Serif,10' -$LibraryMigrationLabel.Visible = $false -$LibraryMigrationLabel.Enabled = $false -$GUIElementsCollection += $LibraryMigrationLabel - -$LibraryLocationBox = New-Object system.Windows.Forms.TextBox -$LibraryLocationBox.multiline = $false -$LibraryLocationBox.width = 175 -$LibraryLocationBox.height = 20 -$LibraryLocationBox.location = New-Object System.Drawing.Point(140,190) -$LibraryLocationBox.Text = $Script:defaultEmbyDataDir -$LibraryLocationBox.Font = 'Microsoft Sans Serif,10' -$LibraryLocationBox.Visible = $false -$LibraryLocationBox.Enabled = $false -$GUIElementsCollection += $LibraryLocationBox - -$CreateShortcutCheck = New-Object system.Windows.Forms.CheckBox -$CreateShortcutCheck.text = "Desktop Shortcut" -$CreateShortcutCheck.AutoSize = $false -$CreateShortcutCheck.width = 150 -$CreateShortcutCheck.height = 20 -$CreateShortcutCheck.location = New-Object System.Drawing.Point(5,215) -$CreateShortcutCheck.Font = 'Microsoft Sans Serif,10' -$GUIElementsCollection += $CreateShortcutCheck - -$StartProgramCheck = New-Object system.Windows.Forms.CheckBox -$StartProgramCheck.text = "Start Jellyfin" -$StartProgramCheck.AutoSize = $false -$StartProgramCheck.width = 160 -$StartProgramCheck.height = 20 -$StartProgramCheck.location = New-Object System.Drawing.Point(160,215) -$StartProgramCheck.Font = 'Microsoft Sans Serif,10' -$GUIElementsCollection += $StartProgramCheck - -$InstallForm.controls.AddRange($GUIElementsCollection) -$InstallForm.Controls.Add($ProgressBar) - -#region gui events { -$InstallButton.Add_Click({ InstallJellyfin }) -$CustomLibraryCheck.Add_CheckedChanged({CustomLibraryCheckChanged}) -$InstallAsServiceCheck.Add_CheckedChanged({ServiceBoxCheckChanged}) -$ServiceUserBox.Add_SelectedValueChanged({ UserSelect }) -$MigrateLibraryCheck.Add_CheckedChanged({MigrateLibraryCheckboxChanged}) -$CreateShortcutCheck.Add_CheckedChanged({CreateShortcutBoxCheckChanged}) -$StartProgramCheck.Add_CheckedChanged({StartJellyFinBoxCheckChanged}) -#endregion events } - -#endregion GUI } - - -[void]$InstallForm.ShowDialog() diff --git a/deployment/windows/legacy/install.bat b/deployment/windows/legacy/install.bat deleted file mode 100644 index e21479a79..000000000 --- a/deployment/windows/legacy/install.bat +++ /dev/null @@ -1 +0,0 @@ -powershell.exe -executionpolicy Bypass -file install-jellyfin.ps1 diff --git a/nuget.config b/nuget.config new file mode 100644 index 000000000..326331f32 --- /dev/null +++ b/nuget.config @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<configuration> + <packageSources> + <add key="NuGet official package source" value="https://api.nuget.org/v3/index.json" /> + </packageSources> +</configuration> diff --git a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs index 84cdbe360..e40af703f 100644 --- a/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/FirstTimeSetupOrElevatedPolicy/FirstTimeSetupOrElevatedHandlerTests.cs @@ -23,7 +23,7 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy { var fixture = new Fixture().Customize(new AutoMoqCustomization()); _configurationManagerMock = fixture.Freeze<Mock<IConfigurationManager>>(); - _requirements = new List<IAuthorizationRequirement> {new FirstTimeSetupOrElevatedRequirement()}; + _requirements = new List<IAuthorizationRequirement> { new FirstTimeSetupOrElevatedRequirement() }; _sut = fixture.Create<FirstTimeSetupOrElevatedHandler>(); } @@ -58,7 +58,7 @@ namespace Jellyfin.Api.Tests.Auth.FirstTimeSetupOrElevatedPolicy private static ClaimsPrincipal SetupUser(string role) { - var claims = new[] {new Claim(ClaimTypes.Role, role)}; + var claims = new[] { new Claim(ClaimTypes.Role, role) }; var identity = new ClaimsIdentity(claims); return new ClaimsPrincipal(identity); } diff --git a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs index e2beea1ad..cd05a8328 100644 --- a/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs +++ b/tests/Jellyfin.Api.Tests/Auth/RequiresElevationPolicy/RequiresElevationHandlerTests.cs @@ -23,9 +23,9 @@ namespace Jellyfin.Api.Tests.Auth.RequiresElevationPolicy [InlineData(UserRoles.Guest, false)] public async Task ShouldHandleRolesCorrectly(string role, bool shouldSucceed) { - var requirements = new List<IAuthorizationRequirement> {new RequiresElevationRequirement()}; + var requirements = new List<IAuthorizationRequirement> { new RequiresElevationRequirement() }; - var claims = new[] {new Claim(ClaimTypes.Role, role)}; + var claims = new[] { new Claim(ClaimTypes.Role, role) }; var identity = new ClaimsIdentity(claims); var user = new ClaimsPrincipal(identity); diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index 1d7e4f7af..b159db2bd 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -11,11 +11,11 @@ <PackageReference Include="AutoFixture" Version="4.11.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.11.0" /> <PackageReference Include="AutoFixture.Xunit2" Version="4.11.0" /> - <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.1" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" /> + <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.3" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> - <PackageReference Include="coverlet.collector" Version="1.2.0" /> + <PackageReference Include="coverlet.collector" Version="1.2.1" /> <PackageReference Include="Moq" Version="4.13.1" /> </ItemGroup> diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj index 86bb11bd4..81a2242e7 100644 --- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj +++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj @@ -8,10 +8,10 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> - <PackageReference Include="coverlet.collector" Version="1.2.0" /> + <PackageReference Include="coverlet.collector" Version="1.2.1" /> </ItemGroup> <ItemGroup> diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj index c63f2e8c6..30994dee6 100644 --- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj +++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj @@ -8,10 +8,10 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> - <PackageReference Include="coverlet.collector" Version="1.2.0" /> + <PackageReference Include="coverlet.collector" Version="1.2.1" /> </ItemGroup> <ItemGroup> diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj index b5e4a1287..78a020ad5 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj @@ -14,10 +14,10 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> - <PackageReference Include="coverlet.collector" Version="1.2.0" /> + <PackageReference Include="coverlet.collector" Version="1.2.1" /> </ItemGroup> <ItemGroup> diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj index 9602d9e58..f404b3e46 100644 --- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj +++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj @@ -7,10 +7,10 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> - <PackageReference Include="coverlet.collector" Version="1.2.0" /> + <PackageReference Include="coverlet.collector" Version="1.2.1" /> </ItemGroup> <ItemGroup> diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs index a2ef2dcd6..49cb2387b 100644 --- a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs @@ -39,11 +39,11 @@ namespace Jellyfin.Naming.Tests.Video [InlineData(@"[rec].mkv", "[rec].mkv", null)] [InlineData(@"St. Vincent (2014)", "St. Vincent", 2014)] [InlineData("Super movie(2009).mp4", "Super movie", 2009)] - // FIXME: [InlineData("Drug War 2013.mp4", "Drug War", 2013)] + [InlineData("Drug War 2013.mp4", "Drug War", 2013)] [InlineData("My Movie (1997) - GreatestReleaseGroup 2019.mp4", "My Movie", 1997)] - // FIXME: [InlineData("First Man 2018 1080p.mkv", "First Man", 2018)] + [InlineData("First Man 2018 1080p.mkv", "First Man", 2018)] [InlineData("First Man (2018) 1080p.mkv", "First Man", 2018)] - // FIXME: [InlineData("Maximum Ride - 2016 - WEBDL-1080p - x264 AC3.mkv", "Maximum Ride", 2016)] + [InlineData("Maximum Ride - 2016 - WEBDL-1080p - x264 AC3.mkv", "Maximum Ride", 2016)] // FIXME: [InlineData("Robin Hood [Multi-Subs] [2018].mkv", "Robin Hood", 2018)] [InlineData(@"3.Days.to.Kill.2014.720p.BluRay.x264.YIFY.mkv", "3.Days.to.Kill", 2014)] // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again public void CleanDateTimeTest(string input, string expectedName, int? expectedYear) diff --git a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs index 1646237a0..a64d17349 100644 --- a/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/ExtraTests.cs @@ -7,6 +7,8 @@ namespace Jellyfin.Naming.Tests.Video { public class ExtraTests : BaseVideoTest { + private readonly NamingOptions _videoOptions = new NamingOptions(); + // Requirements // movie-deleted = ExtraType deletedscene @@ -15,42 +17,64 @@ namespace Jellyfin.Naming.Tests.Video [Fact] public void TestKodiExtras() { - var videoOptions = new NamingOptions(); - - Test("trailer.mp4", ExtraType.Trailer, videoOptions); - Test("300-trailer.mp4", ExtraType.Trailer, videoOptions); + Test("trailer.mp4", ExtraType.Trailer, _videoOptions); + Test("300-trailer.mp4", ExtraType.Trailer, _videoOptions); - Test("theme.mp3", ExtraType.ThemeSong, videoOptions); + Test("theme.mp3", ExtraType.ThemeSong, _videoOptions); } [Fact] public void TestExpandedExtras() { - var videoOptions = new NamingOptions(); + Test("trailer.mp4", ExtraType.Trailer, _videoOptions); + Test("trailer.mp3", null, _videoOptions); + Test("300-trailer.mp4", ExtraType.Trailer, _videoOptions); - Test("trailer.mp4", ExtraType.Trailer, videoOptions); - Test("trailer.mp3", null, videoOptions); - Test("300-trailer.mp4", ExtraType.Trailer, videoOptions); + Test("theme.mp3", ExtraType.ThemeSong, _videoOptions); + Test("theme.mkv", null, _videoOptions); - Test("theme.mp3", ExtraType.ThemeSong, videoOptions); - Test("theme.mkv", null, videoOptions); + Test("300-scene.mp4", ExtraType.Scene, _videoOptions); + Test("300-scene2.mp4", ExtraType.Scene, _videoOptions); + Test("300-clip.mp4", ExtraType.Clip, _videoOptions); + + Test("300-deleted.mp4", ExtraType.DeletedScene, _videoOptions); + Test("300-deletedscene.mp4", ExtraType.DeletedScene, _videoOptions); + Test("300-interview.mp4", ExtraType.Interview, _videoOptions); + Test("300-behindthescenes.mp4", ExtraType.BehindTheScenes, _videoOptions); + } - Test("300-scene.mp4", ExtraType.Scene, videoOptions); - Test("300-scene2.mp4", ExtraType.Scene, videoOptions); - Test("300-clip.mp4", ExtraType.Clip, videoOptions); + [Theory] + [InlineData(ExtraType.BehindTheScenes, "behind the scenes" )] + [InlineData(ExtraType.DeletedScene, "deleted scenes" )] + [InlineData(ExtraType.Interview, "interviews" )] + [InlineData(ExtraType.Scene, "scenes" )] + [InlineData(ExtraType.Sample, "samples" )] + [InlineData(ExtraType.Clip, "shorts" )] + [InlineData(ExtraType.Clip, "featurettes" )] + [InlineData(ExtraType.Unknown, "extras" )] + public void TestDirectories(ExtraType type, string dirName) + { + Test(dirName + "/300.mp4", type, _videoOptions); + Test("300/" + dirName + "/something.mkv", type, _videoOptions); + Test("/data/something/Movies/300/" + dirName + "/whoknows.mp4", type, _videoOptions); + } - Test("300-deleted.mp4", ExtraType.DeletedScene, videoOptions); - Test("300-deletedscene.mp4", ExtraType.DeletedScene, videoOptions); - Test("300-interview.mp4", ExtraType.Interview, videoOptions); - Test("300-behindthescenes.mp4", ExtraType.BehindTheScenes, videoOptions); + [Theory] + [InlineData("gibberish")] + [InlineData("not a scene")] + [InlineData("The Big Short")] + public void TestNonExtraDirectories(string dirName) + { + Test(dirName + "/300.mp4", null, _videoOptions); + Test("300/" + dirName + "/something.mkv", null, _videoOptions); + Test("/data/something/Movies/300/" + dirName + "/whoknows.mp4", null, _videoOptions); + Test("/data/something/Movies/" + dirName + "/" + dirName + ".mp4", null, _videoOptions); } [Fact] public void TestSample() { - var videoOptions = new NamingOptions(); - - Test("300-sample.mp4", ExtraType.Sample, videoOptions); + Test("300-sample.mp4", ExtraType.Sample, _videoOptions); } private void Test(string input, ExtraType? expectedType, NamingOptions videoOptions) diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index 29733a1c4..b7865439c 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -14,7 +14,7 @@ <PackageReference Include="Moq" Version="4.13.1" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> - <PackageReference Include="coverlet.collector" Version="1.2.0" /> + <PackageReference Include="coverlet.collector" Version="1.2.1" /> </ItemGroup> <ItemGroup> |
