diff options
550 files changed, 10190 insertions, 16342 deletions
diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml index 46db0d9fe..46c478b08 100644 --- a/.ci/azure-pipelines.yml +++ b/.ci/azure-pipelines.yml @@ -2,7 +2,7 @@ name: $(Date:yyyyMMdd)$(Rev:.r) variables: - name: TestProjects - value: 'Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj' + value: 'tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj' - name: RestoreBuildProjects value: 'Jellyfin.Server/Jellyfin.Server.csproj' @@ -28,29 +28,43 @@ jobs: - checkout: self clean: true submodules: true - persistCredentials: false + persistCredentials: true - - task: DotNetCoreCLI@2 - displayName: Restore + - task: CmdLine@2 + displayName: "Check out web" + 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: - command: restore - projects: '$(RestoreBuildProjects)' - enabled: false + script: 'git clone --single-branch --branch $(Build.SourceBranchName) --depth=1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web' - - task: DotNetCoreCLI@2 - displayName: Build + - task: CmdLine@2 + displayName: "Check out web (PR)" + 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: - projects: '$(RestoreBuildProjects)' - arguments: '--configuration $(BuildConfiguration)' - enabled: false + script: 'git clone --single-branch --branch $(System.PullRequest.TargetBranch) --depth 1 https://github.com/jellyfin/jellyfin-web.git $(Agent.TempDirectory)/jellyfin-web' - - task: DotNetCoreCLI@2 - displayName: Test + - task: NodeTool@0 + displayName: 'Install Node.js' + 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: - command: test - projects: '$(RestoreBuildProjects)' - arguments: '--configuration $(BuildConfiguration)' - enabled: false + versionSpec: '10.x' + + - task: CmdLine@2 + displayName: "Build Web UI" + 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 + workingDirectory: $(Agent.TempDirectory)/jellyfin-web + + - task: CopyFiles@2 + displayName: Copy the web UI + 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: + sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist # Optional + contents: '**' + targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web + cleanTargetFolder: true # Optional + overWrite: true # Optional + flattenFolders: false # Optional - task: DotNetCoreCLI@2 displayName: Publish @@ -61,13 +75,6 @@ jobs: arguments: '--configuration $(BuildConfiguration) --output $(build.artifactstagingdirectory)' zipAfterPublish: false - # - task: PublishBuildArtifacts@1 - # displayName: 'Publish Artifact' - # inputs: - # PathtoPublish: '$(build.artifactstagingdirectory)' - # artifactName: 'jellyfin-build-$(BuildConfiguration)' - # zipAfterPublish: true - - task: PublishPipelineArtifact@0 displayName: 'Publish Artifact Naming' condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded()) @@ -96,6 +103,173 @@ jobs: targetPath: '$(build.artifactstagingdirectory)/Jellyfin.Server/MediaBrowser.Common.dll' artifactName: 'Jellyfin.Common' + - job: main_test + displayName: Main Test + pool: + vmImage: windows-latest + steps: + - checkout: self + clean: true + submodules: true + persistCredentials: false + + - task: DotNetCoreCLI@2 + displayName: Build + inputs: + command: build + publishWebProjects: false + projects: '$(TestProjects)' + arguments: '--configuration $(BuildConfiguration)' + zipAfterPublish: false + + - task: VisualStudioTestPlatformInstaller@1 + inputs: + packageFeedSelector: 'nugetOrg' # Options: nugetOrg, customFeed, netShare + versionSelector: 'latestPreRelease' # Required when packageFeedSelector == NugetOrg || PackageFeedSelector == CustomFeed# Options: latestPreRelease, latestStable, specificVersion + + - task: VSTest@2 + inputs: + testSelector: 'testAssemblies' # Options: testAssemblies, testPlan, testRun + testAssemblyVer2: | # Required when testSelector == TestAssemblies + **\bin\$(BuildConfiguration)\**\*test*.dll + !**\obj\** + !**\xunit.runner.visualstudio.testadapter.dll + !**\xunit.runner.visualstudio.dotnetcore.testadapter.dll + #testPlan: # Required when testSelector == TestPlan + #testSuite: # Required when testSelector == TestPlan + #testConfiguration: # Required when testSelector == TestPlan + #tcmTestRun: '$(test.RunId)' # Optional + searchFolder: '$(System.DefaultWorkingDirectory)' + #testFiltercriteria: # Optional + #runOnlyImpactedTests: False # Optional + #runAllTestsAfterXBuilds: '50' # Optional + #uiTests: false # Optional + #vstestLocationMethod: 'version' # Optional. Options: version, location + #vsTestVersion: 'latest' # Optional. Options: latest, 16.0, 15.0, 14.0, toolsInstaller + #vstestLocation: # Optional + #runSettingsFile: # Optional + #overrideTestrunParameters: # Optional + #pathtoCustomTestAdapters: # Optional + runInParallel: True # Optional + runTestsInIsolation: True # Optional + codeCoverageEnabled: True # Optional + #otherConsoleOptions: # Optional + #distributionBatchType: 'basedOnTestCases' # Optional. Options: basedOnTestCases, basedOnExecutionTime, basedOnAssembly + #batchingBasedOnAgentsOption: 'autoBatchSize' # Optional. Options: autoBatchSize, customBatchSize + #customBatchSizeValue: '10' # Required when distributionBatchType == BasedOnTestCases && BatchingBasedOnAgentsOption == CustomBatchSize + #batchingBasedOnExecutionTimeOption: 'autoBatchSize' # Optional. Options: autoBatchSize, customTimeBatchSize + #customRunTimePerBatchValue: '60' # Required when distributionBatchType == BasedOnExecutionTime && BatchingBasedOnExecutionTimeOption == CustomTimeBatchSize + #dontDistribute: False # Optional + #testRunTitle: # Optional + #platform: # Optional + configuration: 'Debug' # Optional + publishRunAttachments: true # Optional + #diagnosticsEnabled: false # Optional + #collectDumpOn: 'onAbortOnly' # Optional. Options: onAbortOnly, always, never + #rerunFailedTests: False # Optional + #rerunType: 'basedOnTestFailurePercentage' # Optional. Options: basedOnTestFailurePercentage, basedOnTestFailureCount + #rerunFailedThreshold: '30' # Optional + #rerunFailedTestCasesMaxLimit: '5' # Optional + #rerunMaxAttempts: '3' # Optional + + # - task: PublishTestResults@2 + # inputs: + # testResultsFormat: 'VSTest' # Options: JUnit, NUnit, VSTest, xUnit, cTest + # testResultsFiles: '**/*.trx' + # #searchFolder: '$(System.DefaultWorkingDirectory)' # Optional + # mergeTestResults: true # Optional + # #failTaskOnFailedTests: false # Optional + # #testRunTitle: # Optional + # #buildPlatform: # Optional + # #buildConfiguration: # Optional + # #publishRunAttachments: true # Optional + + - job: main_build_win + displayName: Main Build Windows + pool: + vmImage: windows-latest + strategy: + matrix: + release: + BuildConfiguration: Release + maxParallel: 2 + steps: + - checkout: self + clean: true + submodules: true + persistCredentials: true + + - task: CmdLine@2 + displayName: "Check out web" + 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: "Check out web (PR)" + 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' + + - task: NodeTool@0 + displayName: 'Install Node.js' + 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' + + - task: CmdLine@2 + displayName: "Build Web UI" + 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 + workingDirectory: $(Agent.TempDirectory)/jellyfin-web + + - task: CopyFiles@2 + displayName: Copy the web UI + 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: + sourceFolder: $(Agent.TempDirectory)/jellyfin-web/dist # Optional + contents: '**' + targetFolder: $(Build.SourcesDirectory)/MediaBrowser.WebDashboard/jellyfin-web + cleanTargetFolder: true # Optional + overWrite: true # Optional + flattenFolders: false # Optional + + - task: CmdLine@2 + displayName: Clone the UX repository + inputs: + script: git clone --depth=1 https://github.com/jellyfin/jellyfin-ux $(Agent.TempDirectory)\jellyfin-ux + + - task: PowerShell@2 + displayName: Build the NSIS Installer + inputs: + targetType: 'filePath' # Optional. Options: filePath, inline + filePath: ./deployment/windows/build-jellyfin.ps1 # Required when targetType == FilePath + arguments: -InstallFFMPEG -InstallNSSM -MakeNSIS -UXLocation $(Agent.TempDirectory)\jellyfin-ux -InstallLocation $(build.artifactstagingdirectory) + #script: '# Write your PowerShell commands here.Write-Host Hello World' # Required when targetType == Inline + errorActionPreference: 'stop' # Optional. Options: stop, continue, silentlyContinue + #failOnStderr: false # Optional + #ignoreLASTEXITCODE: false # Optional + #pwsh: false # Optional + workingDirectory: $(Build.SourcesDirectory) # Optional + + - task: CopyFiles@2 + displayName: Copy the NSIS Installer to the artifact directory + inputs: + sourceFolder: $(Build.SourcesDirectory)/deployment/windows/ # Optional + contents: 'jellyfin*.exe' + targetFolder: $(System.ArtifactsDirectory)/setup + cleanTargetFolder: true # Optional + overWrite: true # Optional + flattenFolders: true # Optional + + - task: PublishPipelineArtifact@0 + displayName: 'Publish Setup Artifact' + condition: and(eq(variables['BuildConfiguration'], 'Release'), succeeded()) + inputs: + targetPath: '$(build.artifactstagingdirectory)/setup' + artifactName: 'Jellyfin Server Setup' + - job: dotnet_compat displayName: Compatibility Check pool: diff --git a/.ci/publish-nightly.yml b/.ci/publish-nightly.yml new file mode 100644 index 000000000..a693e10f6 --- /dev/null +++ b/.ci/publish-nightly.yml @@ -0,0 +1,46 @@ +name: Nightly-$(date:yyyyMMdd).$(rev:r) + +variables: + - name: Version + value: '1.0.0' + +trigger: none +pr: none + +jobs: + - job: publish_artifacts_nightly + displayName: Publish Artifacts Nightly + pool: + vmImage: ubuntu-latest + steps: + - checkout: none + - task: DownloadPipelineArtifact@2 + displayName: Download the Windows Setup Artifact + inputs: + source: 'specific' # Options: current, specific + artifact: 'Jellyfin Server Setup' # Optional + path: '$(System.ArtifactsDirectory)/win-installer' + project: '$(System.TeamProjectId)' # Required when source == Specific + pipelineId: 1 # Required when source == Specific + runVersion: 'latestFromBranch' # Required when source == Specific. Options: latest, latestFromBranch, specific + runBranch: 'refs/heads/master' # Required when source == Specific && runVersion == LatestFromBranch + + - task: SSH@0 + displayName: 'Create Drop directory' + inputs: + sshEndpoint: 'Jellyfin Build Server' + commands: 'mkdir -p /srv/incoming/jellyfin_$(Version)/win-installer && ln -s /srv/incoming/jellyfin_$(Version) /srv/incoming/jellyfin_nightly_azure_upload' + + - task: CopyFilesOverSSH@0 + displayName: 'Copy the Windows Setup to the Repo' + inputs: + sshEndpoint: 'Jellyfin Build Server' + sourceFolder: '$(System.ArtifactsDirectory)/win-installer' + contents: 'jellyfin_*.exe' + targetFolder: '/srv/incoming/jellyfin_nightly_azure_upload/win-installer' + + - task: SSH@0 + displayName: 'Clean up SCP symlink' + inputs: + sshEndpoint: 'Jellyfin Build Server' + commands: 'rm -f /srv/incoming/jellyfin_nightly_azure_upload' diff --git a/.ci/publish-release.yml b/.ci/publish-release.yml new file mode 100644 index 000000000..57e77ae5a --- /dev/null +++ b/.ci/publish-release.yml @@ -0,0 +1,48 @@ +name: Release-$(Version)-$(date:yyyyMMdd).$(rev:r) + +variables: + - name: Version + value: '1.0.0' + - name: UsedRunId + value: 0 + +trigger: none +pr: none + +jobs: + - job: publish_artifacts_release + displayName: Publish Artifacts Release + pool: + vmImage: ubuntu-latest + steps: + - checkout: none + - task: DownloadPipelineArtifact@2 + displayName: Download the Windows Setup Artifact + inputs: + source: 'specific' # Options: current, specific + artifact: 'Jellyfin Server Setup' # Optional + path: '$(System.ArtifactsDirectory)/win-installer' + project: '$(System.TeamProjectId)' # Required when source == Specific + pipelineId: 1 # Required when source == Specific + runVersion: 'specific' # Required when source == Specific. Options: latest, latestFromBranch, specific + runId: $(UsedRunId) + + - task: SSH@0 + displayName: 'Create Drop directory' + inputs: + sshEndpoint: 'Jellyfin Build Server' + commands: 'mkdir -p /srv/incoming/jellyfin_$(Version)/win-installer && ln -s /srv/incoming/jellyfin_$(Version) /srv/incoming/jellyfin_release_azure_upload' + + - task: CopyFilesOverSSH@0 + displayName: 'Copy the Windows Setup to the Repo' + inputs: + sshEndpoint: 'Jellyfin Build Server' + sourceFolder: '$(System.ArtifactsDirectory)/win-installer' + contents: 'jellyfin_*.exe' + targetFolder: '/srv/incoming/jellyfin_release_azure_upload/win-installer' + + - task: SSH@0 + displayName: 'Clean up SCP symlink' + inputs: + sshEndpoint: 'Jellyfin Build Server' + commands: 'rm -f /srv/incoming/jellyfin_release_azure_upload' diff --git a/.gitattributes b/.gitattributes index d78b0459d..8ae599725 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,5 @@ * text=auto eol=lf +*.png binary +*.jpg binary CONTRIBUTORS.md merge=union diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 967be0fb7..dc93d2c84 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,6 +1,6 @@ <!-- Ensure your title is short, descriptive, and in the imperative mood (Fix X, Change Y, instead of Fixed X, Changed Y). -For a good inspiration of what to write in commit messages and PRs please review https://chris.beams.io/posts/git-commit/ and our https://jellyfin.readthedocs.io/en/latest/developer-docs/contributing/ page. +For a good inspiration of what to write in commit messages and PRs please review https://chris.beams.io/posts/git-commit/ and our documentation. --> **Changes** diff --git a/.github/stale.yml b/.github/stale.yml index 011f76317..9ea0e3796 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,7 +1,7 @@ # Number of days of inactivity before an issue becomes stale -daysUntilStale: 60 +daysUntilStale: 90 # Number of days of inactivity before a stale issue is closed -daysUntilClose: 7 +daysUntilClose: 14 # Issues with these labels will never be considered stale exemptLabels: - regression @@ -15,8 +15,8 @@ exemptLabels: staleLabel: stale # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > - Issues go stale after 60d of inactivity. Mark the issue as fresh by adding a comment or commit. Stale issues close after an additional 7d of inactivity. + Issues go stale after 90d of inactivity. Mark the issue as fresh by adding a comment or commit. Stale issues close after an additional 14d of inactivity. If this issue is safe to close now please do so. - If you have any questions you can reach us on [Matrix or Social Media](https://jellyfin.readthedocs.io/en/latest/getting-help/). + If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html). # Comment to post when closing a stale issue. Set to `false` to disable closeComment: false diff --git a/.gitignore b/.gitignore index 2ce41d76e..34cf1a84c 100644 --- a/.gitignore +++ b/.gitignore @@ -264,3 +264,7 @@ ci/ # Doxygen doc/ + +# Deployment artifacts +dist +*.exe diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 2b97b1331..000000000 --- a/.gitmodules +++ /dev/null @@ -1,4 +0,0 @@ -[submodule "MediaBrowser.WebDashboard/jellyfin-web"] - path = MediaBrowser.WebDashboard/jellyfin-web - url = https://github.com/jellyfin/jellyfin-web.git - branch = . diff --git a/.vscode/launch.json b/.vscode/launch.json index 21b8323ea..e2a09c0f1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp2.1/jellyfin.dll", + "program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/netcoreapp3.0/jellyfin.dll", "args": [], "cwd": "${workspaceFolder}/Jellyfin.Server", // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window @@ -25,4 +25,4 @@ "processId": "${command:pickProcess}" } ,] -}
\ No newline at end of file +} diff --git a/BDInfo/BDInfo.csproj b/BDInfo/BDInfo.csproj index b2c752d0c..9dbaa9e2f 100644 --- a/BDInfo/BDInfo.csproj +++ b/BDInfo/BDInfo.csproj @@ -11,6 +11,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> </Project> diff --git a/BDInfo/BDROM.cs b/BDInfo/BDROM.cs index 6759ed55a..3a0c14ffd 100644 --- a/BDInfo/BDROM.cs +++ b/BDInfo/BDROM.cs @@ -212,7 +212,6 @@ namespace BDInfo public void Scan() { - var errorStreamClipFiles = new List<TSStreamClipFile>(); foreach (var streamClipFile in StreamClipFiles.Values) { try @@ -221,7 +220,6 @@ namespace BDInfo } catch (Exception ex) { - errorStreamClipFiles.Add(streamClipFile); if (StreamClipFileScanError != null) { if (StreamClipFileScanError(streamClipFile, ex)) @@ -250,7 +248,6 @@ namespace BDInfo StreamFiles.Values.CopyTo(streamFiles, 0); Array.Sort(streamFiles, CompareStreamFiles); - var errorPlaylistFiles = new List<TSPlaylistFile>(); foreach (var playlistFile in PlaylistFiles.Values) { try @@ -259,7 +256,6 @@ namespace BDInfo } catch (Exception ex) { - errorPlaylistFiles.Add(playlistFile); if (PlaylistFileScanError != null) { if (PlaylistFileScanError(playlistFile, ex)) @@ -275,7 +271,6 @@ namespace BDInfo } } - var errorStreamFiles = new List<TSStreamFile>(); foreach (var streamFile in streamFiles) { try @@ -296,7 +291,6 @@ namespace BDInfo } catch (Exception ex) { - errorStreamFiles.Add(streamFile); if (StreamFileScanError != null) { if (StreamFileScanError(streamFile, ex)) @@ -431,7 +425,7 @@ namespace BDInfo { return 1; } - else if ((x != null || x.FileInfo != null) && (y == null || y.FileInfo == null)) + else if ((x != null && x.FileInfo != null) && (y == null || y.FileInfo == null)) { return -1; } @@ -451,6 +445,5 @@ namespace BDInfo } } } - } } diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index c96228f31..f22944a8b 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -28,6 +28,8 @@ - [DrPandemic](https://github.com/drpandemic) - [joern-h](https://github.com/joern-h) - [Khinenw](https://github.com/HelloWorld017) + - [fhriley](https://github.com/fhriley) + - [nevado](https://github.com/nevado) # Emby Contributors diff --git a/Dockerfile b/Dockerfile index 1644a8967..afa8152ff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,34 +1,48 @@ -ARG DOTNET_VERSION=2.2 +ARG DOTNET_VERSION=3.0 +ARG FFMPEG_VERSION=latest + +FROM node:alpine as web-builder +ARG JELLYFIN_WEB_VERSION=master +RUN apk add curl \ + && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ + && cd jellyfin-web-* \ + && yarn install \ + && yarn build \ + && mv dist /dist FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder WORKDIR /repo COPY . . ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 -RUN bash -c "source deployment/common.build.sh && \ - build_jellyfin Jellyfin.Server Release linux-x64 /jellyfin" +RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" + +FROM jellyfin/ffmpeg:${FFMPEG_VERSION} as ffmpeg +FROM debian:stretch-slim -FROM jellyfin/ffmpeg as ffmpeg -FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION} -# libfontconfig1 is required for Skia +COPY --from=ffmpeg /opt/ffmpeg /opt/ffmpeg +COPY --from=builder /jellyfin /jellyfin +COPY --from=web-builder /dist /jellyfin/jellyfin-web +# Install dependencies: +# libfontconfig1: needed for Skia +# libgomp1: needed for ffmpeg +# libva-drm2: needed for ffmpeg +# mesa-va-drivers: needed for VAAPI RUN apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y \ - libfontconfig1 \ + libfontconfig1 libgomp1 libva-drm2 mesa-va-drivers \ && apt-get clean autoclean \ && apt-get autoremove \ && rm -rf /var/lib/apt/lists/* \ && mkdir -p /cache /config /media \ - && chmod 777 /cache /config /media -COPY --from=ffmpeg / / -COPY --from=builder /jellyfin /jellyfin + && chmod 777 /cache /config /media \ + && ln -s /opt/ffmpeg/bin/ffmpeg /usr/local/bin \ + && ln -s /opt/ffmpeg/bin/ffprobe /usr/local/bin -ARG JELLYFIN_WEB_VERSION=v10.3.7 -RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ - && rm -rf /jellyfin/jellyfin-web \ - && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 EXPOSE 8096 VOLUME /cache /config /media -ENTRYPOINT dotnet /jellyfin/jellyfin.dll \ +ENTRYPOINT ./jellyfin/jellyfin \ --datadir /config \ --cachedir /cache \ --ffmpeg /usr/local/bin/ffmpeg diff --git a/Dockerfile.arm b/Dockerfile.arm index b0cf7a8a4..f8c8511ae 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -3,21 +3,28 @@ ARG DOTNET_VERSION=3.0 +FROM node:alpine as web-builder +ARG JELLYFIN_WEB_VERSION=master +RUN apk add curl \ + && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ + && cd jellyfin-web-* \ + && yarn install \ + && yarn build \ + && mv dist /dist + + FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder WORKDIR /repo COPY . . ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 -# TODO Remove or update the sed line when we update dotnet version. -RUN find . -type f -exec sed -i 's/netcoreapp2.1/netcoreapp3.0/g' {} \; # Discard objs - may cause failures if exists RUN find . -type d -name obj | xargs -r rm -r # Build -RUN bash -c "source deployment/common.build.sh && \ - build_jellyfin Jellyfin.Server Release linux-arm /jellyfin" +RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" FROM multiarch/qemu-user-static:x86_64-arm as qemu -FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}-stretch-slim-arm32v7 +FROM debian:stretch-slim-arm32v7 COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin RUN apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \ @@ -25,15 +32,13 @@ RUN apt-get update \ && mkdir -p /cache /config /media \ && chmod 777 /cache /config /media COPY --from=builder /jellyfin /jellyfin +COPY --from=web-builder /dist /jellyfin/jellyfin-web -ARG JELLYFIN_WEB_VERSION=v10.3.7 -RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ - && rm -rf /jellyfin/jellyfin-web \ - && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 EXPOSE 8096 VOLUME /cache /config /media -ENTRYPOINT dotnet /jellyfin/jellyfin.dll \ +ENTRYPOINT ./jellyfin/jellyfin \ --datadir /config \ --cachedir /cache \ --ffmpeg /usr/bin/ffmpeg diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index f8ff0612b..9b343659f 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -3,21 +3,28 @@ ARG DOTNET_VERSION=3.0 +FROM node:alpine as web-builder +ARG JELLYFIN_WEB_VERSION=master +RUN apk add curl \ + && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ + && cd jellyfin-web-* \ + && yarn install \ + && yarn build \ + && mv dist /dist + + FROM mcr.microsoft.com/dotnet/core/sdk:${DOTNET_VERSION} as builder WORKDIR /repo COPY . . ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 -# TODO Remove or update the sed line when we update dotnet version. -RUN find . -type f -exec sed -i 's/netcoreapp2.1/netcoreapp3.0/g' {} \; # Discard objs - may cause failures if exists RUN find . -type d -name obj | xargs -r rm -r # Build -RUN bash -c "source deployment/common.build.sh && \ - build_jellyfin Jellyfin.Server Release linux-arm64 /jellyfin" +RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu -FROM mcr.microsoft.com/dotnet/core/runtime:${DOTNET_VERSION}-stretch-slim-arm64v8 +FROM debian:stretch-slim-arm64v8 COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin RUN apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y ffmpeg \ @@ -25,15 +32,13 @@ RUN apt-get update \ && mkdir -p /cache /config /media \ && chmod 777 /cache /config /media COPY --from=builder /jellyfin /jellyfin +COPY --from=web-builder /dist /jellyfin/jellyfin-web -ARG JELLYFIN_WEB_VERSION=v10.3.7 -RUN curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ - && rm -rf /jellyfin/jellyfin-web \ - && mv jellyfin-web-${JELLYFIN_WEB_VERSION} /jellyfin/jellyfin-web +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 EXPOSE 8096 VOLUME /cache /config /media -ENTRYPOINT dotnet /jellyfin/jellyfin.dll \ +ENTRYPOINT ./jellyfin/jellyfin \ --datadir /config \ --cachedir /cache \ --ffmpeg /usr/bin/ffmpeg diff --git a/Doxyfile b/Doxyfile deleted file mode 100644 index c859737ca..000000000 --- a/Doxyfile +++ /dev/null @@ -1,2565 +0,0 @@ -# Doxyfile 1.8.15 - -# This file describes the settings to be used by the documentation system -# doxygen (www.doxygen.org) for a project. -# -# All text after a double hash (##) is considered a comment and is placed in -# front of the TAG it is preceding. -# -# All text after a single hash (#) is considered a comment and will be ignored. -# The format is: -# TAG = value [value, ...] -# For lists, items can also be appended using: -# TAG += value [value, ...] -# Values that contain spaces should be placed between quotes (\" \"). - -#--------------------------------------------------------------------------- -# Project related configuration options -#--------------------------------------------------------------------------- - -# This tag specifies the encoding used for all characters in the configuration -# file that follow. The default is UTF-8 which is also the encoding used for all -# text before the first occurrence of this tag. Doxygen uses libiconv (or the -# iconv built into libc) for the transcoding. See -# https://www.gnu.org/software/libiconv/ for the list of possible encodings. -# The default value is: UTF-8. - -DOXYFILE_ENCODING = UTF-8 - -# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by -# double-quotes, unless you are using Doxywizard) that should identify the -# project for which the documentation is generated. This name is used in the -# title of most generated pages and in a few other places. -# The default value is: My Project. - -PROJECT_NAME = Jellyfin - -# The PROJECT_NUMBER tag can be used to enter a project or revision number. This -# could be handy for archiving the generated documentation or if some version -# control system is used. - -PROJECT_NUMBER = - -# Using the PROJECT_BRIEF tag one can provide an optional one line description -# for a project that appears at the top of each page and should give viewer a -# quick idea about the purpose of the project. Keep the description short. - -PROJECT_BRIEF = "The Free Software Media System" - -# With the PROJECT_LOGO tag one can specify a logo or an icon that is included -# in the documentation. The maximum height of the logo should not exceed 55 -# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy -# the logo to the output directory. - -PROJECT_LOGO = - -# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path -# into which the generated documentation will be written. If a relative path is -# entered, it will be relative to the location where doxygen was started. If -# left blank the current directory will be used. - -OUTPUT_DIRECTORY = doc - -# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- -# directories (in 2 levels) under the output directory of each output format and -# will distribute the generated files over these directories. Enabling this -# option can be useful when feeding doxygen a huge amount of source files, where -# putting all generated files in the same directory would otherwise causes -# performance problems for the file system. -# The default value is: NO. - -CREATE_SUBDIRS = NO - -# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII -# characters to appear in the names of generated files. If set to NO, non-ASCII -# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode -# U+3044. -# The default value is: NO. - -ALLOW_UNICODE_NAMES = NO - -# The OUTPUT_LANGUAGE tag is used to specify the language in which all -# documentation generated by doxygen is written. Doxygen will use this -# information to generate all constant output in the proper language. -# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese, -# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States), -# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian, -# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages), -# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian, -# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian, -# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish, -# Ukrainian and Vietnamese. -# The default value is: English. - -OUTPUT_LANGUAGE = English - -# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all -# documentation generated by doxygen is written. Doxygen will use this -# information to generate all generated output in the proper direction. -# Possible values are: None, LTR, RTL and Context. -# The default value is: None. - -OUTPUT_TEXT_DIRECTION = None - -# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member -# descriptions after the members that are listed in the file and class -# documentation (similar to Javadoc). Set to NO to disable this. -# The default value is: YES. - -BRIEF_MEMBER_DESC = YES - -# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief -# description of a member or function before the detailed description -# -# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the -# brief descriptions will be completely suppressed. -# The default value is: YES. - -REPEAT_BRIEF = YES - -# This tag implements a quasi-intelligent brief description abbreviator that is -# used to form the text in various listings. Each string in this list, if found -# as the leading text of the brief description, will be stripped from the text -# and the result, after processing the whole list, is used as the annotated -# text. Otherwise, the brief description is used as-is. If left blank, the -# following values are used ($name is automatically replaced with the name of -# the entity):The $name class, The $name widget, The $name file, is, provides, -# specifies, contains, represents, a, an and the. - -ABBREVIATE_BRIEF = "The $name class" \ - "The $name widget" \ - "The $name file" \ - is \ - provides \ - specifies \ - contains \ - represents \ - a \ - an \ - the - -# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then -# doxygen will generate a detailed section even if there is only a brief -# description. -# The default value is: NO. - -ALWAYS_DETAILED_SEC = NO - -# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all -# inherited members of a class in the documentation of that class as if those -# members were ordinary class members. Constructors, destructors and assignment -# operators of the base classes will not be shown. -# The default value is: NO. - -INLINE_INHERITED_MEMB = NO - -# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path -# before files name in the file list and in the header files. If set to NO the -# shortest path that makes the file name unique will be used -# The default value is: YES. - -FULL_PATH_NAMES = YES - -# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path. -# Stripping is only done if one of the specified strings matches the left-hand -# part of the path. The tag can be used to show relative paths in the file list. -# If left blank the directory from which doxygen is run is used as the path to -# strip. -# -# Note that you can specify absolute paths here, but also relative paths, which -# will be relative from the directory where doxygen is started. -# This tag requires that the tag FULL_PATH_NAMES is set to YES. - -STRIP_FROM_PATH = - -# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the -# path mentioned in the documentation of a class, which tells the reader which -# header file to include in order to use a class. If left blank only the name of -# the header file containing the class definition is used. Otherwise one should -# specify the list of include paths that are normally passed to the compiler -# using the -I flag. - -STRIP_FROM_INC_PATH = - -# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but -# less readable) file names. This can be useful is your file systems doesn't -# support long names like on DOS, Mac, or CD-ROM. -# The default value is: NO. - -SHORT_NAMES = NO - -# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the -# first line (until the first dot) of a Javadoc-style comment as the brief -# description. If set to NO, the Javadoc-style will behave just like regular Qt- -# style comments (thus requiring an explicit @brief command for a brief -# description.) -# The default value is: NO. - -JAVADOC_AUTOBRIEF = NO - -# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first -# line (until the first dot) of a Qt-style comment as the brief description. If -# set to NO, the Qt-style will behave just like regular Qt-style comments (thus -# requiring an explicit \brief command for a brief description.) -# The default value is: NO. - -QT_AUTOBRIEF = NO - -# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a -# multi-line C++ special comment block (i.e. a block of //! or /// comments) as -# a brief description. This used to be the default behavior. The new default is -# to treat a multi-line C++ comment block as a detailed description. Set this -# tag to YES if you prefer the old behavior instead. -# -# Note that setting this tag to YES also means that rational rose comments are -# not recognized any more. -# The default value is: NO. - -MULTILINE_CPP_IS_BRIEF = NO - -# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the -# documentation from any documented member that it re-implements. -# The default value is: YES. - -INHERIT_DOCS = YES - -# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new -# page for each member. If set to NO, the documentation of a member will be part -# of the file/class/namespace that contains it. -# The default value is: NO. - -SEPARATE_MEMBER_PAGES = NO - -# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen -# uses this value to replace tabs by spaces in code fragments. -# Minimum value: 1, maximum value: 16, default value: 4. - -TAB_SIZE = 4 - -# This tag can be used to specify a number of aliases that act as commands in -# the documentation. An alias has the form: -# name=value -# For example adding -# "sideeffect=@par Side Effects:\n" -# will allow you to put the command \sideeffect (or @sideeffect) in the -# documentation, which will result in a user-defined paragraph with heading -# "Side Effects:". You can put \n's in the value part of an alias to insert -# newlines (in the resulting output). You can put ^^ in the value part of an -# alias to insert a newline as if a physical newline was in the original file. -# When you need a literal { or } or , in the value part of an alias you have to -# escape them by means of a backslash (\), this can lead to conflicts with the -# commands \{ and \} for these it is advised to use the version @{ and @} or use -# a double escape (\\{ and \\}) - -ALIASES = - -# This tag can be used to specify a number of word-keyword mappings (TCL only). -# A mapping has the form "name=value". For example adding "class=itcl::class" -# will allow you to use the command class in the itcl::class meaning. - -TCL_SUBST = - -# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources -# only. Doxygen will then generate output that is more tailored for C. For -# instance, some of the names that are used will be different. The list of all -# members will be omitted, etc. -# The default value is: NO. - -OPTIMIZE_OUTPUT_FOR_C = NO - -# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or -# Python sources only. Doxygen will then generate output that is more tailored -# for that language. For instance, namespaces will be presented as packages, -# qualified scopes will look different, etc. -# The default value is: NO. - -OPTIMIZE_OUTPUT_JAVA = YES - -# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran -# sources. Doxygen will then generate output that is tailored for Fortran. -# The default value is: NO. - -OPTIMIZE_FOR_FORTRAN = NO - -# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL -# sources. Doxygen will then generate output that is tailored for VHDL. -# The default value is: NO. - -OPTIMIZE_OUTPUT_VHDL = NO - -# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice -# sources only. Doxygen will then generate output that is more tailored for that -# language. For instance, namespaces will be presented as modules, types will be -# separated into more groups, etc. -# The default value is: NO. - -OPTIMIZE_OUTPUT_SLICE = NO - -# Doxygen selects the parser to use depending on the extension of the files it -# parses. With this tag you can assign which parser to use for a given -# extension. Doxygen has a built-in mapping, but you can override or extend it -# using this tag. The format is ext=language, where ext is a file extension, and -# language is one of the parsers supported by doxygen: IDL, Java, Javascript, -# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, -# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: -# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser -# tries to guess whether the code is fixed or free formatted code, this is the -# default for Fortran type files), VHDL, tcl. For instance to make doxygen treat -# .inc files as Fortran files (default is PHP), and .f files as C (default is -# Fortran), use: inc=Fortran f=C. -# -# Note: For files without extension you can use no_extension as a placeholder. -# -# Note that for custom extensions you also need to set FILE_PATTERNS otherwise -# the files are not read by doxygen. - -EXTENSION_MAPPING = - -# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments -# according to the Markdown format, which allows for more readable -# documentation. See https://daringfireball.net/projects/markdown/ for details. -# The output of markdown processing is further processed by doxygen, so you can -# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in -# case of backward compatibilities issues. -# The default value is: YES. - -MARKDOWN_SUPPORT = YES - -# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up -# to that level are automatically included in the table of contents, even if -# they do not have an id attribute. -# Note: This feature currently applies only to Markdown headings. -# Minimum value: 0, maximum value: 99, default value: 0. -# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. - -TOC_INCLUDE_HEADINGS = 0 - -# When enabled doxygen tries to link words that correspond to documented -# classes, or namespaces to their corresponding documentation. Such a link can -# be prevented in individual cases by putting a % sign in front of the word or -# globally by setting AUTOLINK_SUPPORT to NO. -# The default value is: YES. - -AUTOLINK_SUPPORT = YES - -# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want -# to include (a tag file for) the STL sources as input, then you should set this -# tag to YES in order to let doxygen match functions declarations and -# definitions whose arguments contain STL classes (e.g. func(std::string); -# versus func(std::string) {}). This also make the inheritance and collaboration -# diagrams that involve STL classes more complete and accurate. -# The default value is: NO. - -BUILTIN_STL_SUPPORT = NO - -# If you use Microsoft's C++/CLI language, you should set this option to YES to -# enable parsing support. -# The default value is: NO. - -CPP_CLI_SUPPORT = NO - -# Set the SIP_SUPPORT tag to YES if your project consists of sip (see: -# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen -# will parse them like normal C++ but will assume all classes use public instead -# of private inheritance when no explicit protection keyword is present. -# The default value is: NO. - -SIP_SUPPORT = NO - -# For Microsoft's IDL there are propget and propput attributes to indicate -# getter and setter methods for a property. Setting this option to YES will make -# doxygen to replace the get and set methods by a property in the documentation. -# This will only work if the methods are indeed getting or setting a simple -# type. If this is not the case, or you want to show the methods anyway, you -# should set this option to NO. -# The default value is: YES. - -IDL_PROPERTY_SUPPORT = YES - -# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC -# tag is set to YES then doxygen will reuse the documentation of the first -# member in the group (if any) for the other members of the group. By default -# all members of a group must be documented explicitly. -# The default value is: NO. - -DISTRIBUTE_GROUP_DOC = NO - -# If one adds a struct or class to a group and this option is enabled, then also -# any nested class or struct is added to the same group. By default this option -# is disabled and one has to add nested compounds explicitly via \ingroup. -# The default value is: NO. - -GROUP_NESTED_COMPOUNDS = NO - -# Set the SUBGROUPING tag to YES to allow class member groups of the same type -# (for instance a group of public functions) to be put as a subgroup of that -# type (e.g. under the Public Functions section). Set it to NO to prevent -# subgrouping. Alternatively, this can be done per class using the -# \nosubgrouping command. -# The default value is: YES. - -SUBGROUPING = YES - -# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions -# are shown inside the group in which they are included (e.g. using \ingroup) -# instead of on a separate page (for HTML and Man pages) or section (for LaTeX -# and RTF). -# -# Note that this feature does not work in combination with -# SEPARATE_MEMBER_PAGES. -# The default value is: NO. - -INLINE_GROUPED_CLASSES = NO - -# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions -# with only public data fields or simple typedef fields will be shown inline in -# the documentation of the scope in which they are defined (i.e. file, -# namespace, or group documentation), provided this scope is documented. If set -# to NO, structs, classes, and unions are shown on a separate page (for HTML and -# Man pages) or section (for LaTeX and RTF). -# The default value is: NO. - -INLINE_SIMPLE_STRUCTS = NO - -# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or -# enum is documented as struct, union, or enum with the name of the typedef. So -# typedef struct TypeS {} TypeT, will appear in the documentation as a struct -# with name TypeT. When disabled the typedef will appear as a member of a file, -# namespace, or class. And the struct will be named TypeS. This can typically be -# useful for C code in case the coding convention dictates that all compound -# types are typedef'ed and only the typedef is referenced, never the tag name. -# The default value is: NO. - -TYPEDEF_HIDES_STRUCT = NO - -# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This -# cache is used to resolve symbols given their name and scope. Since this can be -# an expensive process and often the same symbol appears multiple times in the -# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small -# doxygen will become slower. If the cache is too large, memory is wasted. The -# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range -# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536 -# symbols. At the end of a run doxygen will report the cache usage and suggest -# the optimal cache size from a speed point of view. -# Minimum value: 0, maximum value: 9, default value: 0. - -LOOKUP_CACHE_SIZE = 0 - -#--------------------------------------------------------------------------- -# Build related configuration options -#--------------------------------------------------------------------------- - -# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in -# documentation are documented, even if no documentation was available. Private -# class members and static file members will be hidden unless the -# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. -# Note: This will also disable the warnings about undocumented members that are -# normally produced when WARNINGS is set to YES. -# The default value is: NO. - -EXTRACT_ALL = YES - -# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will -# be included in the documentation. -# The default value is: NO. - -EXTRACT_PRIVATE = NO - -# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal -# scope will be included in the documentation. -# The default value is: NO. - -EXTRACT_PACKAGE = NO - -# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be -# included in the documentation. -# The default value is: NO. - -EXTRACT_STATIC = NO - -# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined -# locally in source files will be included in the documentation. If set to NO, -# only classes defined in header files are included. Does not have any effect -# for Java sources. -# The default value is: YES. - -EXTRACT_LOCAL_CLASSES = YES - -# This flag is only useful for Objective-C code. If set to YES, local methods, -# which are defined in the implementation section but not in the interface are -# included in the documentation. If set to NO, only methods in the interface are -# included. -# The default value is: NO. - -EXTRACT_LOCAL_METHODS = NO - -# If this flag is set to YES, the members of anonymous namespaces will be -# extracted and appear in the documentation as a namespace called -# 'anonymous_namespace{file}', where file will be replaced with the base name of -# the file that contains the anonymous namespace. By default anonymous namespace -# are hidden. -# The default value is: NO. - -EXTRACT_ANON_NSPACES = NO - -# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all -# undocumented members inside documented classes or files. If set to NO these -# members will be included in the various overviews, but no documentation -# section is generated. This option has no effect if EXTRACT_ALL is enabled. -# The default value is: NO. - -HIDE_UNDOC_MEMBERS = NO - -# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all -# undocumented classes that are normally visible in the class hierarchy. If set -# to NO, these classes will be included in the various overviews. This option -# has no effect if EXTRACT_ALL is enabled. -# The default value is: NO. - -HIDE_UNDOC_CLASSES = NO - -# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend -# (class|struct|union) declarations. If set to NO, these declarations will be -# included in the documentation. -# The default value is: NO. - -HIDE_FRIEND_COMPOUNDS = NO - -# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any -# documentation blocks found inside the body of a function. If set to NO, these -# blocks will be appended to the function's detailed documentation block. -# The default value is: NO. - -HIDE_IN_BODY_DOCS = NO - -# The INTERNAL_DOCS tag determines if documentation that is typed after a -# \internal command is included. If the tag is set to NO then the documentation -# will be excluded. Set it to YES to include the internal documentation. -# The default value is: NO. - -INTERNAL_DOCS = NO - -# If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file -# names in lower-case letters. If set to YES, upper-case letters are also -# allowed. This is useful if you have classes or files whose names only differ -# in case and if your file system supports case sensitive file names. Windows -# and Mac users are advised to set this option to NO. -# The default value is: system dependent. - -CASE_SENSE_NAMES = NO - -# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with -# their full class and namespace scopes in the documentation. If set to YES, the -# scope will be hidden. -# The default value is: NO. - -HIDE_SCOPE_NAMES = NO - -# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will -# append additional text to a page's title, such as Class Reference. If set to -# YES the compound reference will be hidden. -# The default value is: NO. - -HIDE_COMPOUND_REFERENCE= NO - -# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of -# the files that are included by a file in the documentation of that file. -# The default value is: YES. - -SHOW_INCLUDE_FILES = YES - -# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each -# grouped member an include statement to the documentation, telling the reader -# which file to include in order to use the member. -# The default value is: NO. - -SHOW_GROUPED_MEMB_INC = NO - -# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include -# files with double quotes in the documentation rather than with sharp brackets. -# The default value is: NO. - -FORCE_LOCAL_INCLUDES = NO - -# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the -# documentation for inline members. -# The default value is: YES. - -INLINE_INFO = YES - -# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the -# (detailed) documentation of file and class members alphabetically by member -# name. If set to NO, the members will appear in declaration order. -# The default value is: YES. - -SORT_MEMBER_DOCS = YES - -# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief -# descriptions of file, namespace and class members alphabetically by member -# name. If set to NO, the members will appear in declaration order. Note that -# this will also influence the order of the classes in the class list. -# The default value is: NO. - -SORT_BRIEF_DOCS = NO - -# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the -# (brief and detailed) documentation of class members so that constructors and -# destructors are listed first. If set to NO the constructors will appear in the -# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS. -# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief -# member documentation. -# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting -# detailed member documentation. -# The default value is: NO. - -SORT_MEMBERS_CTORS_1ST = NO - -# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy -# of group names into alphabetical order. If set to NO the group names will -# appear in their defined order. -# The default value is: NO. - -SORT_GROUP_NAMES = NO - -# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by -# fully-qualified names, including namespaces. If set to NO, the class list will -# be sorted only by class name, not including the namespace part. -# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES. -# Note: This option applies only to the class list, not to the alphabetical -# list. -# The default value is: NO. - -SORT_BY_SCOPE_NAME = NO - -# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper -# type resolution of all parameters of a function it will reject a match between -# the prototype and the implementation of a member function even if there is -# only one candidate or it is obvious which candidate to choose by doing a -# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still -# accept a match between prototype and implementation in such cases. -# The default value is: NO. - -STRICT_PROTO_MATCHING = NO - -# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo -# list. This list is created by putting \todo commands in the documentation. -# The default value is: YES. - -GENERATE_TODOLIST = YES - -# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test -# list. This list is created by putting \test commands in the documentation. -# The default value is: YES. - -GENERATE_TESTLIST = YES - -# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug -# list. This list is created by putting \bug commands in the documentation. -# The default value is: YES. - -GENERATE_BUGLIST = YES - -# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) -# the deprecated list. This list is created by putting \deprecated commands in -# the documentation. -# The default value is: YES. - -GENERATE_DEPRECATEDLIST= YES - -# The ENABLED_SECTIONS tag can be used to enable conditional documentation -# sections, marked by \if <section_label> ... \endif and \cond <section_label> -# ... \endcond blocks. - -ENABLED_SECTIONS = - -# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the -# initial value of a variable or macro / define can have for it to appear in the -# documentation. If the initializer consists of more lines than specified here -# it will be hidden. Use a value of 0 to hide initializers completely. The -# appearance of the value of individual variables and macros / defines can be -# controlled using \showinitializer or \hideinitializer command in the -# documentation regardless of this setting. -# Minimum value: 0, maximum value: 10000, default value: 30. - -MAX_INITIALIZER_LINES = 30 - -# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at -# the bottom of the documentation of classes and structs. If set to YES, the -# list will mention the files that were used to generate the documentation. -# The default value is: YES. - -SHOW_USED_FILES = YES - -# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This -# will remove the Files entry from the Quick Index and from the Folder Tree View -# (if specified). -# The default value is: YES. - -SHOW_FILES = YES - -# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces -# page. This will remove the Namespaces entry from the Quick Index and from the -# Folder Tree View (if specified). -# The default value is: YES. - -SHOW_NAMESPACES = YES - -# The FILE_VERSION_FILTER tag can be used to specify a program or script that -# doxygen should invoke to get the current version for each file (typically from -# the version control system). Doxygen will invoke the program by executing (via -# popen()) the command command input-file, where command is the value of the -# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided -# by doxygen. Whatever the program writes to standard output is used as the file -# version. For an example see the documentation. - -FILE_VERSION_FILTER = - -# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed -# by doxygen. The layout file controls the global structure of the generated -# output files in an output format independent way. To create the layout file -# that represents doxygen's defaults, run doxygen with the -l option. You can -# optionally specify a file name after the option, if omitted DoxygenLayout.xml -# will be used as the name of the layout file. -# -# Note that if you run doxygen from a directory containing a file called -# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE -# tag is left empty. - -LAYOUT_FILE = - -# The CITE_BIB_FILES tag can be used to specify one or more bib files containing -# the reference definitions. This must be a list of .bib files. The .bib -# extension is automatically appended if omitted. This requires the bibtex tool -# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info. -# For LaTeX the style of the bibliography can be controlled using -# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the -# search path. See also \cite for info how to create references. - -CITE_BIB_FILES = - -#--------------------------------------------------------------------------- -# Configuration options related to warning and progress messages -#--------------------------------------------------------------------------- - -# The QUIET tag can be used to turn on/off the messages that are generated to -# standard output by doxygen. If QUIET is set to YES this implies that the -# messages are off. -# The default value is: NO. - -QUIET = NO - -# The WARNINGS tag can be used to turn on/off the warning messages that are -# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES -# this implies that the warnings are on. -# -# Tip: Turn warnings on while writing the documentation. -# The default value is: YES. - -WARNINGS = YES - -# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate -# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag -# will automatically be disabled. -# The default value is: YES. - -WARN_IF_UNDOCUMENTED = YES - -# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for -# potential errors in the documentation, such as not documenting some parameters -# in a documented function, or documenting parameters that don't exist or using -# markup commands wrongly. -# The default value is: YES. - -WARN_IF_DOC_ERROR = YES - -# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that -# are documented, but have no documentation for their parameters or return -# value. If set to NO, doxygen will only warn about wrong or incomplete -# parameter documentation, but not about the absence of documentation. If -# EXTRACT_ALL is set to YES then this flag will automatically be disabled. -# The default value is: NO. - -WARN_NO_PARAMDOC = NO - -# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when -# a warning is encountered. -# The default value is: NO. - -WARN_AS_ERROR = NO - -# The WARN_FORMAT tag determines the format of the warning messages that doxygen -# can produce. The string should contain the $file, $line, and $text tags, which -# will be replaced by the file and line number from which the warning originated -# and the warning text. Optionally the format may contain $version, which will -# be replaced by the version of the file (if it could be obtained via -# FILE_VERSION_FILTER) -# The default value is: $file:$line: $text. - -WARN_FORMAT = "$file:$line: $text" - -# The WARN_LOGFILE tag can be used to specify a file to which warning and error -# messages should be written. If left blank the output is written to standard -# error (stderr). - -WARN_LOGFILE = - -#--------------------------------------------------------------------------- -# Configuration options related to the input files -#--------------------------------------------------------------------------- - -# The INPUT tag is used to specify the files and/or directories that contain -# documented source files. You may enter file names like myfile.cpp or -# directories like /usr/src/myproject. Separate the files or directories with -# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING -# Note: If this tag is empty the current directory is searched. - -INPUT = - -# This tag can be used to specify the character encoding of the source files -# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses -# libiconv (or the iconv built into libc) for the transcoding. See the libiconv -# documentation (see: https://www.gnu.org/software/libiconv/) for the list of -# possible encodings. -# The default value is: UTF-8. - -INPUT_ENCODING = UTF-8 - -# If the value of the INPUT tag contains directories, you can use the -# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and -# *.h) to filter out the source-files in the directories. -# -# Note that for custom extensions or not directly supported extensions you also -# need to set EXTENSION_MAPPING for the extension otherwise the files are not -# read by doxygen. -# -# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, -# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, -# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, -# *.m, *.markdown, *.md, *.mm, *.dox, *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, -# *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice. - -FILE_PATTERNS = *.c \ - *.cc \ - *.cxx \ - *.cpp \ - *.c++ \ - *.java \ - *.ii \ - *.ixx \ - *.ipp \ - *.i++ \ - *.inl \ - *.idl \ - *.ddl \ - *.odl \ - *.h \ - *.hh \ - *.hxx \ - *.hpp \ - *.h++ \ - *.cs \ - *.d \ - *.php \ - *.php4 \ - *.php5 \ - *.phtml \ - *.inc \ - *.m \ - *.markdown \ - *.md \ - *.mm \ - *.dox \ - *.py \ - *.pyw \ - *.f90 \ - *.f95 \ - *.f03 \ - *.f08 \ - *.f \ - *.for \ - *.tcl \ - *.vhd \ - *.vhdl \ - *.ucf \ - *.qsf \ - *.ice - -# The RECURSIVE tag can be used to specify whether or not subdirectories should -# be searched for input files as well. -# The default value is: NO. - -RECURSIVE = YES - -# The EXCLUDE tag can be used to specify files and/or directories that should be -# excluded from the INPUT source files. This way you can easily exclude a -# subdirectory from a directory tree whose root is specified with the INPUT tag. -# -# Note that relative paths are relative to the directory from which doxygen is -# run. - -EXCLUDE = - -# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or -# directories that are symbolic links (a Unix file system feature) are excluded -# from the input. -# The default value is: NO. - -EXCLUDE_SYMLINKS = NO - -# If the value of the INPUT tag contains directories, you can use the -# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude -# certain files from those directories. -# -# Note that the wildcards are matched against the file with absolute path, so to -# exclude all test directories for example use the pattern */test/* - -EXCLUDE_PATTERNS = - -# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names -# (namespaces, classes, functions, etc.) that should be excluded from the -# output. The symbol name can be a fully qualified name, a word, or if the -# wildcard * is used, a substring. Examples: ANamespace, AClass, -# AClass::ANamespace, ANamespace::*Test -# -# Note that the wildcards are matched against the file with absolute path, so to -# exclude all test directories use the pattern */test/* - -EXCLUDE_SYMBOLS = - -# The EXAMPLE_PATH tag can be used to specify one or more files or directories -# that contain example code fragments that are included (see the \include -# command). - -EXAMPLE_PATH = - -# If the value of the EXAMPLE_PATH tag contains directories, you can use the -# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and -# *.h) to filter out the source-files in the directories. If left blank all -# files are included. - -EXAMPLE_PATTERNS = * - -# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be -# searched for input files to be used with the \include or \dontinclude commands -# irrespective of the value of the RECURSIVE tag. -# The default value is: NO. - -EXAMPLE_RECURSIVE = NO - -# The IMAGE_PATH tag can be used to specify one or more files or directories -# that contain images that are to be included in the documentation (see the -# \image command). - -IMAGE_PATH = - -# The INPUT_FILTER tag can be used to specify a program that doxygen should -# invoke to filter for each input file. Doxygen will invoke the filter program -# by executing (via popen()) the command: -# -# <filter> <input-file> -# -# where <filter> is the value of the INPUT_FILTER tag, and <input-file> is the -# name of an input file. Doxygen will then use the output that the filter -# program writes to standard output. If FILTER_PATTERNS is specified, this tag -# will be ignored. -# -# Note that the filter must not add or remove lines; it is applied before the -# code is scanned, but not when the output code is generated. If lines are added -# or removed, the anchors will not be placed correctly. -# -# Note that for custom extensions or not directly supported extensions you also -# need to set EXTENSION_MAPPING for the extension otherwise the files are not -# properly processed by doxygen. - -INPUT_FILTER = - -# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern -# basis. Doxygen will compare the file name with each pattern and apply the -# filter if there is a match. The filters are a list of the form: pattern=filter -# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how -# filters are used. If the FILTER_PATTERNS tag is empty or if none of the -# patterns match the file name, INPUT_FILTER is applied. -# -# Note that for custom extensions or not directly supported extensions you also -# need to set EXTENSION_MAPPING for the extension otherwise the files are not -# properly processed by doxygen. - -FILTER_PATTERNS = - -# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using -# INPUT_FILTER) will also be used to filter the input files that are used for -# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). -# The default value is: NO. - -FILTER_SOURCE_FILES = NO - -# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file -# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and -# it is also possible to disable source filtering for a specific pattern using -# *.ext= (so without naming a filter). -# This tag requires that the tag FILTER_SOURCE_FILES is set to YES. - -FILTER_SOURCE_PATTERNS = - -# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that -# is part of the input, its contents will be placed on the main page -# (index.html). This can be useful if you have a project on for instance GitHub -# and want to reuse the introduction page also for the doxygen output. - -USE_MDFILE_AS_MAINPAGE = ./README.md - -#--------------------------------------------------------------------------- -# Configuration options related to source browsing -#--------------------------------------------------------------------------- - -# If the SOURCE_BROWSER tag is set to YES then a list of source files will be -# generated. Documented entities will be cross-referenced with these sources. -# -# Note: To get rid of all source code in the generated output, make sure that -# also VERBATIM_HEADERS is set to NO. -# The default value is: NO. - -SOURCE_BROWSER = YES - -# Setting the INLINE_SOURCES tag to YES will include the body of functions, -# classes and enums directly into the documentation. -# The default value is: NO. - -INLINE_SOURCES = NO - -# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any -# special comment blocks from generated source code fragments. Normal C, C++ and -# Fortran comments will always remain visible. -# The default value is: YES. - -STRIP_CODE_COMMENTS = YES - -# If the REFERENCED_BY_RELATION tag is set to YES then for each documented -# entity all documented functions referencing it will be listed. -# The default value is: NO. - -REFERENCED_BY_RELATION = NO - -# If the REFERENCES_RELATION tag is set to YES then for each documented function -# all documented entities called/used by that function will be listed. -# The default value is: NO. - -REFERENCES_RELATION = NO - -# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set -# to YES then the hyperlinks from functions in REFERENCES_RELATION and -# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will -# link to the documentation. -# The default value is: YES. - -REFERENCES_LINK_SOURCE = YES - -# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the -# source code will show a tooltip with additional information such as prototype, -# brief description and links to the definition and documentation. Since this -# will make the HTML file larger and loading of large files a bit slower, you -# can opt to disable this feature. -# The default value is: YES. -# This tag requires that the tag SOURCE_BROWSER is set to YES. - -SOURCE_TOOLTIPS = YES - -# If the USE_HTAGS tag is set to YES then the references to source code will -# point to the HTML generated by the htags(1) tool instead of doxygen built-in -# source browser. The htags tool is part of GNU's global source tagging system -# (see https://www.gnu.org/software/global/global.html). You will need version -# 4.8.6 or higher. -# -# To use it do the following: -# - Install the latest version of global -# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file -# - Make sure the INPUT points to the root of the source tree -# - Run doxygen as normal -# -# Doxygen will invoke htags (and that will in turn invoke gtags), so these -# tools must be available from the command line (i.e. in the search path). -# -# The result: instead of the source browser generated by doxygen, the links to -# source code will now point to the output of htags. -# The default value is: NO. -# This tag requires that the tag SOURCE_BROWSER is set to YES. - -USE_HTAGS = NO - -# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a -# verbatim copy of the header file for each class for which an include is -# specified. Set to NO to disable this. -# See also: Section \class. -# The default value is: YES. - -VERBATIM_HEADERS = YES - -# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the -# clang parser (see: http://clang.llvm.org/) for more accurate parsing at the -# cost of reduced performance. This can be particularly helpful with template -# rich C++ code for which doxygen's built-in parser lacks the necessary type -# information. -# Note: The availability of this option depends on whether or not doxygen was -# generated with the -Duse_libclang=ON option for CMake. -# The default value is: NO. - -CLANG_ASSISTED_PARSING = NO - -# If clang assisted parsing is enabled you can provide the compiler with command -# line options that you would normally use when invoking the compiler. Note that -# the include paths will already be set by doxygen for the files and directories -# specified with INPUT and INCLUDE_PATH. -# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. - -CLANG_OPTIONS = - -# If clang assisted parsing is enabled you can provide the clang parser with the -# path to the compilation database (see: -# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) used when the files -# were built. This is equivalent to specifying the "-p" option to a clang tool, -# such as clang-check. These options will then be passed to the parser. -# Note: The availability of this option depends on whether or not doxygen was -# generated with the -Duse_libclang=ON option for CMake. - -CLANG_DATABASE_PATH = - -#--------------------------------------------------------------------------- -# Configuration options related to the alphabetical class index -#--------------------------------------------------------------------------- - -# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all -# compounds will be generated. Enable this if the project contains a lot of -# classes, structs, unions or interfaces. -# The default value is: YES. - -ALPHABETICAL_INDEX = YES - -# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in -# which the alphabetical index list will be split. -# Minimum value: 1, maximum value: 20, default value: 5. -# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. - -COLS_IN_ALPHA_INDEX = 5 - -# In case all classes in a project start with a common prefix, all classes will -# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag -# can be used to specify a prefix (or a list of prefixes) that should be ignored -# while generating the index headers. -# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. - -IGNORE_PREFIX = - -#--------------------------------------------------------------------------- -# Configuration options related to the HTML output -#--------------------------------------------------------------------------- - -# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output -# The default value is: YES. - -GENERATE_HTML = YES - -# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a -# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of -# it. -# The default directory is: html. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_OUTPUT = html - -# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each -# generated HTML page (for example: .htm, .php, .asp). -# The default value is: .html. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_FILE_EXTENSION = .html - -# The HTML_HEADER tag can be used to specify a user-defined HTML header file for -# each generated HTML page. If the tag is left blank doxygen will generate a -# standard header. -# -# To get valid HTML the header file that includes any scripts and style sheets -# that doxygen needs, which is dependent on the configuration options used (e.g. -# the setting GENERATE_TREEVIEW). It is highly recommended to start with a -# default header using -# doxygen -w html new_header.html new_footer.html new_stylesheet.css -# YourConfigFile -# and then modify the file new_header.html. See also section "Doxygen usage" -# for information on how to generate the default header that doxygen normally -# uses. -# Note: The header is subject to change so you typically have to regenerate the -# default header when upgrading to a newer version of doxygen. For a description -# of the possible markers and block names see the documentation. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_HEADER = - -# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each -# generated HTML page. If the tag is left blank doxygen will generate a standard -# footer. See HTML_HEADER for more information on how to generate a default -# footer and what special commands can be used inside the footer. See also -# section "Doxygen usage" for information on how to generate the default footer -# that doxygen normally uses. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_FOOTER = - -# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style -# sheet that is used by each HTML page. It can be used to fine-tune the look of -# the HTML output. If left blank doxygen will generate a default style sheet. -# See also section "Doxygen usage" for information on how to generate the style -# sheet that doxygen normally uses. -# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as -# it is more robust and this tag (HTML_STYLESHEET) will in the future become -# obsolete. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_STYLESHEET = - -# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined -# cascading style sheets that are included after the standard style sheets -# created by doxygen. Using this option one can overrule certain style aspects. -# This is preferred over using HTML_STYLESHEET since it does not replace the -# standard style sheet and is therefore more robust against future updates. -# Doxygen will copy the style sheet files to the output directory. -# Note: The order of the extra style sheet files is of importance (e.g. the last -# style sheet in the list overrules the setting of the previous ones in the -# list). For an example see the documentation. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_EXTRA_STYLESHEET = - -# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or -# other source files which should be copied to the HTML output directory. Note -# that these files will be copied to the base HTML output directory. Use the -# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these -# files. In the HTML_STYLESHEET file, use the file name only. Also note that the -# files will be copied as-is; there are no commands or markers available. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_EXTRA_FILES = - -# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen -# will adjust the colors in the style sheet and background images according to -# this color. Hue is specified as an angle on a colorwheel, see -# https://en.wikipedia.org/wiki/Hue for more information. For instance the value -# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 -# purple, and 360 is red again. -# Minimum value: 0, maximum value: 359, default value: 220. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_COLORSTYLE_HUE = 195 - -# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors -# in the HTML output. For a value of 0 the output will use grayscales only. A -# value of 255 will produce the most vivid colors. -# Minimum value: 0, maximum value: 255, default value: 100. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_COLORSTYLE_SAT = 100 - -# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the -# luminance component of the colors in the HTML output. Values below 100 -# gradually make the output lighter, whereas values above 100 make the output -# darker. The value divided by 100 is the actual gamma applied, so 80 represents -# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not -# change the gamma. -# Minimum value: 40, maximum value: 240, default value: 80. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_COLORSTYLE_GAMMA = 86 - -# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML -# page will contain the date and time when the page was generated. Setting this -# to YES can help to show when doxygen was last run and thus if the -# documentation is up to date. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_TIMESTAMP = NO - -# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML -# documentation will contain a main index with vertical navigation menus that -# are dynamically created via Javascript. If disabled, the navigation index will -# consists of multiple levels of tabs that are statically embedded in every HTML -# page. Disable this option to support browsers that do not have Javascript, -# like the Qt help browser. -# The default value is: YES. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_DYNAMIC_MENUS = YES - -# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML -# documentation will contain sections that can be hidden and shown after the -# page has loaded. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_DYNAMIC_SECTIONS = NO - -# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries -# shown in the various tree structured indices initially; the user can expand -# and collapse entries dynamically later on. Doxygen will expand the tree to -# such a level that at most the specified number of entries are visible (unless -# a fully collapsed tree already exceeds this amount). So setting the number of -# entries 1 will produce a full collapsed tree by default. 0 is a special value -# representing an infinite number of entries and will result in a full expanded -# tree by default. -# Minimum value: 0, maximum value: 9999, default value: 100. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_INDEX_NUM_ENTRIES = 100 - -# If the GENERATE_DOCSET tag is set to YES, additional index files will be -# generated that can be used as input for Apple's Xcode 3 integrated development -# environment (see: https://developer.apple.com/xcode/), introduced with OSX -# 10.5 (Leopard). To create a documentation set, doxygen will generate a -# Makefile in the HTML output directory. Running make will produce the docset in -# that directory and running make install will install the docset in -# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at -# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy -# genXcode/_index.html for more information. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -GENERATE_DOCSET = NO - -# This tag determines the name of the docset feed. A documentation feed provides -# an umbrella under which multiple documentation sets from a single provider -# (such as a company or product suite) can be grouped. -# The default value is: Doxygen generated docs. -# This tag requires that the tag GENERATE_DOCSET is set to YES. - -DOCSET_FEEDNAME = "Doxygen generated docs" - -# This tag specifies a string that should uniquely identify the documentation -# set bundle. This should be a reverse domain-name style string, e.g. -# com.mycompany.MyDocSet. Doxygen will append .docset to the name. -# The default value is: org.doxygen.Project. -# This tag requires that the tag GENERATE_DOCSET is set to YES. - -DOCSET_BUNDLE_ID = org.doxygen.Project - -# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify -# the documentation publisher. This should be a reverse domain-name style -# string, e.g. com.mycompany.MyDocSet.documentation. -# The default value is: org.doxygen.Publisher. -# This tag requires that the tag GENERATE_DOCSET is set to YES. - -DOCSET_PUBLISHER_ID = org.doxygen.Publisher - -# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher. -# The default value is: Publisher. -# This tag requires that the tag GENERATE_DOCSET is set to YES. - -DOCSET_PUBLISHER_NAME = Publisher - -# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three -# additional HTML index files: index.hhp, index.hhc, and index.hhk. The -# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop -# (see: https://www.microsoft.com/en-us/download/details.aspx?id=21138) on -# Windows. -# -# The HTML Help Workshop contains a compiler that can convert all HTML output -# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML -# files are now used as the Windows 98 help format, and will replace the old -# Windows help format (.hlp) on all Windows platforms in the future. Compressed -# HTML files also contain an index, a table of contents, and you can search for -# words in the documentation. The HTML workshop also contains a viewer for -# compressed HTML files. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -GENERATE_HTMLHELP = NO - -# The CHM_FILE tag can be used to specify the file name of the resulting .chm -# file. You can add a path in front of the file if the result should not be -# written to the html output directory. -# This tag requires that the tag GENERATE_HTMLHELP is set to YES. - -CHM_FILE = - -# The HHC_LOCATION tag can be used to specify the location (absolute path -# including file name) of the HTML help compiler (hhc.exe). If non-empty, -# doxygen will try to run the HTML help compiler on the generated index.hhp. -# The file has to be specified with full path. -# This tag requires that the tag GENERATE_HTMLHELP is set to YES. - -HHC_LOCATION = - -# The GENERATE_CHI flag controls if a separate .chi index file is generated -# (YES) or that it should be included in the master .chm file (NO). -# The default value is: NO. -# This tag requires that the tag GENERATE_HTMLHELP is set to YES. - -GENERATE_CHI = NO - -# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) -# and project file content. -# This tag requires that the tag GENERATE_HTMLHELP is set to YES. - -CHM_INDEX_ENCODING = - -# The BINARY_TOC flag controls whether a binary table of contents is generated -# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it -# enables the Previous and Next buttons. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTMLHELP is set to YES. - -BINARY_TOC = NO - -# The TOC_EXPAND flag can be set to YES to add extra items for group members to -# the table of contents of the HTML help documentation and to the tree view. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTMLHELP is set to YES. - -TOC_EXPAND = NO - -# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and -# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that -# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help -# (.qch) of the generated HTML documentation. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -GENERATE_QHP = NO - -# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify -# the file name of the resulting .qch file. The path specified is relative to -# the HTML output folder. -# This tag requires that the tag GENERATE_QHP is set to YES. - -QCH_FILE = - -# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help -# Project output. For more information please see Qt Help Project / Namespace -# (see: http://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). -# The default value is: org.doxygen.Project. -# This tag requires that the tag GENERATE_QHP is set to YES. - -QHP_NAMESPACE = org.doxygen.Project - -# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt -# Help Project output. For more information please see Qt Help Project / Virtual -# Folders (see: http://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual- -# folders). -# The default value is: doc. -# This tag requires that the tag GENERATE_QHP is set to YES. - -QHP_VIRTUAL_FOLDER = doc - -# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom -# filter to add. For more information please see Qt Help Project / Custom -# Filters (see: http://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom- -# filters). -# This tag requires that the tag GENERATE_QHP is set to YES. - -QHP_CUST_FILTER_NAME = - -# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the -# custom filter to add. For more information please see Qt Help Project / Custom -# Filters (see: http://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom- -# filters). -# This tag requires that the tag GENERATE_QHP is set to YES. - -QHP_CUST_FILTER_ATTRS = - -# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this -# project's filter section matches. Qt Help Project / Filter Attributes (see: -# http://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes). -# This tag requires that the tag GENERATE_QHP is set to YES. - -QHP_SECT_FILTER_ATTRS = - -# The QHG_LOCATION tag can be used to specify the location of Qt's -# qhelpgenerator. If non-empty doxygen will try to run qhelpgenerator on the -# generated .qhp file. -# This tag requires that the tag GENERATE_QHP is set to YES. - -QHG_LOCATION = - -# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be -# generated, together with the HTML files, they form an Eclipse help plugin. To -# install this plugin and make it available under the help contents menu in -# Eclipse, the contents of the directory containing the HTML and XML files needs -# to be copied into the plugins directory of eclipse. The name of the directory -# within the plugins directory should be the same as the ECLIPSE_DOC_ID value. -# After copying Eclipse needs to be restarted before the help appears. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -GENERATE_ECLIPSEHELP = NO - -# A unique identifier for the Eclipse help plugin. When installing the plugin -# the directory name containing the HTML and XML files should also have this -# name. Each documentation set should have its own identifier. -# The default value is: org.doxygen.Project. -# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES. - -ECLIPSE_DOC_ID = org.doxygen.Project - -# If you want full control over the layout of the generated HTML pages it might -# be necessary to disable the index and replace it with your own. The -# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top -# of each HTML page. A value of NO enables the index and the value YES disables -# it. Since the tabs in the index contain the same information as the navigation -# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -DISABLE_INDEX = NO - -# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index -# structure should be generated to display hierarchical information. If the tag -# value is set to YES, a side panel will be generated containing a tree-like -# index structure (just like the one that is generated for HTML Help). For this -# to work a browser that supports JavaScript, DHTML, CSS and frames is required -# (i.e. any modern browser). Windows users are probably better off using the -# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can -# further fine-tune the look of the index. As an example, the default style -# sheet generated by doxygen has an example that shows how to put an image at -# the root of the tree instead of the PROJECT_NAME. Since the tree basically has -# the same information as the tab index, you could consider setting -# DISABLE_INDEX to YES when enabling this option. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -GENERATE_TREEVIEW = YES - -# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that -# doxygen will group on one line in the generated HTML documentation. -# -# Note that a value of 0 will completely suppress the enum values from appearing -# in the overview section. -# Minimum value: 0, maximum value: 20, default value: 4. -# This tag requires that the tag GENERATE_HTML is set to YES. - -ENUM_VALUES_PER_LINE = 4 - -# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used -# to set the initial width (in pixels) of the frame in which the tree is shown. -# Minimum value: 0, maximum value: 1500, default value: 250. -# This tag requires that the tag GENERATE_HTML is set to YES. - -TREEVIEW_WIDTH = 250 - -# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to -# external symbols imported via tag files in a separate window. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -EXT_LINKS_IN_WINDOW = NO - -# Use this tag to change the font size of LaTeX formulas included as images in -# the HTML documentation. When you change the font size after a successful -# doxygen run you need to manually remove any form_*.png images from the HTML -# output directory to force them to be regenerated. -# Minimum value: 8, maximum value: 50, default value: 10. -# This tag requires that the tag GENERATE_HTML is set to YES. - -FORMULA_FONTSIZE = 10 - -# Use the FORMULA_TRANSPARENT tag to determine whether or not the images -# generated for formulas are transparent PNGs. Transparent PNGs are not -# supported properly for IE 6.0, but are supported on all modern browsers. -# -# Note that when changing this option you need to delete any form_*.png files in -# the HTML output directory before the changes have effect. -# The default value is: YES. -# This tag requires that the tag GENERATE_HTML is set to YES. - -FORMULA_TRANSPARENT = YES - -# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see -# https://www.mathjax.org) which uses client side Javascript for the rendering -# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX -# installed or if you want to formulas look prettier in the HTML output. When -# enabled you may also need to install MathJax separately and configure the path -# to it using the MATHJAX_RELPATH option. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -USE_MATHJAX = NO - -# When MathJax is enabled you can set the default output format to be used for -# the MathJax output. See the MathJax site (see: -# http://docs.mathjax.org/en/latest/output.html) for more details. -# Possible values are: HTML-CSS (which is slower, but has the best -# compatibility), NativeMML (i.e. MathML) and SVG. -# The default value is: HTML-CSS. -# This tag requires that the tag USE_MATHJAX is set to YES. - -MATHJAX_FORMAT = HTML-CSS - -# When MathJax is enabled you need to specify the location relative to the HTML -# output directory using the MATHJAX_RELPATH option. The destination directory -# should contain the MathJax.js script. For instance, if the mathjax directory -# is located at the same level as the HTML output directory, then -# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax -# Content Delivery Network so you can quickly see the result without installing -# MathJax. However, it is strongly recommended to install a local copy of -# MathJax from https://www.mathjax.org before deployment. -# The default value is: https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/. -# This tag requires that the tag USE_MATHJAX is set to YES. - -MATHJAX_RELPATH = https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/ - -# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax -# extension names that should be enabled during MathJax rendering. For example -# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols -# This tag requires that the tag USE_MATHJAX is set to YES. - -MATHJAX_EXTENSIONS = - -# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces -# of code that will be used on startup of the MathJax code. See the MathJax site -# (see: http://docs.mathjax.org/en/latest/output.html) for more details. For an -# example see the documentation. -# This tag requires that the tag USE_MATHJAX is set to YES. - -MATHJAX_CODEFILE = - -# When the SEARCHENGINE tag is enabled doxygen will generate a search box for -# the HTML output. The underlying search engine uses javascript and DHTML and -# should work on any modern browser. Note that when using HTML help -# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET) -# there is already a search function so this one should typically be disabled. -# For large projects the javascript based search engine can be slow, then -# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to -# search using the keyboard; to jump to the search box use <access key> + S -# (what the <access key> is depends on the OS and browser, but it is typically -# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down -# key> to jump into the search results window, the results can be navigated -# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel -# the search. The filter options can be selected when the cursor is inside the -# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys> -# to select a filter and <Enter> or <escape> to activate or cancel the filter -# option. -# The default value is: YES. -# This tag requires that the tag GENERATE_HTML is set to YES. - -SEARCHENGINE = YES - -# When the SERVER_BASED_SEARCH tag is enabled the search engine will be -# implemented using a web server instead of a web client using Javascript. There -# are two flavors of web server based searching depending on the EXTERNAL_SEARCH -# setting. When disabled, doxygen will generate a PHP script for searching and -# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing -# and searching needs to be provided by external tools. See the section -# "External Indexing and Searching" for details. -# The default value is: NO. -# This tag requires that the tag SEARCHENGINE is set to YES. - -SERVER_BASED_SEARCH = NO - -# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP -# script for searching. Instead the search results are written to an XML file -# which needs to be processed by an external indexer. Doxygen will invoke an -# external search engine pointed to by the SEARCHENGINE_URL option to obtain the -# search results. -# -# Doxygen ships with an example indexer (doxyindexer) and search engine -# (doxysearch.cgi) which are based on the open source search engine library -# Xapian (see: https://xapian.org/). -# -# See the section "External Indexing and Searching" for details. -# The default value is: NO. -# This tag requires that the tag SEARCHENGINE is set to YES. - -EXTERNAL_SEARCH = NO - -# The SEARCHENGINE_URL should point to a search engine hosted by a web server -# which will return the search results when EXTERNAL_SEARCH is enabled. -# -# Doxygen ships with an example indexer (doxyindexer) and search engine -# (doxysearch.cgi) which are based on the open source search engine library -# Xapian (see: https://xapian.org/). See the section "External Indexing and -# Searching" for details. -# This tag requires that the tag SEARCHENGINE is set to YES. - -SEARCHENGINE_URL = - -# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed -# search data is written to a file for indexing by an external tool. With the -# SEARCHDATA_FILE tag the name of this file can be specified. -# The default file is: searchdata.xml. -# This tag requires that the tag SEARCHENGINE is set to YES. - -SEARCHDATA_FILE = searchdata.xml - -# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the -# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is -# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple -# projects and redirect the results back to the right project. -# This tag requires that the tag SEARCHENGINE is set to YES. - -EXTERNAL_SEARCH_ID = - -# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen -# projects other than the one defined by this configuration file, but that are -# all added to the same external search index. Each project needs to have a -# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of -# to a relative location where the documentation can be found. The format is: -# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ... -# This tag requires that the tag SEARCHENGINE is set to YES. - -EXTRA_SEARCH_MAPPINGS = - -#--------------------------------------------------------------------------- -# Configuration options related to the LaTeX output -#--------------------------------------------------------------------------- - -# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output. -# The default value is: YES. - -GENERATE_LATEX = NO - -# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a -# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of -# it. -# The default directory is: latex. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_OUTPUT = latex - -# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be -# invoked. -# -# Note that when not enabling USE_PDFLATEX the default is latex when enabling -# USE_PDFLATEX the default is pdflatex and when in the later case latex is -# chosen this is overwritten by pdflatex. For specific output languages the -# default can have been set differently, this depends on the implementation of -# the output language. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_CMD_NAME = - -# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate -# index for LaTeX. -# Note: This tag is used in the Makefile / make.bat. -# See also: LATEX_MAKEINDEX_CMD for the part in the generated output file -# (.tex). -# The default file is: makeindex. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -MAKEINDEX_CMD_NAME = makeindex - -# The LATEX_MAKEINDEX_CMD tag can be used to specify the command name to -# generate index for LaTeX. -# Note: This tag is used in the generated output file (.tex). -# See also: MAKEINDEX_CMD_NAME for the part in the Makefile / make.bat. -# The default value is: \makeindex. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_MAKEINDEX_CMD = \makeindex - -# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX -# documents. This may be useful for small projects and may help to save some -# trees in general. -# The default value is: NO. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -COMPACT_LATEX = NO - -# The PAPER_TYPE tag can be used to set the paper type that is used by the -# printer. -# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x -# 14 inches) and executive (7.25 x 10.5 inches). -# The default value is: a4. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -PAPER_TYPE = a4 - -# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names -# that should be included in the LaTeX output. The package can be specified just -# by its name or with the correct syntax as to be used with the LaTeX -# \usepackage command. To get the times font for instance you can specify : -# EXTRA_PACKAGES=times or EXTRA_PACKAGES={times} -# To use the option intlimits with the amsmath package you can specify: -# EXTRA_PACKAGES=[intlimits]{amsmath} -# If left blank no extra packages will be included. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -EXTRA_PACKAGES = - -# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the -# generated LaTeX document. The header should contain everything until the first -# chapter. If it is left blank doxygen will generate a standard header. See -# section "Doxygen usage" for information on how to let doxygen write the -# default header to a separate file. -# -# Note: Only use a user-defined header if you know what you are doing! The -# following commands have a special meaning inside the header: $title, -# $datetime, $date, $doxygenversion, $projectname, $projectnumber, -# $projectbrief, $projectlogo. Doxygen will replace $title with the empty -# string, for the replacement values of the other commands the user is referred -# to HTML_HEADER. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_HEADER = - -# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the -# generated LaTeX document. The footer should contain everything after the last -# chapter. If it is left blank doxygen will generate a standard footer. See -# LATEX_HEADER for more information on how to generate a default footer and what -# special commands can be used inside the footer. -# -# Note: Only use a user-defined footer if you know what you are doing! -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_FOOTER = - -# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined -# LaTeX style sheets that are included after the standard style sheets created -# by doxygen. Using this option one can overrule certain style aspects. Doxygen -# will copy the style sheet files to the output directory. -# Note: The order of the extra style sheet files is of importance (e.g. the last -# style sheet in the list overrules the setting of the previous ones in the -# list). -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_EXTRA_STYLESHEET = - -# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or -# other source files which should be copied to the LATEX_OUTPUT output -# directory. Note that the files will be copied as-is; there are no commands or -# markers available. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_EXTRA_FILES = - -# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is -# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will -# contain links (just like the HTML output) instead of page references. This -# makes the output suitable for online browsing using a PDF viewer. -# The default value is: YES. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -PDF_HYPERLINKS = YES - -# If the USE_PDFLATEX tag is set to YES, doxygen will use pdflatex to generate -# the PDF file directly from the LaTeX files. Set this option to YES, to get a -# higher quality PDF documentation. -# The default value is: YES. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -USE_PDFLATEX = YES - -# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode -# command to the generated LaTeX files. This will instruct LaTeX to keep running -# if errors occur, instead of asking the user for help. This option is also used -# when generating formulas in HTML. -# The default value is: NO. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_BATCHMODE = NO - -# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the -# index chapters (such as File Index, Compound Index, etc.) in the output. -# The default value is: NO. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_HIDE_INDICES = NO - -# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source -# code with syntax highlighting in the LaTeX output. -# -# Note that which sources are shown also depends on other settings such as -# SOURCE_BROWSER. -# The default value is: NO. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_SOURCE_CODE = NO - -# The LATEX_BIB_STYLE tag can be used to specify the style to use for the -# bibliography, e.g. plainnat, or ieeetr. See -# https://en.wikipedia.org/wiki/BibTeX and \cite for more info. -# The default value is: plain. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_BIB_STYLE = plain - -# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated -# page will contain the date and time when the page was generated. Setting this -# to NO can help when comparing the output of multiple runs. -# The default value is: NO. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_TIMESTAMP = NO - -# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute) -# path from which the emoji images will be read. If a relative path is entered, -# it will be relative to the LATEX_OUTPUT directory. If left blank the -# LATEX_OUTPUT directory will be used. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_EMOJI_DIRECTORY = - -#--------------------------------------------------------------------------- -# Configuration options related to the RTF output -#--------------------------------------------------------------------------- - -# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The -# RTF output is optimized for Word 97 and may not look too pretty with other RTF -# readers/editors. -# The default value is: NO. - -GENERATE_RTF = NO - -# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. If a -# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of -# it. -# The default directory is: rtf. -# This tag requires that the tag GENERATE_RTF is set to YES. - -RTF_OUTPUT = rtf - -# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF -# documents. This may be useful for small projects and may help to save some -# trees in general. -# The default value is: NO. -# This tag requires that the tag GENERATE_RTF is set to YES. - -COMPACT_RTF = NO - -# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated will -# contain hyperlink fields. The RTF file will contain links (just like the HTML -# output) instead of page references. This makes the output suitable for online -# browsing using Word or some other Word compatible readers that support those -# fields. -# -# Note: WordPad (write) and others do not support links. -# The default value is: NO. -# This tag requires that the tag GENERATE_RTF is set to YES. - -RTF_HYPERLINKS = NO - -# Load stylesheet definitions from file. Syntax is similar to doxygen's -# configuration file, i.e. a series of assignments. You only have to provide -# replacements, missing definitions are set to their default value. -# -# See also section "Doxygen usage" for information on how to generate the -# default style sheet that doxygen normally uses. -# This tag requires that the tag GENERATE_RTF is set to YES. - -RTF_STYLESHEET_FILE = - -# Set optional variables used in the generation of an RTF document. Syntax is -# similar to doxygen's configuration file. A template extensions file can be -# generated using doxygen -e rtf extensionFile. -# This tag requires that the tag GENERATE_RTF is set to YES. - -RTF_EXTENSIONS_FILE = - -# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code -# with syntax highlighting in the RTF output. -# -# Note that which sources are shown also depends on other settings such as -# SOURCE_BROWSER. -# The default value is: NO. -# This tag requires that the tag GENERATE_RTF is set to YES. - -RTF_SOURCE_CODE = NO - -#--------------------------------------------------------------------------- -# Configuration options related to the man page output -#--------------------------------------------------------------------------- - -# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for -# classes and files. -# The default value is: NO. - -GENERATE_MAN = NO - -# The MAN_OUTPUT tag is used to specify where the man pages will be put. If a -# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of -# it. A directory man3 will be created inside the directory specified by -# MAN_OUTPUT. -# The default directory is: man. -# This tag requires that the tag GENERATE_MAN is set to YES. - -MAN_OUTPUT = man - -# The MAN_EXTENSION tag determines the extension that is added to the generated -# man pages. In case the manual section does not start with a number, the number -# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is -# optional. -# The default value is: .3. -# This tag requires that the tag GENERATE_MAN is set to YES. - -MAN_EXTENSION = .3 - -# The MAN_SUBDIR tag determines the name of the directory created within -# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by -# MAN_EXTENSION with the initial . removed. -# This tag requires that the tag GENERATE_MAN is set to YES. - -MAN_SUBDIR = - -# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it -# will generate one additional man file for each entity documented in the real -# man page(s). These additional files only source the real man page, but without -# them the man command would be unable to find the correct page. -# The default value is: NO. -# This tag requires that the tag GENERATE_MAN is set to YES. - -MAN_LINKS = NO - -#--------------------------------------------------------------------------- -# Configuration options related to the XML output -#--------------------------------------------------------------------------- - -# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that -# captures the structure of the code including all documentation. -# The default value is: NO. - -GENERATE_XML = NO - -# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a -# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of -# it. -# The default directory is: xml. -# This tag requires that the tag GENERATE_XML is set to YES. - -XML_OUTPUT = xml - -# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program -# listings (including syntax highlighting and cross-referencing information) to -# the XML output. Note that enabling this will significantly increase the size -# of the XML output. -# The default value is: YES. -# This tag requires that the tag GENERATE_XML is set to YES. - -XML_PROGRAMLISTING = YES - -# If the XML_NS_MEMB_FILE_SCOPE tag is set to YES, doxygen will include -# namespace members in file scope as well, matching the HTML output. -# The default value is: NO. -# This tag requires that the tag GENERATE_XML is set to YES. - -XML_NS_MEMB_FILE_SCOPE = NO - -#--------------------------------------------------------------------------- -# Configuration options related to the DOCBOOK output -#--------------------------------------------------------------------------- - -# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files -# that can be used to generate PDF. -# The default value is: NO. - -GENERATE_DOCBOOK = NO - -# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put. -# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in -# front of it. -# The default directory is: docbook. -# This tag requires that the tag GENERATE_DOCBOOK is set to YES. - -DOCBOOK_OUTPUT = docbook - -# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the -# program listings (including syntax highlighting and cross-referencing -# information) to the DOCBOOK output. Note that enabling this will significantly -# increase the size of the DOCBOOK output. -# The default value is: NO. -# This tag requires that the tag GENERATE_DOCBOOK is set to YES. - -DOCBOOK_PROGRAMLISTING = NO - -#--------------------------------------------------------------------------- -# Configuration options for the AutoGen Definitions output -#--------------------------------------------------------------------------- - -# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an -# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures -# the structure of the code including all documentation. Note that this feature -# is still experimental and incomplete at the moment. -# The default value is: NO. - -GENERATE_AUTOGEN_DEF = NO - -#--------------------------------------------------------------------------- -# Configuration options related to the Perl module output -#--------------------------------------------------------------------------- - -# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module -# file that captures the structure of the code including all documentation. -# -# Note that this feature is still experimental and incomplete at the moment. -# The default value is: NO. - -GENERATE_PERLMOD = NO - -# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary -# Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI -# output from the Perl module output. -# The default value is: NO. -# This tag requires that the tag GENERATE_PERLMOD is set to YES. - -PERLMOD_LATEX = NO - -# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely -# formatted so it can be parsed by a human reader. This is useful if you want to -# understand what is going on. On the other hand, if this tag is set to NO, the -# size of the Perl module output will be much smaller and Perl will parse it -# just the same. -# The default value is: YES. -# This tag requires that the tag GENERATE_PERLMOD is set to YES. - -PERLMOD_PRETTY = YES - -# The names of the make variables in the generated doxyrules.make file are -# prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. This is useful -# so different doxyrules.make files included by the same Makefile don't -# overwrite each other's variables. -# This tag requires that the tag GENERATE_PERLMOD is set to YES. - -PERLMOD_MAKEVAR_PREFIX = - -#--------------------------------------------------------------------------- -# Configuration options related to the preprocessor -#--------------------------------------------------------------------------- - -# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all -# C-preprocessor directives found in the sources and include files. -# The default value is: YES. - -ENABLE_PREPROCESSING = YES - -# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names -# in the source code. If set to NO, only conditional compilation will be -# performed. Macro expansion can be done in a controlled way by setting -# EXPAND_ONLY_PREDEF to YES. -# The default value is: NO. -# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. - -MACRO_EXPANSION = NO - -# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then -# the macro expansion is limited to the macros specified with the PREDEFINED and -# EXPAND_AS_DEFINED tags. -# The default value is: NO. -# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. - -EXPAND_ONLY_PREDEF = NO - -# If the SEARCH_INCLUDES tag is set to YES, the include files in the -# INCLUDE_PATH will be searched if a #include is found. -# The default value is: YES. -# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. - -SEARCH_INCLUDES = YES - -# The INCLUDE_PATH tag can be used to specify one or more directories that -# contain include files that are not input files but should be processed by the -# preprocessor. -# This tag requires that the tag SEARCH_INCLUDES is set to YES. - -INCLUDE_PATH = - -# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard -# patterns (like *.h and *.hpp) to filter out the header-files in the -# directories. If left blank, the patterns specified with FILE_PATTERNS will be -# used. -# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. - -INCLUDE_FILE_PATTERNS = - -# The PREDEFINED tag can be used to specify one or more macro names that are -# defined before the preprocessor is started (similar to the -D option of e.g. -# gcc). The argument of the tag is a list of macros of the form: name or -# name=definition (no spaces). If the definition and the "=" are omitted, "=1" -# is assumed. To prevent a macro definition from being undefined via #undef or -# recursively expanded use the := operator instead of the = operator. -# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. - -PREDEFINED = - -# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this -# tag can be used to specify a list of macro names that should be expanded. The -# macro definition that is found in the sources will be used. Use the PREDEFINED -# tag if you want to use a different macro definition that overrules the -# definition found in the source code. -# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. - -EXPAND_AS_DEFINED = - -# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will -# remove all references to function-like macros that are alone on a line, have -# an all uppercase name, and do not end with a semicolon. Such function macros -# are typically used for boiler-plate code, and will confuse the parser if not -# removed. -# The default value is: YES. -# This tag requires that the tag ENABLE_PREPROCESSING is set to YES. - -SKIP_FUNCTION_MACROS = YES - -#--------------------------------------------------------------------------- -# Configuration options related to external references -#--------------------------------------------------------------------------- - -# The TAGFILES tag can be used to specify one or more tag files. For each tag -# file the location of the external documentation should be added. The format of -# a tag file without this location is as follows: -# TAGFILES = file1 file2 ... -# Adding location for the tag files is done as follows: -# TAGFILES = file1=loc1 "file2 = loc2" ... -# where loc1 and loc2 can be relative or absolute paths or URLs. See the -# section "Linking to external documentation" for more information about the use -# of tag files. -# Note: Each tag file must have a unique name (where the name does NOT include -# the path). If a tag file is not located in the directory in which doxygen is -# run, you must also specify the path to the tagfile here. - -TAGFILES = - -# When a file name is specified after GENERATE_TAGFILE, doxygen will create a -# tag file that is based on the input files it reads. See section "Linking to -# external documentation" for more information about the usage of tag files. - -GENERATE_TAGFILE = - -# If the ALLEXTERNALS tag is set to YES, all external class will be listed in -# the class index. If set to NO, only the inherited external classes will be -# listed. -# The default value is: NO. - -ALLEXTERNALS = NO - -# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed -# in the modules index. If set to NO, only the current project's groups will be -# listed. -# The default value is: YES. - -EXTERNAL_GROUPS = YES - -# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in -# the related pages index. If set to NO, only the current project's pages will -# be listed. -# The default value is: YES. - -EXTERNAL_PAGES = YES - -# The PERL_PATH should be the absolute path and name of the perl script -# interpreter (i.e. the result of 'which perl'). -# The default file (with absolute path) is: /usr/bin/perl. - -PERL_PATH = /usr/bin/perl - -#--------------------------------------------------------------------------- -# Configuration options related to the dot tool -#--------------------------------------------------------------------------- - -# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram -# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to -# NO turns the diagrams off. Note that this option also works with HAVE_DOT -# disabled, but it is recommended to install and use dot, since it yields more -# powerful graphs. -# The default value is: YES. - -CLASS_DIAGRAMS = YES - -# You can define message sequence charts within doxygen comments using the \msc -# command. Doxygen will then run the mscgen tool (see: -# http://www.mcternan.me.uk/mscgen/)) to produce the chart and insert it in the -# documentation. The MSCGEN_PATH tag allows you to specify the directory where -# the mscgen tool resides. If left empty the tool is assumed to be found in the -# default search path. - -MSCGEN_PATH = - -# You can include diagrams made with dia in doxygen documentation. Doxygen will -# then run dia to produce the diagram and insert it in the documentation. The -# DIA_PATH tag allows you to specify the directory where the dia binary resides. -# If left empty dia is assumed to be found in the default search path. - -DIA_PATH = - -# If set to YES the inheritance and collaboration graphs will hide inheritance -# and usage relations if the target is undocumented or is not a class. -# The default value is: YES. - -HIDE_UNDOC_RELATIONS = YES - -# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is -# available from the path. This tool is part of Graphviz (see: -# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent -# Bell Labs. The other options in this section have no effect if this option is -# set to NO -# The default value is: NO. - -HAVE_DOT = NO - -# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed -# to run in parallel. When set to 0 doxygen will base this on the number of -# processors available in the system. You can set it explicitly to a value -# larger than 0 to get control over the balance between CPU load and processing -# speed. -# Minimum value: 0, maximum value: 32, default value: 0. -# This tag requires that the tag HAVE_DOT is set to YES. - -DOT_NUM_THREADS = 0 - -# When you want a differently looking font in the dot files that doxygen -# generates you can specify the font name using DOT_FONTNAME. You need to make -# sure dot is able to find the font, which can be done by putting it in a -# standard location or by setting the DOTFONTPATH environment variable or by -# setting DOT_FONTPATH to the directory containing the font. -# The default value is: Helvetica. -# This tag requires that the tag HAVE_DOT is set to YES. - -DOT_FONTNAME = Helvetica - -# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of -# dot graphs. -# Minimum value: 4, maximum value: 24, default value: 10. -# This tag requires that the tag HAVE_DOT is set to YES. - -DOT_FONTSIZE = 10 - -# By default doxygen will tell dot to use the default font as specified with -# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set -# the path where dot can find it using this tag. -# This tag requires that the tag HAVE_DOT is set to YES. - -DOT_FONTPATH = - -# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for -# each documented class showing the direct and indirect inheritance relations. -# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO. -# The default value is: YES. -# This tag requires that the tag HAVE_DOT is set to YES. - -CLASS_GRAPH = YES - -# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a -# graph for each documented class showing the direct and indirect implementation -# dependencies (inheritance, containment, and class references variables) of the -# class with other documented classes. -# The default value is: YES. -# This tag requires that the tag HAVE_DOT is set to YES. - -COLLABORATION_GRAPH = YES - -# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for -# groups, showing the direct groups dependencies. -# The default value is: YES. -# This tag requires that the tag HAVE_DOT is set to YES. - -GROUP_GRAPHS = YES - -# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and -# collaboration diagrams in a style similar to the OMG's Unified Modeling -# Language. -# The default value is: NO. -# This tag requires that the tag HAVE_DOT is set to YES. - -UML_LOOK = NO - -# If the UML_LOOK tag is enabled, the fields and methods are shown inside the -# class node. If there are many fields or methods and many nodes the graph may -# become too big to be useful. The UML_LIMIT_NUM_FIELDS threshold limits the -# number of items for each type to make the size more manageable. Set this to 0 -# for no limit. Note that the threshold may be exceeded by 50% before the limit -# is enforced. So when you set the threshold to 10, up to 15 fields may appear, -# but if the number exceeds 15, the total amount of fields shown is limited to -# 10. -# Minimum value: 0, maximum value: 100, default value: 10. -# This tag requires that the tag HAVE_DOT is set to YES. - -UML_LIMIT_NUM_FIELDS = 10 - -# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and -# collaboration graphs will show the relations between templates and their -# instances. -# The default value is: NO. -# This tag requires that the tag HAVE_DOT is set to YES. - -TEMPLATE_RELATIONS = NO - -# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to -# YES then doxygen will generate a graph for each documented file showing the -# direct and indirect include dependencies of the file with other documented -# files. -# The default value is: YES. -# This tag requires that the tag HAVE_DOT is set to YES. - -INCLUDE_GRAPH = YES - -# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are -# set to YES then doxygen will generate a graph for each documented file showing -# the direct and indirect include dependencies of the file with other documented -# files. -# The default value is: YES. -# This tag requires that the tag HAVE_DOT is set to YES. - -INCLUDED_BY_GRAPH = YES - -# If the CALL_GRAPH tag is set to YES then doxygen will generate a call -# dependency graph for every global function or class method. -# -# Note that enabling this option will significantly increase the time of a run. -# So in most cases it will be better to enable call graphs for selected -# functions only using the \callgraph command. Disabling a call graph can be -# accomplished by means of the command \hidecallgraph. -# The default value is: NO. -# This tag requires that the tag HAVE_DOT is set to YES. - -CALL_GRAPH = NO - -# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller -# dependency graph for every global function or class method. -# -# Note that enabling this option will significantly increase the time of a run. -# So in most cases it will be better to enable caller graphs for selected -# functions only using the \callergraph command. Disabling a caller graph can be -# accomplished by means of the command \hidecallergraph. -# The default value is: NO. -# This tag requires that the tag HAVE_DOT is set to YES. - -CALLER_GRAPH = NO - -# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical -# hierarchy of all classes instead of a textual one. -# The default value is: YES. -# This tag requires that the tag HAVE_DOT is set to YES. - -GRAPHICAL_HIERARCHY = YES - -# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the -# dependencies a directory has on other directories in a graphical way. The -# dependency relations are determined by the #include relations between the -# files in the directories. -# The default value is: YES. -# This tag requires that the tag HAVE_DOT is set to YES. - -DIRECTORY_GRAPH = YES - -# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images -# generated by dot. For an explanation of the image formats see the section -# output formats in the documentation of the dot tool (Graphviz (see: -# http://www.graphviz.org/)). -# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order -# to make the SVG files visible in IE 9+ (other browsers do not have this -# requirement). -# Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo, -# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and -# png:gdiplus:gdiplus. -# The default value is: png. -# This tag requires that the tag HAVE_DOT is set to YES. - -DOT_IMAGE_FORMAT = png - -# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to -# enable generation of interactive SVG images that allow zooming and panning. -# -# Note that this requires a modern browser other than Internet Explorer. Tested -# and working are Firefox, Chrome, Safari, and Opera. -# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make -# the SVG files visible. Older versions of IE do not have SVG support. -# The default value is: NO. -# This tag requires that the tag HAVE_DOT is set to YES. - -INTERACTIVE_SVG = NO - -# The DOT_PATH tag can be used to specify the path where the dot tool can be -# found. If left blank, it is assumed the dot tool can be found in the path. -# This tag requires that the tag HAVE_DOT is set to YES. - -DOT_PATH = - -# The DOTFILE_DIRS tag can be used to specify one or more directories that -# contain dot files that are included in the documentation (see the \dotfile -# command). -# This tag requires that the tag HAVE_DOT is set to YES. - -DOTFILE_DIRS = - -# The MSCFILE_DIRS tag can be used to specify one or more directories that -# contain msc files that are included in the documentation (see the \mscfile -# command). - -MSCFILE_DIRS = - -# The DIAFILE_DIRS tag can be used to specify one or more directories that -# contain dia files that are included in the documentation (see the \diafile -# command). - -DIAFILE_DIRS = - -# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the -# path where java can find the plantuml.jar file. If left blank, it is assumed -# PlantUML is not used or called during a preprocessing step. Doxygen will -# generate a warning when it encounters a \startuml command in this case and -# will not generate output for the diagram. - -PLANTUML_JAR_PATH = - -# When using plantuml, the PLANTUML_CFG_FILE tag can be used to specify a -# configuration file for plantuml. - -PLANTUML_CFG_FILE = - -# When using plantuml, the specified paths are searched for files specified by -# the !include statement in a plantuml block. - -PLANTUML_INCLUDE_PATH = - -# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes -# that will be shown in the graph. If the number of nodes in a graph becomes -# larger than this value, doxygen will truncate the graph, which is visualized -# by representing a node as a red box. Note that doxygen if the number of direct -# children of the root node in a graph is already larger than -# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that -# the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH. -# Minimum value: 0, maximum value: 10000, default value: 50. -# This tag requires that the tag HAVE_DOT is set to YES. - -DOT_GRAPH_MAX_NODES = 50 - -# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs -# generated by dot. A depth value of 3 means that only nodes reachable from the -# root by following a path via at most 3 edges will be shown. Nodes that lay -# further from the root node will be omitted. Note that setting this option to 1 -# or 2 may greatly reduce the computation time needed for large code bases. Also -# note that the size of a graph can be further restricted by -# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction. -# Minimum value: 0, maximum value: 1000, default value: 0. -# This tag requires that the tag HAVE_DOT is set to YES. - -MAX_DOT_GRAPH_DEPTH = 0 - -# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent -# background. This is disabled by default, because dot on Windows does not seem -# to support this out of the box. -# -# Warning: Depending on the platform used, enabling this option may lead to -# badly anti-aliased labels on the edges of a graph (i.e. they become hard to -# read). -# The default value is: NO. -# This tag requires that the tag HAVE_DOT is set to YES. - -DOT_TRANSPARENT = NO - -# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output -# files in one run (i.e. multiple -o and -T options on the command line). This -# makes dot run faster, but since only newer versions of dot (>1.8.10) support -# this, this feature is disabled by default. -# The default value is: NO. -# This tag requires that the tag HAVE_DOT is set to YES. - -DOT_MULTI_TARGETS = NO - -# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page -# explaining the meaning of the various boxes and arrows in the dot generated -# graphs. -# The default value is: YES. -# This tag requires that the tag HAVE_DOT is set to YES. - -GENERATE_LEGEND = YES - -# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate dot -# files that are used to generate the various graphs. -# The default value is: YES. -# This tag requires that the tag HAVE_DOT is set to YES. - -DOT_CLEANUP = YES diff --git a/DvdLib/DvdLib.csproj b/DvdLib/DvdLib.csproj index b2c752d0c..9dbaa9e2f 100644 --- a/DvdLib/DvdLib.csproj +++ b/DvdLib/DvdLib.csproj @@ -11,6 +11,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> </Project> diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index 4f8c89e48..d22fc2177 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -289,7 +289,7 @@ namespace Emby.Dlna.ContentDirectory var childrenResult = GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount); totalCount = childrenResult.TotalRecordCount; - provided = childrenResult.Items.Length; + provided = childrenResult.Items.Count; foreach (var i in childrenResult.Items) { @@ -309,6 +309,7 @@ namespace Emby.Dlna.ContentDirectory } } } + writer.WriteFullEndElement(); //writer.WriteEndDocument(); } @@ -386,7 +387,7 @@ namespace Emby.Dlna.ContentDirectory totalCount = childrenResult.TotalRecordCount; - provided = childrenResult.Items.Length; + provided = childrenResult.Items.Count; var dlnaOptions = _config.GetDlnaConfiguration(); @@ -677,7 +678,7 @@ namespace Emby.Dlna.ContentDirectory return new QueryResult<ServerItem> { - Items = list.ToArray(), + Items = list, TotalRecordCount = list.Count }; } @@ -755,7 +756,7 @@ namespace Emby.Dlna.ContentDirectory return new QueryResult<ServerItem> { - Items = list.ToArray(), + Items = list, TotalRecordCount = list.Count }; } @@ -860,7 +861,7 @@ namespace Emby.Dlna.ContentDirectory return new QueryResult<ServerItem> { - Items = list.ToArray(), + Items = list, TotalRecordCount = list.Count }; } diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index a21aff9f9..85ef9d482 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -158,7 +158,7 @@ namespace Emby.Dlna.Didl AddGeneralProperties(item, null, context, writer, filter); - AddSamsungBookmarkInfo(item, user, writer); + AddSamsungBookmarkInfo(item, user, writer, streamInfo); // refID? // storeAttribute(itemNode, object, ClassProperties.REF_ID, false); @@ -581,7 +581,7 @@ namespace Emby.Dlna.Didl writer.WriteFullEndElement(); } - private void AddSamsungBookmarkInfo(BaseItem item, User user, XmlWriter writer) + private void AddSamsungBookmarkInfo(BaseItem item, User user, XmlWriter writer, StreamInfo streamInfo) { if (!item.SupportsPositionTicksResume || item is Folder) { @@ -605,10 +605,11 @@ namespace Emby.Dlna.Didl } var userdata = _userDataManager.GetUserData(user, item); + var playbackPositionTicks = (streamInfo != null && streamInfo.StartPositionTicks > 0) ? streamInfo.StartPositionTicks : userdata.PlaybackPositionTicks; - if (userdata.PlaybackPositionTicks > 0) + if (playbackPositionTicks > 0) { - var elementValue = string.Format("BM={0}", Convert.ToInt32(TimeSpan.FromTicks(userdata.PlaybackPositionTicks).TotalSeconds).ToString(_usCulture)); + var elementValue = string.Format("BM={0}", Convert.ToInt32(TimeSpan.FromTicks(playbackPositionTicks).TotalSeconds).ToString(_usCulture)); AddValue(writer, "sec", "dcmInfo", elementValue, secAttribute.Value); } } @@ -1082,7 +1083,7 @@ namespace Emby.Dlna.Didl public static string GetClientId(Guid idValue, StubType? stubType) { - var id = idValue.ToString("N"); + var id = idValue.ToString("N", CultureInfo.InvariantCulture); if (stubType.HasValue) { @@ -1096,7 +1097,7 @@ namespace Emby.Dlna.Didl { var url = string.Format("{0}/Items/{1}/Images/{2}/0/{3}/{4}/{5}/{6}/0/0", _serverAddress, - info.ItemId.ToString("N"), + info.ItemId.ToString("N", CultureInfo.InvariantCulture), info.Type, info.ImageTag, format, diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index 2b76d2702..d5d788021 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection; @@ -300,7 +301,7 @@ namespace Emby.Dlna profile = ReserializeProfile(tempProfile); - profile.Id = path.ToLowerInvariant().GetMD5().ToString("N"); + profile.Id = path.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture); _profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile); @@ -352,7 +353,7 @@ namespace Emby.Dlna Info = new DeviceProfileInfo { - Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N"), + Id = file.FullName.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture), Name = _fileSystem.GetFileNameWithoutExtension(file), Type = type } diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj index 4c07087c5..34b49120b 100644 --- a/Emby.Dlna/Emby.Dlna.csproj +++ b/Emby.Dlna/Emby.Dlna.csproj @@ -14,6 +14,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> <ItemGroup> diff --git a/Emby.Dlna/Eventing/EventManager.cs b/Emby.Dlna/Eventing/EventManager.cs index b4ff3ec1d..4b542a820 100644 --- a/Emby.Dlna/Eventing/EventManager.cs +++ b/Emby.Dlna/Eventing/EventManager.cs @@ -55,7 +55,7 @@ namespace Emby.Dlna.Eventing public EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl) { var timeout = ParseTimeout(requestedTimeoutString) ?? 300; - var id = "uuid:" + Guid.NewGuid().ToString("N"); + var id = "uuid:" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); // Remove logging for now because some devices are sending this very frequently // TODO re-enable with dlna debug logging setting diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs index 206a873e1..77bde0ca2 100644 --- a/Emby.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -1,5 +1,6 @@ using System; using System.Net.Sockets; +using System.Globalization; using System.Threading; using System.Threading.Tasks; using Emby.Dlna.PlayTo; @@ -307,7 +308,7 @@ namespace Emby.Dlna.Main { guid = text.GetMD5(); } - return guid.ToString("N"); + return guid.ToString("N", CultureInfo.InvariantCulture); } private void SetProperies(SsdpDevice device, string fullDeviceType) diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs index c0a441871..a3a013096 100644 --- a/Emby.Dlna/PlayTo/PlayToManager.cs +++ b/Emby.Dlna/PlayTo/PlayToManager.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Linq; using System.Net; using System.Threading; @@ -141,7 +142,7 @@ namespace Emby.Dlna.PlayTo return usn; } - return usn.GetMD5().ToString("N"); + return usn.GetMD5().ToString("N", CultureInfo.InvariantCulture); } private async Task AddDevice(UpnpDeviceInfo info, string location, CancellationToken cancellationToken) @@ -156,7 +157,7 @@ namespace Emby.Dlna.PlayTo } else { - uuid = location.GetMD5().ToString("N"); + uuid = location.GetMD5().ToString("N", CultureInfo.InvariantCulture); } var sessionInfo = _sessionManager.LogSessionActivity("DLNA", _appHost.ApplicationVersion, uuid, null, uri.OriginalString, null); diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs index 22aaa6885..66c634150 100644 --- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs +++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs @@ -16,6 +16,8 @@ namespace Emby.Dlna.PlayTo private const string USERAGENT = "Microsoft-Windows/6.2 UPnP/1.0 Microsoft-DLNA DLNADOC/1.50"; private const string FriendlyName = "Jellyfin"; + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + private readonly IHttpClient _httpClient; private readonly IServerConfigurationManager _config; @@ -25,7 +27,8 @@ namespace Emby.Dlna.PlayTo _config = config; } - public async Task<XDocument> SendCommandAsync(string baseUrl, + public async Task<XDocument> SendCommandAsync( + string baseUrl, DeviceService service, string command, string postData, @@ -35,12 +38,20 @@ namespace Emby.Dlna.PlayTo var cancellationToken = CancellationToken.None; var url = NormalizeServiceUrl(baseUrl, service.ControlUrl); - using (var response = await PostSoapDataAsync(url, '\"' + service.ServiceType + '#' + command + '\"', postData, header, logRequest, cancellationToken) + using (var response = await PostSoapDataAsync( + url, + $"\"{service.ServiceType}#{command}\"", + postData, + header, + logRequest, + cancellationToken) .ConfigureAwait(false)) using (var stream = response.Content) using (var reader = new StreamReader(stream, Encoding.UTF8)) { - return XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace); + return XDocument.Parse( + await reader.ReadToEndAsync().ConfigureAwait(false), + LoadOptions.PreserveWhitespace); } } @@ -58,9 +69,8 @@ namespace Emby.Dlna.PlayTo return baseUrl + serviceUrl; } - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - - public async Task SubscribeAsync(string url, + public async Task SubscribeAsync( + string url, string ip, int port, string localIp, @@ -73,9 +83,6 @@ namespace Emby.Dlna.PlayTo UserAgent = USERAGENT, LogErrorResponseBody = true, BufferContent = false, - - // The periodic requests may keep some devices awake - LogRequestAsDebug = true }; options.RequestHeaders["HOST"] = ip + ":" + port.ToString(_usCulture); @@ -98,23 +105,18 @@ namespace Emby.Dlna.PlayTo LogErrorResponseBody = true, BufferContent = false, - // The periodic requests may keep some devices awake - LogRequestAsDebug = true, - CancellationToken = cancellationToken }; options.RequestHeaders["FriendlyName.DLNA.ORG"] = FriendlyName; using (var response = await _httpClient.SendAsync(options, "GET").ConfigureAwait(false)) + using (var stream = response.Content) + using (var reader = new StreamReader(stream, Encoding.UTF8)) { - using (var stream = response.Content) - { - using (var reader = new StreamReader(stream, Encoding.UTF8)) - { - return XDocument.Parse(reader.ReadToEnd(), LoadOptions.PreserveWhitespace); - } - } + return XDocument.Parse( + await reader.ReadToEndAsync().ConfigureAwait(false), + LoadOptions.PreserveWhitespace); } } @@ -128,20 +130,16 @@ namespace Emby.Dlna.PlayTo { if (soapAction[0] != '\"') { - soapAction = '\"' + soapAction + '\"'; + soapAction = $"\"{soapAction}\""; } var options = new HttpRequestOptions { Url = url, UserAgent = USERAGENT, - LogRequest = logRequest || _config.GetDlnaConfiguration().EnableDebugLog, LogErrorResponseBody = true, BufferContent = false, - // The periodic requests may keep some devices awake - LogRequestAsDebug = true, - CancellationToken = cancellationToken }; diff --git a/Emby.Drawing/Emby.Drawing.csproj b/Emby.Drawing/Emby.Drawing.csproj index 9f97baf77..2e539f2c7 100644 --- a/Emby.Drawing/Emby.Drawing.csproj +++ b/Emby.Drawing/Emby.Drawing.csproj @@ -3,6 +3,8 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> <ItemGroup> @@ -15,4 +17,9 @@ <Compile Include="..\SharedVersion.cs" /> </ItemGroup> + <PropertyGroup> + <!-- We need at least C# 7.1 for the "default literal" feature--> + <LangVersion>latest</LangVersion> + </PropertyGroup> + </Project> diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs index 6d209d8d0..ce8089e59 100644 --- a/Emby.Drawing/ImageProcessor.cs +++ b/Emby.Drawing/ImageProcessor.cs @@ -22,42 +22,47 @@ using Microsoft.Extensions.Logging; namespace Emby.Drawing { /// <summary> - /// Class ImageProcessor + /// Class ImageProcessor. /// </summary> public class ImageProcessor : IImageProcessor, IDisposable { - /// <summary> - /// The us culture - /// </summary> - protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); + // Increment this when there's a change requiring caches to be invalidated + private const string Version = "3"; - /// <summary> - /// Gets the list of currently registered image processors - /// Image processors are specialized metadata providers that run after the normal ones - /// </summary> - /// <value>The image enhancers.</value> - public IImageEnhancer[] ImageEnhancers { get; private set; } + private static readonly HashSet<string> _transparentImageTypes + = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" }; /// <summary> /// The _logger /// </summary> private readonly ILogger _logger; - private readonly IFileSystem _fileSystem; private readonly IServerApplicationPaths _appPaths; private IImageEncoder _imageEncoder; private readonly Func<ILibraryManager> _libraryManager; private readonly Func<IMediaEncoder> _mediaEncoder; + private readonly Dictionary<string, LockInfo> _locks = new Dictionary<string, LockInfo>(); + private bool _disposed = false; + + /// <summary> + /// + /// </summary> + /// <param name="logger"></param> + /// <param name="appPaths"></param> + /// <param name="fileSystem"></param> + /// <param name="imageEncoder"></param> + /// <param name="libraryManager"></param> + /// <param name="mediaEncoder"></param> public ImageProcessor( - ILoggerFactory loggerFactory, + ILogger<ImageProcessor> logger, IServerApplicationPaths appPaths, IFileSystem fileSystem, IImageEncoder imageEncoder, Func<ILibraryManager> libraryManager, Func<IMediaEncoder> mediaEncoder) { - _logger = loggerFactory.CreateLogger(nameof(ImageProcessor)); + _logger = logger; _fileSystem = fileSystem; _imageEncoder = imageEncoder; _libraryManager = libraryManager; @@ -69,20 +74,11 @@ namespace Emby.Drawing ImageHelper.ImageProcessor = this; } - public IImageEncoder ImageEncoder - { - get => _imageEncoder; - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } + private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images"); - _imageEncoder = value; - } - } + private string EnhancedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "enhanced-images"); + /// <inheritdoc /> public IReadOnlyCollection<string> SupportedInputFormats => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { @@ -115,18 +111,20 @@ namespace Emby.Drawing "wbmp" }; + /// <inheritdoc /> + public IReadOnlyCollection<IImageEnhancer> ImageEnhancers { get; set; } + /// <inheritdoc /> public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation; - private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images"); - - private string EnhancedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "enhanced-images"); - - public void AddParts(IEnumerable<IImageEnhancer> enhancers) + /// <inheritdoc /> + public IImageEncoder ImageEncoder { - ImageEnhancers = enhancers.ToArray(); + get => _imageEncoder; + set => _imageEncoder = value ?? throw new ArgumentNullException(nameof(value)); } + /// <inheritdoc /> public async Task ProcessImage(ImageProcessingOptions options, Stream toStream) { var file = await ProcessImage(options).ConfigureAwait(false); @@ -137,15 +135,15 @@ namespace Emby.Drawing } } + /// <inheritdoc /> public IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats() => _imageEncoder.SupportedOutputFormats; - private static readonly HashSet<string> TransparentImageTypes - = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" }; - + /// <inheritdoc /> public bool SupportsTransparency(string path) - => TransparentImageTypes.Contains(Path.GetExtension(path)); + => _transparentImageTypes.Contains(Path.GetExtension(path)); + /// <inheritdoc /> public async Task<(string path, string mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options) { if (options == null) @@ -187,9 +185,9 @@ namespace Emby.Drawing } dateModified = supportedImageInfo.dateModified; - bool requiresTransparency = TransparentImageTypes.Contains(Path.GetExtension(originalImagePath)); + bool requiresTransparency = _transparentImageTypes.Contains(Path.GetExtension(originalImagePath)); - if (options.Enhancers.Length > 0) + if (options.Enhancers.Count > 0) { if (item == null) { @@ -279,7 +277,7 @@ namespace Emby.Drawing } } - private ImageFormat GetOutputFormat(ImageFormat[] clientSupportedFormats, bool requiresTransparency) + private ImageFormat GetOutputFormat(IReadOnlyCollection<ImageFormat> clientSupportedFormats, bool requiresTransparency) { var serverFormats = GetSupportedImageOutputFormats(); @@ -321,11 +319,6 @@ namespace Emby.Drawing } /// <summary> - /// Increment this when there's a change requiring caches to be invalidated - /// </summary> - private const string Version = "3"; - - /// <summary> /// Gets the cache file path based on a set of parameters /// </summary> private string GetCacheFilePath(string originalPath, ImageDimensions outputSize, int quality, DateTime dateModified, ImageFormat format, bool addPlayedIndicator, double percentPlayed, int? unwatchedCount, int? blur, string backgroundColor, string foregroundLayer) @@ -372,9 +365,11 @@ namespace Emby.Drawing return GetCachePath(ResizedImageCachePath, filename, "." + format.ToString().ToLowerInvariant()); } + /// <inheritdoc /> public ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info) => GetImageDimensions(item, info, true); + /// <inheritdoc /> public ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info, bool updateItem) { int width = info.Width; @@ -400,26 +395,19 @@ namespace Emby.Drawing return size; } - /// <summary> - /// Gets the size of the image. - /// </summary> + /// <inheritdoc /> public ImageDimensions GetImageDimensions(string path) => _imageEncoder.GetImageSize(path); - /// <summary> - /// Gets the image cache tag. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="image">The image.</param> - /// <returns>Guid.</returns> - /// <exception cref="ArgumentNullException">item</exception> + /// <inheritdoc /> public string GetImageCacheTag(BaseItem item, ItemImageInfo image) { - var supportedEnhancers = GetSupportedEnhancers(item, image.Type); + var supportedEnhancers = GetSupportedEnhancers(item, image.Type).ToArray(); return GetImageCacheTag(item, image, supportedEnhancers); } + /// <inheritdoc /> public string GetImageCacheTag(BaseItem item, ChapterInfo chapter) { try @@ -437,31 +425,24 @@ namespace Emby.Drawing } } - /// <summary> - /// Gets the image cache tag. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="image">The image.</param> - /// <param name="imageEnhancers">The image enhancers.</param> - /// <returns>Guid.</returns> - /// <exception cref="ArgumentNullException">item</exception> - public string GetImageCacheTag(BaseItem item, ItemImageInfo image, IImageEnhancer[] imageEnhancers) + /// <inheritdoc /> + public string GetImageCacheTag(BaseItem item, ItemImageInfo image, IReadOnlyCollection<IImageEnhancer> imageEnhancers) { string originalImagePath = image.Path; DateTime dateModified = image.DateModified; ImageType imageType = image.Type; // Optimization - if (imageEnhancers.Length == 0) + if (imageEnhancers.Count == 0) { - return (originalImagePath + dateModified.Ticks).GetMD5().ToString("N"); + return (originalImagePath + dateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture); } // Cache name is created with supported enhancers combined with the last config change so we pick up new config changes var cacheKeys = imageEnhancers.Select(i => i.GetConfigurationCacheKey(item, imageType)).ToList(); cacheKeys.Add(originalImagePath + dateModified.Ticks); - return string.Join("|", cacheKeys).GetMD5().ToString("N"); + return string.Join("|", cacheKeys).GetMD5().ToString("N", CultureInfo.InvariantCulture); } private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified) @@ -480,7 +461,7 @@ namespace Emby.Drawing { try { - string filename = (originalImagePath + dateModified.Ticks.ToString(UsCulture)).GetMD5().ToString("N"); + string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture); string cacheExtension = _mediaEncoder().SupportsEncoder("libwebp") ? ".webp" : ".png"; var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension); @@ -507,16 +488,10 @@ namespace Emby.Drawing return (originalImagePath, dateModified); } - /// <summary> - /// Gets the enhanced image. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="imageType">Type of the image.</param> - /// <param name="imageIndex">Index of the image.</param> - /// <returns>Task{System.String}.</returns> + /// <inheritdoc /> public async Task<string> GetEnhancedImage(BaseItem item, ImageType imageType, int imageIndex) { - var enhancers = GetSupportedEnhancers(item, imageType); + var enhancers = GetSupportedEnhancers(item, imageType).ToArray(); ItemImageInfo imageInfo = item.GetImageInfo(imageType, imageIndex); @@ -532,7 +507,7 @@ namespace Emby.Drawing bool inputImageSupportsTransparency, BaseItem item, int imageIndex, - IImageEnhancer[] enhancers, + IReadOnlyCollection<IImageEnhancer> enhancers, CancellationToken cancellationToken) { var originalImagePath = image.Path; @@ -573,6 +548,7 @@ namespace Emby.Drawing /// <param name="imageIndex">Index of the image.</param> /// <param name="supportedEnhancers">The supported enhancers.</param> /// <param name="cacheGuid">The cache unique identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task<System.String>.</returns> /// <exception cref="ArgumentNullException"> /// originalImagePath @@ -584,9 +560,9 @@ namespace Emby.Drawing BaseItem item, ImageType imageType, int imageIndex, - IImageEnhancer[] supportedEnhancers, + IReadOnlyCollection<IImageEnhancer> supportedEnhancers, string cacheGuid, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(originalImagePath)) { @@ -680,6 +656,7 @@ namespace Emby.Drawing { throw new ArgumentNullException(nameof(path)); } + if (string.IsNullOrEmpty(uniqueName)) { throw new ArgumentNullException(nameof(uniqueName)); @@ -722,6 +699,7 @@ namespace Emby.Drawing return Path.Combine(path, prefix, filename); } + /// <inheritdoc /> public void CreateImageCollage(ImageCollageOptions options) { _logger.LogInformation("Creating image collage and saving to {Path}", options.OutputPath); @@ -731,38 +709,25 @@ namespace Emby.Drawing _logger.LogInformation("Completed creation of image collage and saved to {Path}", options.OutputPath); } - public IImageEnhancer[] GetSupportedEnhancers(BaseItem item, ImageType imageType) + /// <inheritdoc /> + public IEnumerable<IImageEnhancer> GetSupportedEnhancers(BaseItem item, ImageType imageType) { - List<IImageEnhancer> list = null; - foreach (var i in ImageEnhancers) { - try - { - if (i.Supports(item, imageType)) - { - if (list == null) - { - list = new List<IImageEnhancer>(); - } - list.Add(i); - } - } - catch (Exception ex) + if (i.Supports(item, imageType)) { - _logger.LogError(ex, "Error in image enhancer: {0}", i.GetType().Name); + yield return i; } } - - return list == null ? Array.Empty<IImageEnhancer>() : list.ToArray(); } - private Dictionary<string, LockInfo> _locks = new Dictionary<string, LockInfo>(); + private class LockInfo { public SemaphoreSlim Lock = new SemaphoreSlim(1, 1); public int Count = 1; } + private LockInfo GetLock(string key) { lock (_locks) @@ -795,7 +760,7 @@ namespace Emby.Drawing } } - private bool _disposed; + /// <inheritdoc /> public void Dispose() { _disposed = true; diff --git a/Emby.Drawing/NullImageEncoder.cs b/Emby.Drawing/NullImageEncoder.cs index fc4a5af9f..5af7f1622 100644 --- a/Emby.Drawing/NullImageEncoder.cs +++ b/Emby.Drawing/NullImageEncoder.cs @@ -5,36 +5,40 @@ using MediaBrowser.Model.Drawing; namespace Emby.Drawing { + /// <summary> + /// A fallback implementation of <see cref="IImageEncoder" />. + /// </summary> public class NullImageEncoder : IImageEncoder { + /// <inheritdoc /> public IReadOnlyCollection<string> SupportedInputFormats => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "png", "jpeg", "jpg" }; + /// <inheritdoc /> public IReadOnlyCollection<ImageFormat> SupportedOutputFormats => new HashSet<ImageFormat>() { ImageFormat.Jpg, ImageFormat.Png }; - public void CropWhiteSpace(string inputPath, string outputPath) - { - throw new NotImplementedException(); - } - - public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) - { - throw new NotImplementedException(); - } - - public void CreateImageCollage(ImageCollageOptions options) - { - throw new NotImplementedException(); - } - + /// <inheritdoc /> public string Name => "Null Image Encoder"; + /// <inheritdoc /> public bool SupportsImageCollageCreation => false; + /// <inheritdoc /> public bool SupportsImageEncoding => false; + /// <inheritdoc /> public ImageDimensions GetImageSize(string path) + => throw new NotImplementedException(); + + /// <inheritdoc /> + public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) + { + throw new NotImplementedException(); + } + + /// <inheritdoc /> + public void CreateImageCollage(ImageCollageOptions options) { throw new NotImplementedException(); } diff --git a/Emby.IsoMounting/.gitignore b/Emby.IsoMounting/.gitignore deleted file mode 100644 index bdc3535f7..000000000 --- a/Emby.IsoMounting/.gitignore +++ /dev/null @@ -1,108 +0,0 @@ -# Build Folders (you can keep bin if you'd like, to store dlls and pdbs) -[Bb]in/ -[Oo]bj/ - -# mstest test results -TestResults - -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.sln.docstates - -# Build results -[Dd]ebug/ -[Rr]elease/ -x64/ -*_i.c -*_p.c -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.log -*.vspscc -*.vssscc -.builds - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opensdf -*.sdf - -# Visual Studio profiler -*.psess -*.vsp -*.vspx - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper* - -# NCrunch -*.ncrunch* -.*crunch*.local.xml - -# Installshield output folder -[Ee]xpress - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish - -# Publish Web Output -*.Publish.xml - -# NuGet Packages Directory -packages - -# Windows Azure Build Output -csx -*.build.csdef - -# Windows Store app package directory -AppPackages/ - -# Others -[Bb]in -[Oo]bj -sql -TestResults -[Tt]est[Rr]esult* -*.Cache -ClientBin -[Ss]tyle[Cc]op.* -~$* -*.dbmdl -Generated_Code #added for RIA/Silverlight projects - -# Backup & report files from converting an old project file to a newer -# Visual Studio version. Backup files are not needed, because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML diff --git a/Emby.IsoMounting/IsoMounter.sln b/Emby.IsoMounting/IsoMounter.sln deleted file mode 100644 index 55db1b1ae..000000000 --- a/Emby.IsoMounting/IsoMounter.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27004.2009 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IsoMounter", "IsoMounter\IsoMounter.csproj", "{B94C929C-6552-4620-9BE5-422DD9A151BA}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {B94C929C-6552-4620-9BE5-422DD9A151BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B94C929C-6552-4620-9BE5-422DD9A151BA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B94C929C-6552-4620-9BE5-422DD9A151BA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B94C929C-6552-4620-9BE5-422DD9A151BA}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {C0E8EAD1-E4D7-44CD-B801-03BD12F30B1B} - EndGlobalSection -EndGlobal diff --git a/Emby.IsoMounting/IsoMounter/Configuration/PluginConfiguration.cs b/Emby.IsoMounting/IsoMounter/Configuration/PluginConfiguration.cs deleted file mode 100644 index 4755e4e82..000000000 --- a/Emby.IsoMounting/IsoMounter/Configuration/PluginConfiguration.cs +++ /dev/null @@ -1,8 +0,0 @@ -using MediaBrowser.Model.Plugins; - -namespace IsoMounter.Configuration -{ - public class PluginConfiguration : BasePluginConfiguration - { - } -} diff --git a/Emby.IsoMounting/IsoMounter/IsoMounter.csproj b/Emby.IsoMounting/IsoMounter/IsoMounter.csproj deleted file mode 100644 index dafa51cd5..000000000 --- a/Emby.IsoMounting/IsoMounter/IsoMounter.csproj +++ /dev/null @@ -1,17 +0,0 @@ -<Project Sdk="Microsoft.NET.Sdk"> - - <ItemGroup> - <Compile Include="..\..\SharedVersion.cs" /> - </ItemGroup> - - <ItemGroup> - <ProjectReference Include="..\..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> - <ProjectReference Include="..\..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> - </ItemGroup> - - <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> - <GenerateAssemblyInfo>false</GenerateAssemblyInfo> - </PropertyGroup> - -</Project> diff --git a/Emby.IsoMounting/IsoMounter/LinuxIsoManager.cs b/Emby.IsoMounting/IsoMounter/LinuxIsoManager.cs deleted file mode 100644 index 2f0003be8..000000000 --- a/Emby.IsoMounting/IsoMounter/LinuxIsoManager.cs +++ /dev/null @@ -1,477 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Diagnostics; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.System; -using Microsoft.Extensions.Logging; -using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; - -namespace IsoMounter -{ - public class LinuxIsoManager : IIsoMounter - { - [DllImport("libc", SetLastError = true)] - static extern uint getuid(); - - #region Private Fields - - private readonly bool ExecutablesAvailable; - private readonly ILogger _logger; - private readonly string MountCommand; - private readonly string MountPointRoot; - private readonly IProcessFactory ProcessFactory; - private readonly string SudoCommand; - private readonly string UmountCommand; - - #endregion - - #region Constructor(s) - - public LinuxIsoManager(ILogger logger, IProcessFactory processFactory) - { - _logger = logger; - ProcessFactory = processFactory; - - MountPointRoot = Path.DirectorySeparatorChar + "tmp" + Path.DirectorySeparatorChar + "Emby"; - - _logger.LogDebug( - "[{0}] System PATH is currently set to [{1}].", - Name, - Environment.GetEnvironmentVariable("PATH") ?? "" - ); - - _logger.LogDebug( - "[{0}] System path separator is [{1}].", - Name, - Path.PathSeparator - ); - - _logger.LogDebug( - "[{0}] Mount point root is [{1}].", - Name, - MountPointRoot - ); - - // - // Get the location of the executables we need to support mounting/unmounting ISO images. - // - - SudoCommand = GetFullPathForExecutable("sudo"); - - _logger.LogInformation( - "[{0}] Using version of [sudo] located at [{1}].", - Name, - SudoCommand - ); - - MountCommand = GetFullPathForExecutable("mount"); - - _logger.LogInformation( - "[{0}] Using version of [mount] located at [{1}].", - Name, - MountCommand - ); - - UmountCommand = GetFullPathForExecutable("umount"); - - _logger.LogInformation( - "[{0}] Using version of [umount] located at [{1}].", - Name, - UmountCommand - ); - - if (!string.IsNullOrEmpty(SudoCommand) && !string.IsNullOrEmpty(MountCommand) && !string.IsNullOrEmpty(UmountCommand)) - { - ExecutablesAvailable = true; - } - else - { - ExecutablesAvailable = false; - } - - } - - #endregion - - #region Interface Implementation for IIsoMounter - - public bool IsInstalled => true; - - public string Name => "LinuxMount"; - - public bool RequiresInstallation => false; - - public bool CanMount(string path) - { - - if (OperatingSystem.Id != OperatingSystemId.Linux) - { - return false; - } - _logger.LogInformation( - "[{0}] Checking we can attempt to mount [{1}], Extension = [{2}], Operating System = [{3}], Executables Available = [{4}].", - Name, - path, - Path.GetExtension(path), - OperatingSystem.Name, - ExecutablesAvailable - ); - - if (ExecutablesAvailable) - { - return string.Equals(Path.GetExtension(path), ".iso", StringComparison.OrdinalIgnoreCase); - } - else - { - return false; - } - } - - public Task Install(CancellationToken cancellationToken) - { - return Task.FromResult(false); - } - - public Task<IIsoMount> Mount(string isoPath, CancellationToken cancellationToken) - { - if (MountISO(isoPath, out LinuxMount mountedISO)) - { - return Task.FromResult<IIsoMount>(mountedISO); - } - else - { - throw new IOException(string.Format( - "An error occurred trying to mount image [$0].", - isoPath - )); - } - } - - #endregion - - #region Interface Implementation for IDisposable - - // Flag: Has Dispose already been called? - private bool disposed = false; - - public void Dispose() - { - - // Dispose of unmanaged resources. - Dispose(true); - - // Suppress finalization. - GC.SuppressFinalize(this); - - } - - protected virtual void Dispose(bool disposing) - { - - if (disposed) - { - return; - } - - _logger.LogInformation( - "[{0}] Disposing [{1}].", - Name, - disposing - ); - - if (disposing) - { - - // - // Free managed objects here. - // - - } - - // - // Free any unmanaged objects here. - // - - disposed = true; - - } - - #endregion - - #region Private Methods - - private string GetFullPathForExecutable(string name) - { - - foreach (string test in (Environment.GetEnvironmentVariable("PATH") ?? "").Split(Path.PathSeparator)) - { - string path = test.Trim(); - - if (!string.IsNullOrEmpty(path) && File.Exists(path = Path.Combine(path, name))) - { - return Path.GetFullPath(path); - } - } - - return string.Empty; - } - - private uint GetUID() - { - - var uid = getuid(); - - _logger.LogDebug( - "[{0}] GetUserId() returned [{2}].", - Name, - uid - ); - - return uid; - - } - - private bool ExecuteCommand(string cmdFilename, string cmdArguments) - { - - bool processFailed = false; - - var process = ProcessFactory.Create( - new ProcessOptions - { - CreateNoWindow = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - FileName = cmdFilename, - Arguments = cmdArguments, - IsHidden = true, - ErrorDialog = false, - EnableRaisingEvents = true - } - ); - - try - { - process.Start(); - - //StreamReader outputReader = process.StandardOutput.; - //StreamReader errorReader = process.StandardError; - - _logger.LogDebug( - "[{Name}] Standard output from process is [{Error}].", - Name, - process.StandardOutput.ReadToEnd() - ); - - _logger.LogDebug( - "[{Name}] Standard error from process is [{Error}].", - Name, - process.StandardError.ReadToEnd() - ); - } - catch (Exception ex) - { - processFailed = true; - _logger.LogDebug(ex, "[{Name}] Unhandled exception executing command.", Name); - } - - if (!processFailed && process.ExitCode == 0) - { - return true; - } - else - { - return false; - } - - } - - private bool MountISO(string isoPath, out LinuxMount mountedISO) - { - - string cmdArguments; - string cmdFilename; - string mountPoint = Path.Combine(MountPointRoot, Guid.NewGuid().ToString()); - - if (!string.IsNullOrEmpty(isoPath)) - { - - _logger.LogInformation( - "[{Name}] Attempting to mount [{Path}].", - Name, - isoPath - ); - - _logger.LogDebug( - "[{Name}] ISO will be mounted at [{Path}].", - Name, - mountPoint - ); - - } - else - { - - throw new ArgumentNullException(nameof(isoPath)); - - } - - try - { - Directory.CreateDirectory(mountPoint); - } - catch (UnauthorizedAccessException) - { - throw new IOException("Unable to create mount point(Permission denied) for " + isoPath); - } - catch (Exception) - { - throw new IOException("Unable to create mount point for " + isoPath); - } - - if (GetUID() == 0) - { - cmdFilename = MountCommand; - cmdArguments = string.Format("\"{0}\" \"{1}\"", isoPath, mountPoint); - } - else - { - cmdFilename = SudoCommand; - cmdArguments = string.Format("\"{0}\" \"{1}\" \"{2}\"", MountCommand, isoPath, mountPoint); - } - - _logger.LogDebug( - "[{0}] Mount command [{1}], mount arguments [{2}].", - Name, - cmdFilename, - cmdArguments - ); - - if (ExecuteCommand(cmdFilename, cmdArguments)) - { - - _logger.LogInformation( - "[{0}] ISO mount completed successfully.", - Name - ); - - mountedISO = new LinuxMount(this, isoPath, mountPoint); - - } - else - { - - _logger.LogInformation( - "[{0}] ISO mount completed with errors.", - Name - ); - - try - { - Directory.Delete(mountPoint, false); - } - catch (Exception ex) - { - _logger.LogInformation(ex, "[{Name}] Unhandled exception removing mount point.", Name); - } - - mountedISO = null; - - } - - return mountedISO != null; - - } - - private void UnmountISO(LinuxMount mount) - { - - string cmdArguments; - string cmdFilename; - - if (mount != null) - { - - _logger.LogInformation( - "[{0}] Attempting to unmount ISO [{1}] mounted on [{2}].", - Name, - mount.IsoPath, - mount.MountedPath - ); - - } - else - { - - throw new ArgumentNullException(nameof(mount)); - - } - - if (GetUID() == 0) - { - cmdFilename = UmountCommand; - cmdArguments = string.Format("\"{0}\"", mount.MountedPath); - } - else - { - cmdFilename = SudoCommand; - cmdArguments = string.Format("\"{0}\" \"{1}\"", UmountCommand, mount.MountedPath); - } - - _logger.LogDebug( - "[{0}] Umount command [{1}], umount arguments [{2}].", - Name, - cmdFilename, - cmdArguments - ); - - if (ExecuteCommand(cmdFilename, cmdArguments)) - { - - _logger.LogInformation( - "[{0}] ISO unmount completed successfully.", - Name - ); - - } - else - { - - _logger.LogInformation( - "[{0}] ISO unmount completed with errors.", - Name - ); - - } - - try - { - Directory.Delete(mount.MountedPath, false); - } - catch (Exception ex) - { - _logger.LogInformation(ex, "[{Name}] Unhandled exception removing mount point.", Name); - } - } - - #endregion - - #region Internal Methods - - internal void OnUnmount(LinuxMount mount) - { - - UnmountISO(mount); - - } - - #endregion - - } - -} - diff --git a/Emby.IsoMounting/IsoMounter/LinuxMount.cs b/Emby.IsoMounting/IsoMounter/LinuxMount.cs deleted file mode 100644 index b8636822b..000000000 --- a/Emby.IsoMounting/IsoMounter/LinuxMount.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using MediaBrowser.Model.IO; - -namespace IsoMounter -{ - internal class LinuxMount : IIsoMount - { - - #region Private Fields - - private readonly LinuxIsoManager linuxIsoManager; - - #endregion - - #region Constructor(s) - - internal LinuxMount(LinuxIsoManager isoManager, string isoPath, string mountFolder) - { - - linuxIsoManager = isoManager; - - IsoPath = isoPath; - MountedPath = mountFolder; - - } - - #endregion - - #region Interface Implementation for IDisposable - - // Flag: Has Dispose already been called? - private bool disposed = false; - - public void Dispose() - { - - // Dispose of unmanaged resources. - Dispose(true); - - // Suppress finalization. - GC.SuppressFinalize(this); - - } - - protected virtual void Dispose(bool disposing) - { - - if (disposed) - { - return; - } - - if (disposing) - { - - // - // Free managed objects here. - // - - linuxIsoManager.OnUnmount(this); - - } - - // - // Free any unmanaged objects here. - // - - disposed = true; - - } - - #endregion - - #region Interface Implementation for IIsoMount - - public string IsoPath { get; private set; } - public string MountedPath { get; private set; } - - #endregion - - } - -} diff --git a/Emby.IsoMounting/IsoMounter/Plugin.cs b/Emby.IsoMounting/IsoMounter/Plugin.cs deleted file mode 100644 index f45b39d3e..000000000 --- a/Emby.IsoMounting/IsoMounter/Plugin.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using IsoMounter.Configuration; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Plugins; -using MediaBrowser.Model.Serialization; - -namespace IsoMounter -{ - public class Plugin : BasePlugin<PluginConfiguration> - { - public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer) - { - } - - private Guid _id = new Guid("4682DD4C-A675-4F1B-8E7C-79ADF137A8F8"); - public override Guid Id => _id; - - /// <summary> - /// Gets the name of the plugin - /// </summary> - /// <value>The name.</value> - public override string Name => "Iso Mounter"; - - /// <summary> - /// Gets the description. - /// </summary> - /// <value>The description.</value> - public override string Description => "Mount and stream ISO contents"; - } -} diff --git a/Emby.IsoMounting/IsoMounter/Properties/AssemblyInfo.cs b/Emby.IsoMounting/IsoMounter/Properties/AssemblyInfo.cs deleted file mode 100644 index 5956fc3b3..000000000 --- a/Emby.IsoMounting/IsoMounter/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Reflection; -using System.Resources; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("IsoMounter")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin Server")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: NeutralResourcesLanguage("en")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] diff --git a/Emby.IsoMounting/LICENSE b/Emby.IsoMounting/LICENSE deleted file mode 100644 index d7f105139..000000000 --- a/Emby.IsoMounting/LICENSE +++ /dev/null @@ -1,339 +0,0 @@ -GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/> - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - {description} - Copyright (C) {year} {fullname} - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along - with this program; if not, write to the Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - {signature of Ty Coon}, 1 April 1989 - Ty Coon, President of Vice - -This General Public License does not permit incorporating your program into -proprietary programs. If your program is a subroutine library, you may -consider it more useful to permit linking proprietary applications with the -library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. diff --git a/Emby.IsoMounting/README.md b/Emby.IsoMounting/README.md deleted file mode 100644 index 78bab9936..000000000 --- a/Emby.IsoMounting/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# MediaBrowser.IsoMounting.Linux -This implements two core interfaces, IIsoManager, and IIsoMount. -### IIsoManager -The manager class can be used to create a mount, and also determine if the mounter is capable of mounting a given file. -### IIsoMount -IIsoMount then represents a mount instance, which will be unmounted on disposal. -*** -This Linux version use sudo, mount and umount. - -You need to add this to your sudo file via visudo(change the username): - - Defaults:jsmith !requiretty - jsmith ALL=(root) NOPASSWD: /bin/mount - jsmith ALL=(root) NOPASSWD: /bin/umount diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index 9e2a4950f..fca0aa1b3 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> </PropertyGroup> @@ -23,7 +23,7 @@ <!-- Code analysers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.3" /> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.4" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" /> </ItemGroup> diff --git a/Emby.Naming/Extensions/StringExtensions.cs b/Emby.Naming/Extensions/StringExtensions.cs deleted file mode 100644 index 5512127a8..000000000 --- a/Emby.Naming/Extensions/StringExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Text; - -namespace Emby.Naming.Extensions -{ - public static class StringExtensions - { - // TODO: @bond remove this when moving to netstandard2.1 - public static string Replace(this string str, string oldValue, string newValue, StringComparison comparison) - { - var sb = new StringBuilder(); - - var previousIndex = 0; - var index = str.IndexOf(oldValue, comparison); - - while (index != -1) - { - sb.Append(str.Substring(previousIndex, index - previousIndex)); - sb.Append(newValue); - index += oldValue.Length; - - previousIndex = index; - index = str.IndexOf(oldValue, index, comparison); - } - - sb.Append(str.Substring(previousIndex)); - - return sb.ToString(); - } - } -} diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs index e81b2bb34..9096ccaf5 100644 --- a/Emby.Naming/TV/SeasonPathParser.cs +++ b/Emby.Naming/TV/SeasonPathParser.cs @@ -2,8 +2,6 @@ using System; using System.Globalization; using System.IO; using System.Linq; -using Emby.Naming.Common; -using Emby.Naming.Extensions; namespace Emby.Naming.TV { diff --git a/Emby.Notifications/Emby.Notifications.csproj b/Emby.Notifications/Emby.Notifications.csproj index 5c68e48c8..cbd3bde4f 100644 --- a/Emby.Notifications/Emby.Notifications.csproj +++ b/Emby.Notifications/Emby.Notifications.csproj @@ -3,6 +3,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> <ItemGroup> diff --git a/Emby.Notifications/NotificationManager.cs b/Emby.Notifications/NotificationManager.cs index 3d1d4722d..eecbbea07 100644 --- a/Emby.Notifications/NotificationManager.cs +++ b/Emby.Notifications/NotificationManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -88,7 +89,7 @@ namespace Emby.Notifications return _userManager.Users.Where(i => i.Policy.IsAdministrator) .Select(i => i.Id); case SendToUserType.All: - return _userManager.Users.Select(i => i.Id); + return _userManager.UsersIds; case SendToUserType.Custom: return request.UserIds; default: @@ -101,7 +102,7 @@ namespace Emby.Notifications var config = GetConfiguration(); return _userManager.Users - .Where(i => config.IsEnabledToSendToUser(request.NotificationType, i.Id.ToString("N"), i.Policy)) + .Where(i => config.IsEnabledToSendToUser(request.NotificationType, i.Id.ToString("N", CultureInfo.InvariantCulture), i.Policy)) .Select(i => i.Id); } @@ -197,7 +198,7 @@ namespace Emby.Notifications return _services.Select(i => new NameIdPair { Name = i.Name, - Id = i.Name.GetMD5().ToString("N") + Id = i.Name.GetMD5().ToString("N", CultureInfo.InvariantCulture) }).OrderBy(i => i.Name); } diff --git a/Emby.Notifications/Notifications.cs b/Emby.Notifications/Notifications.cs index ec08fd193..7aa1e7ae8 100644 --- a/Emby.Notifications/Notifications.cs +++ b/Emby.Notifications/Notifications.cs @@ -209,47 +209,48 @@ namespace Emby.Notifications public static string GetItemName(BaseItem item) { var name = item.Name; - var episode = item as Episode; - if (episode != null) + if (item is Episode episode) { if (episode.IndexNumber.HasValue) { - name = string.Format("Ep{0} - {1}", episode.IndexNumber.Value.ToString(CultureInfo.InvariantCulture), name); + name = string.Format( + CultureInfo.InvariantCulture, + "Ep{0} - {1}", + episode.IndexNumber.Value, + name); } if (episode.ParentIndexNumber.HasValue) { - name = string.Format("S{0}, {1}", episode.ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture), name); + name = string.Format( + CultureInfo.InvariantCulture, + "S{0}, {1}", + episode.ParentIndexNumber.Value, + name); } } - var hasSeries = item as IHasSeries; - if (hasSeries != null) + if (item is IHasSeries hasSeries) { name = hasSeries.SeriesName + " - " + name; } - var hasAlbumArtist = item as IHasAlbumArtist; - if (hasAlbumArtist != null) + if (item is IHasAlbumArtist hasAlbumArtist) { var artists = hasAlbumArtist.AlbumArtists; - if (artists.Length > 0) + if (artists.Count > 0) { name = artists[0] + " - " + name; } } - else + else if (item is IHasArtist hasArtist) { - var hasArtist = item as IHasArtist; - if (hasArtist != null) - { - var artists = hasArtist.Artists; + var artists = hasArtist.Artists; - if (artists.Length > 0) - { - name = artists[0] + " - " + name; - } + if (artists.Count > 0) + { + name = artists[0] + " - " + name; } } diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj index c9830abc5..b57b93a8c 100644 --- a/Emby.Photos/Emby.Photos.csproj +++ b/Emby.Photos/Emby.Photos.csproj @@ -10,12 +10,25 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="TagLibSharp" Version="2.2.0-beta" /> + <PackageReference Include="TagLibSharp" Version="2.2.0" /> </ItemGroup> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + </PropertyGroup> + + <!-- Code analysers--> + <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.4" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" /> + <PackageReference Include="SerilogAnalyzer" Version="0.15.0" /> + </ItemGroup> + + <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> + <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> </PropertyGroup> </Project> diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs index 99a635e60..63631e512 100644 --- a/Emby.Photos/PhotoProvider.cs +++ b/Emby.Photos/PhotoProvider.cs @@ -17,39 +17,50 @@ using TagLib.IFD.Tags; namespace Emby.Photos { + /// <summary> + /// Metadata provider for photos. + /// </summary> public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IHasItemChangeMonitor { private readonly ILogger _logger; private readonly IImageProcessor _imageProcessor; // These are causing taglib to hang - private string[] _includextensions = new string[] { ".jpg", ".jpeg", ".png", ".tiff", ".cr2" }; - - public PhotoProvider(ILogger logger, IImageProcessor imageProcessor) + private readonly string[] _includeExtensions = new string[] { ".jpg", ".jpeg", ".png", ".tiff", ".cr2" }; + + /// <summary> + /// Initializes a new instance of the <see cref="PhotoProvider" /> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="imageProcessor">The image processor.</param> + public PhotoProvider(ILogger<PhotoProvider> logger, IImageProcessor imageProcessor) { _logger = logger; _imageProcessor = imageProcessor; } + /// <inheritdoc /> public string Name => "Embedded Information"; + /// <inheritdoc /> public bool HasChanged(BaseItem item, IDirectoryService directoryService) { if (item.IsFileProtocol) { var file = directoryService.GetFile(item.Path); - return (file != null && file.LastWriteTimeUtc != item.DateModified); + return file != null && file.LastWriteTimeUtc != item.DateModified; } return false; } + /// <inheritdoc /> public Task<ItemUpdateType> FetchAsync(Photo item, MetadataRefreshOptions options, CancellationToken cancellationToken) { item.SetImagePath(ImageType.Primary, item.Path); // Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs - if (_includextensions.Contains(Path.GetExtension(item.Path), StringComparer.OrdinalIgnoreCase)) + if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparer.OrdinalIgnoreCase)) { try { @@ -88,14 +99,7 @@ namespace Emby.Photos item.Height = image.Properties.PhotoHeight; var rating = image.ImageTag.Rating; - if (rating.HasValue) - { - item.CommunityRating = rating; - } - else - { - item.CommunityRating = null; - } + item.CommunityRating = rating.HasValue ? rating : null; item.Overview = image.ImageTag.Comment; @@ -170,8 +174,8 @@ namespace Emby.Photos } } - const ItemUpdateType result = ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataImport; - return Task.FromResult(result); + const ItemUpdateType Result = ItemUpdateType.ImageUpdate | ItemUpdateType.MetadataImport; + return Task.FromResult(Result); } } } diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs index 0530a251c..1514402d6 100644 --- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs +++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -75,7 +76,6 @@ namespace Emby.Server.Implementations.Activity _sessionManager.AuthenticationFailed += OnAuthenticationFailed; _sessionManager.AuthenticationSucceeded += OnAuthenticationSucceeded; _sessionManager.SessionEnded += OnSessionEnded; - _sessionManager.PlaybackStart += OnPlaybackStart; _sessionManager.PlaybackStopped += OnPlaybackStopped; @@ -96,7 +96,10 @@ namespace Emby.Server.Implementations.Activity { CreateLogEntry(new ActivityLogEntry { - Name = string.Format(_localization.GetLocalizedString("CameraImageUploadedFrom"), e.Argument.Device.Name), + Name = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("CameraImageUploadedFrom"), + e.Argument.Device.Name), Type = NotificationType.CameraImageUploaded.ToString() }); } @@ -105,7 +108,10 @@ namespace Emby.Server.Implementations.Activity { CreateLogEntry(new ActivityLogEntry { - Name = string.Format(_localization.GetLocalizedString("UserLockedOutWithName"), e.Argument.Name), + Name = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("UserLockedOutWithName"), + e.Argument.Name), Type = NotificationType.UserLockedOut.ToString(), UserId = e.Argument.Id }); @@ -115,9 +121,13 @@ namespace Emby.Server.Implementations.Activity { CreateLogEntry(new ActivityLogEntry { - Name = string.Format(_localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"), e.Provider, Notifications.Notifications.GetItemName(e.Item)), + Name = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"), + e.Provider, + Notifications.Notifications.GetItemName(e.Item)), Type = "SubtitleDownloadFailure", - ItemId = e.Item.Id.ToString("N"), + ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture), ShortOverview = e.Exception.Message }); } @@ -178,7 +188,12 @@ namespace Emby.Server.Implementations.Activity CreateLogEntry(new ActivityLogEntry { - Name = string.Format(_localization.GetLocalizedString("UserStartedPlayingItemWithValues"), user.Name, GetItemName(item), e.DeviceName), + Name = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("UserStartedPlayingItemWithValues"), + user.Name, + GetItemName(item), + e.DeviceName), Type = GetPlaybackNotificationType(item.MediaType), UserId = user.Id }); @@ -193,7 +208,7 @@ namespace Emby.Server.Implementations.Activity name = item.SeriesName + " - " + name; } - if (item.Artists != null && item.Artists.Length > 0) + if (item.Artists != null && item.Artists.Count > 0) { name = item.Artists[0] + " - " + name; } @@ -238,21 +253,31 @@ namespace Emby.Server.Implementations.Activity if (string.IsNullOrEmpty(session.UserName)) { - name = string.Format(_localization.GetLocalizedString("DeviceOfflineWithName"), session.DeviceName); + name = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("DeviceOfflineWithName"), + session.DeviceName); // Causing too much spam for now return; } else { - name = string.Format(_localization.GetLocalizedString("UserOfflineFromDevice"), session.UserName, session.DeviceName); + name = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("UserOfflineFromDevice"), + session.UserName, + session.DeviceName); } CreateLogEntry(new ActivityLogEntry { Name = name, Type = "SessionEnded", - ShortOverview = string.Format(_localization.GetLocalizedString("LabelIpAddressValue"), session.RemoteEndPoint), + ShortOverview = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("LabelIpAddressValue"), + session.RemoteEndPoint), UserId = session.UserId }); } @@ -263,9 +288,15 @@ namespace Emby.Server.Implementations.Activity CreateLogEntry(new ActivityLogEntry { - Name = string.Format(_localization.GetLocalizedString("AuthenticationSucceededWithUserName"), user.Name), + Name = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("AuthenticationSucceededWithUserName"), + user.Name), Type = "AuthenticationSucceeded", - ShortOverview = string.Format(_localization.GetLocalizedString("LabelIpAddressValue"), e.Argument.SessionInfo.RemoteEndPoint), + ShortOverview = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("LabelIpAddressValue"), + e.Argument.SessionInfo.RemoteEndPoint), UserId = user.Id }); } @@ -274,9 +305,15 @@ namespace Emby.Server.Implementations.Activity { CreateLogEntry(new ActivityLogEntry { - Name = string.Format(_localization.GetLocalizedString("FailedLoginAttemptWithUserName"), e.Argument.Username), + Name = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("FailedLoginAttemptWithUserName"), + e.Argument.Username), Type = "AuthenticationFailed", - ShortOverview = string.Format(_localization.GetLocalizedString("LabelIpAddressValue"), e.Argument.RemoteEndPoint), + ShortOverview = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("LabelIpAddressValue"), + e.Argument.RemoteEndPoint), Severity = LogLevel.Error }); } @@ -285,7 +322,10 @@ namespace Emby.Server.Implementations.Activity { CreateLogEntry(new ActivityLogEntry { - Name = string.Format(_localization.GetLocalizedString("UserPolicyUpdatedWithName"), e.Argument.Name), + Name = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("UserPolicyUpdatedWithName"), + e.Argument.Name), Type = "UserPolicyUpdated", UserId = e.Argument.Id }); @@ -295,7 +335,10 @@ namespace Emby.Server.Implementations.Activity { CreateLogEntry(new ActivityLogEntry { - Name = string.Format(_localization.GetLocalizedString("UserDeletedWithName"), e.Argument.Name), + Name = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("UserDeletedWithName"), + e.Argument.Name), Type = "UserDeleted" }); } @@ -304,7 +347,10 @@ namespace Emby.Server.Implementations.Activity { CreateLogEntry(new ActivityLogEntry { - Name = string.Format(_localization.GetLocalizedString("UserPasswordChangedWithName"), e.Argument.Name), + Name = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("UserPasswordChangedWithName"), + e.Argument.Name), Type = "UserPasswordChanged", UserId = e.Argument.Id }); @@ -314,7 +360,10 @@ namespace Emby.Server.Implementations.Activity { CreateLogEntry(new ActivityLogEntry { - Name = string.Format(_localization.GetLocalizedString("UserCreatedWithName"), e.Argument.Name), + Name = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("UserCreatedWithName"), + e.Argument.Name), Type = "UserCreated", UserId = e.Argument.Id }); @@ -327,32 +376,48 @@ namespace Emby.Server.Implementations.Activity if (string.IsNullOrEmpty(session.UserName)) { - name = string.Format(_localization.GetLocalizedString("DeviceOnlineWithName"), session.DeviceName); + name = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("DeviceOnlineWithName"), + session.DeviceName); // Causing too much spam for now return; } else { - name = string.Format(_localization.GetLocalizedString("UserOnlineFromDevice"), session.UserName, session.DeviceName); + name = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("UserOnlineFromDevice"), + session.UserName, + session.DeviceName); } CreateLogEntry(new ActivityLogEntry { Name = name, Type = "SessionStarted", - ShortOverview = string.Format(_localization.GetLocalizedString("LabelIpAddressValue"), session.RemoteEndPoint), + ShortOverview = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("LabelIpAddressValue"), + session.RemoteEndPoint), UserId = session.UserId }); } - private void OnPluginUpdated(object sender, GenericEventArgs<Tuple<IPlugin, PackageVersionInfo>> e) + private void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, PackageVersionInfo)> e) { CreateLogEntry(new ActivityLogEntry { - Name = string.Format(_localization.GetLocalizedString("PluginUpdatedWithName"), e.Argument.Item1.Name), + Name = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("PluginUpdatedWithName"), + e.Argument.Item1.Name), Type = NotificationType.PluginUpdateInstalled.ToString(), - ShortOverview = string.Format(_localization.GetLocalizedString("VersionNumber"), e.Argument.Item2.versionStr), + ShortOverview = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("VersionNumber"), + e.Argument.Item2.versionStr), Overview = e.Argument.Item2.description }); } @@ -361,7 +426,10 @@ namespace Emby.Server.Implementations.Activity { CreateLogEntry(new ActivityLogEntry { - Name = string.Format(_localization.GetLocalizedString("PluginUninstalledWithName"), e.Argument.Name), + Name = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("PluginUninstalledWithName"), + e.Argument.Name), Type = NotificationType.PluginUninstalled.ToString() }); } @@ -370,9 +438,15 @@ namespace Emby.Server.Implementations.Activity { CreateLogEntry(new ActivityLogEntry { - Name = string.Format(_localization.GetLocalizedString("PluginInstalledWithName"), e.Argument.name), + Name = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("PluginInstalledWithName"), + e.Argument.name), Type = NotificationType.PluginInstalled.ToString(), - ShortOverview = string.Format(_localization.GetLocalizedString("VersionNumber"), e.Argument.versionStr) + ShortOverview = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("VersionNumber"), + e.Argument.versionStr) }); } @@ -382,9 +456,15 @@ namespace Emby.Server.Implementations.Activity CreateLogEntry(new ActivityLogEntry { - Name = string.Format(_localization.GetLocalizedString("NameInstallFailed"), installationInfo.Name), + Name = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("NameInstallFailed"), + installationInfo.Name), Type = NotificationType.InstallationFailed.ToString(), - ShortOverview = string.Format(_localization.GetLocalizedString("VersionNumber"), installationInfo.Version), + ShortOverview = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("VersionNumber"), + installationInfo.Version), Overview = e.Exception.Message }); } @@ -401,7 +481,10 @@ namespace Emby.Server.Implementations.Activity } var time = result.EndTimeUtc - result.StartTimeUtc; - var runningTime = string.Format(_localization.GetLocalizedString("LabelRunningTimeValue"), ToUserFriendlyString(time)); + var runningTime = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("LabelRunningTimeValue"), + ToUserFriendlyString(time)); if (result.Status == TaskCompletionStatus.Failed) { @@ -419,7 +502,10 @@ namespace Emby.Server.Implementations.Activity CreateLogEntry(new ActivityLogEntry { - Name = string.Format(_localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name), + Name = string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("ScheduledTaskFailedWithName"), + task.Name), Type = NotificationType.TaskFailed.ToString(), Overview = string.Join(Environment.NewLine, vals), ShortOverview = runningTime, @@ -534,8 +620,11 @@ namespace Emby.Server.Implementations.Activity /// <param name="description">The name of this item (singular form)</param> private static string CreateValueString(int value, string description) { - return string.Format("{0:#,##0} {1}", - value, value == 1 ? description : string.Format("{0}s", description)); + return string.Format( + CultureInfo.InvariantCulture, + "{0:#,##0} {1}", + value, + value == 1 ? description : string.Format(CultureInfo.InvariantCulture, "{0}s", description)); } } } diff --git a/Emby.Server.Implementations/Activity/ActivityRepository.cs b/Emby.Server.Implementations/Activity/ActivityRepository.cs index de46ab965..ffaeaa541 100644 --- a/Emby.Server.Implementations/Activity/ActivityRepository.cs +++ b/Emby.Server.Implementations/Activity/ActivityRepository.cs @@ -102,7 +102,7 @@ namespace Emby.Server.Implementations.Activity } else { - statement.TryBind("@UserId", entry.UserId.ToString("N")); + statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture)); } statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue()); @@ -141,7 +141,7 @@ namespace Emby.Server.Implementations.Activity } else { - statement.TryBind("@UserId", entry.UserId.ToString("N")); + statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture)); } statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue()); @@ -155,94 +155,100 @@ namespace Emby.Server.Implementations.Activity public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? startIndex, int? limit) { - using (var connection = GetConnection(true)) - { - var commandText = BaseActivitySelectText; - var whereClauses = new List<string>(); + var commandText = BaseActivitySelectText; + var whereClauses = new List<string>(); - if (minDate.HasValue) + if (minDate.HasValue) + { + whereClauses.Add("DateCreated>=@DateCreated"); + } + if (hasUserId.HasValue) + { + if (hasUserId.Value) { - whereClauses.Add("DateCreated>=@DateCreated"); + whereClauses.Add("UserId not null"); } - if (hasUserId.HasValue) + else { - if (hasUserId.Value) - { - whereClauses.Add("UserId not null"); - } - else - { - whereClauses.Add("UserId is null"); - } + whereClauses.Add("UserId is null"); } + } - var whereTextWithoutPaging = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - if (startIndex.HasValue && startIndex.Value > 0) - { - var pagingWhereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM ActivityLog {0} ORDER BY DateCreated DESC LIMIT {1})", - pagingWhereText, - startIndex.Value.ToString(_usCulture))); - } + var whereTextWithoutPaging = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); - var whereText = whereClauses.Count == 0 ? + if (startIndex.HasValue && startIndex.Value > 0) + { + var pagingWhereText = whereClauses.Count == 0 ? string.Empty : " where " + string.Join(" AND ", whereClauses.ToArray()); - commandText += whereText; + whereClauses.Add( + string.Format( + CultureInfo.InvariantCulture, + "Id NOT IN (SELECT Id FROM ActivityLog {0} ORDER BY DateCreated DESC LIMIT {1})", + pagingWhereText, + startIndex.Value)); + } - commandText += " ORDER BY DateCreated DESC"; + var whereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); - if (limit.HasValue) - { - commandText += " LIMIT " + limit.Value.ToString(_usCulture); - } + commandText += whereText; - var statementTexts = new List<string>(); - statementTexts.Add(commandText); - statementTexts.Add("select count (Id) from ActivityLog" + whereTextWithoutPaging); + commandText += " ORDER BY DateCreated DESC"; - return connection.RunInTransaction(db => - { - var list = new List<ActivityLogEntry>(); - var result = new QueryResult<ActivityLogEntry>(); + if (limit.HasValue) + { + commandText += " LIMIT " + limit.Value.ToString(_usCulture); + } + + var statementTexts = new[] + { + commandText, + "select count (Id) from ActivityLog" + whereTextWithoutPaging + }; - var statements = PrepareAll(db, statementTexts).ToList(); + var list = new List<ActivityLogEntry>(); + var result = new QueryResult<ActivityLogEntry>(); - using (var statement = statements[0]) + using (var connection = GetConnection(true)) + { + connection.RunInTransaction( + db => { - if (minDate.HasValue) - { - statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue()); - } + var statements = PrepareAll(db, statementTexts).ToList(); - foreach (var row in statement.ExecuteQuery()) + using (var statement = statements[0]) { - list.Add(GetEntry(row)); + if (minDate.HasValue) + { + statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue()); + } + + foreach (var row in statement.ExecuteQuery()) + { + list.Add(GetEntry(row)); + } } - } - using (var statement = statements[1]) - { - if (minDate.HasValue) + using (var statement = statements[1]) { - statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue()); - } + if (minDate.HasValue) + { + statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue()); + } - result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); - } - - result.Items = list.ToArray(); - return result; - - }, ReadTransactionMode); + result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); + } + }, + ReadTransactionMode); } + + result.Items = list; + return result; } private static ActivityLogEntry GetEntry(IReadOnlyList<IResultSetValue> reader) diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index 00cfa0c9a..c3cdcc222 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -10,6 +10,8 @@ namespace Emby.Server.Implementations.AppBase /// </summary> public abstract class BaseApplicationPaths : IApplicationPaths { + private string _dataPath; + /// <summary> /// Initializes a new instance of the <see cref="BaseApplicationPaths"/> class. /// </summary> @@ -30,36 +32,34 @@ namespace Emby.Server.Implementations.AppBase } /// <summary> - /// Gets the path to the program data folder + /// Gets the path to the program data folder. /// </summary> /// <value>The program data path.</value> - public string ProgramDataPath { get; private set; } + public 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> - public string WebPath { get; set; } + public string WebPath { get; } /// <summary> - /// Gets the path to the system folder + /// Gets the path to the system folder. /// </summary> + /// <value>The path to the system folder.</value> public string ProgramSystemPath { get; } = AppContext.BaseDirectory; /// <summary> - /// Gets the folder path to the data directory + /// Gets the folder path to the data directory. /// </summary> /// <value>The data directory.</value> - private string _dataPath; public string DataPath { get => _dataPath; private set => _dataPath = Directory.CreateDirectory(value).FullName; } - /// <summary> - /// Gets the magic strings used for virtual path manipulation. - /// </summary> + /// <inheritdoc /> public string VirtualDataPath { get; } = "%AppDataPath%"; /// <summary> @@ -69,43 +69,43 @@ namespace Emby.Server.Implementations.AppBase public string ImageCachePath => Path.Combine(CachePath, "images"); /// <summary> - /// Gets the path to the plugin directory + /// Gets the path to the plugin directory. /// </summary> /// <value>The plugins path.</value> public string PluginsPath => Path.Combine(ProgramDataPath, "plugins"); /// <summary> - /// Gets the path to the plugin configurations directory + /// Gets the path to the plugin configurations directory. /// </summary> /// <value>The plugin configurations path.</value> public string PluginConfigurationsPath => Path.Combine(PluginsPath, "configurations"); /// <summary> - /// Gets the path to the log directory + /// Gets the path to the log directory. /// </summary> /// <value>The log directory path.</value> - public string LogDirectoryPath { get; private set; } + public string LogDirectoryPath { get; } /// <summary> - /// Gets the path to the application configuration root directory + /// Gets the path to the application configuration root directory. /// </summary> /// <value>The configuration directory path.</value> - public string ConfigurationDirectoryPath { get; private set; } + public string ConfigurationDirectoryPath { get; } /// <summary> - /// Gets the path to the system configuration file + /// Gets the path to the system configuration file. /// </summary> /// <value>The system configuration file path.</value> public string SystemConfigurationFilePath => Path.Combine(ConfigurationDirectoryPath, "system.xml"); /// <summary> - /// Gets the folder path to the cache directory + /// Gets or sets the folder path to the cache directory. /// </summary> /// <value>The cache directory.</value> public string CachePath { get; set; } /// <summary> - /// Gets the folder path to the temp directory within the cache folder + /// Gets the folder path to the temp directory within the cache folder. /// </summary> /// <value>The temp directory.</value> public string TempDirectory => Path.Combine(CachePath, "temp"); diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index af60a8dce..4832c19c4 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading; @@ -19,11 +20,44 @@ namespace Emby.Server.Implementations.AppBase /// </summary> public abstract class BaseConfigurationManager : IConfigurationManager { + private readonly IFileSystem _fileSystem; + + private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>(); + + private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>(); + private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>(); + /// <summary> - /// Gets the type of the configuration. + /// The _configuration loaded. /// </summary> - /// <value>The type of the configuration.</value> - protected abstract Type ConfigurationType { get; } + private bool _configurationLoaded; + + /// <summary> + /// The _configuration sync lock. + /// </summary> + private object _configurationSyncLock = new object(); + + /// <summary> + /// The _configuration. + /// </summary> + private BaseApplicationConfiguration _configuration; + + /// <summary> + /// Initializes a new instance of the <see cref="BaseConfigurationManager" /> class. + /// </summary> + /// <param name="applicationPaths">The application paths.</param> + /// <param name="loggerFactory">The logger factory.</param> + /// <param name="xmlSerializer">The XML serializer.</param> + /// <param name="fileSystem">The file system</param> + protected BaseConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem) + { + CommonApplicationPaths = applicationPaths; + XmlSerializer = xmlSerializer; + _fileSystem = fileSystem; + Logger = loggerFactory.CreateLogger(GetType().Name); + + UpdateCachePath(); + } /// <summary> /// Occurs when [configuration updated]. @@ -41,6 +75,12 @@ namespace Emby.Server.Implementations.AppBase public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdated; /// <summary> + /// Gets the type of the configuration. + /// </summary> + /// <value>The type of the configuration.</value> + protected abstract Type ConfigurationType { get; } + + /// <summary> /// Gets the logger. /// </summary> /// <value>The logger.</value> @@ -56,21 +96,8 @@ namespace Emby.Server.Implementations.AppBase /// </summary> /// <value>The application paths.</value> public IApplicationPaths CommonApplicationPaths { get; private set; } - public readonly IFileSystem FileSystem; /// <summary> - /// The _configuration loaded - /// </summary> - private bool _configurationLoaded; - /// <summary> - /// The _configuration sync lock - /// </summary> - private object _configurationSyncLock = new object(); - /// <summary> - /// The _configuration - /// </summary> - private BaseApplicationConfiguration _configuration; - /// <summary> /// Gets the system configuration /// </summary> /// <value>The configuration.</value> @@ -90,26 +117,6 @@ namespace Emby.Server.Implementations.AppBase } } - private ConfigurationStore[] _configurationStores = { }; - private IConfigurationFactory[] _configurationFactories = { }; - - /// <summary> - /// Initializes a new instance of the <see cref="BaseConfigurationManager" /> class. - /// </summary> - /// <param name="applicationPaths">The application paths.</param> - /// <param name="loggerFactory">The logger factory.</param> - /// <param name="xmlSerializer">The XML serializer.</param> - /// <param name="fileSystem">The file system</param> - protected BaseConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem) - { - CommonApplicationPaths = applicationPaths; - XmlSerializer = xmlSerializer; - FileSystem = fileSystem; - Logger = loggerFactory.CreateLogger(GetType().Name); - - UpdateCachePath(); - } - public virtual void AddParts(IEnumerable<IConfigurationFactory> factories) { _configurationFactories = factories.ToArray(); @@ -171,6 +178,7 @@ namespace Emby.Server.Implementations.AppBase private void UpdateCachePath() { string cachePath; + // If the configuration file has no entry (i.e. not set in UI) if (string.IsNullOrWhiteSpace(CommonConfiguration.CachePath)) { @@ -207,12 +215,16 @@ namespace Emby.Server.Implementations.AppBase var newPath = newConfig.CachePath; if (!string.IsNullOrWhiteSpace(newPath) - && !string.Equals(CommonConfiguration.CachePath ?? string.Empty, newPath)) + && !string.Equals(CommonConfiguration.CachePath ?? string.Empty, newPath, StringComparison.Ordinal)) { // Validate if (!Directory.Exists(newPath)) { - throw new FileNotFoundException(string.Format("{0} does not exist.", newPath)); + throw new FileNotFoundException( + string.Format( + CultureInfo.InvariantCulture, + "{0} does not exist.", + newPath)); } EnsureWriteAccess(newPath); @@ -223,11 +235,9 @@ namespace Emby.Server.Implementations.AppBase { var file = Path.Combine(path, Guid.NewGuid().ToString()); File.WriteAllText(file, string.Empty); - FileSystem.DeleteFile(file); + _fileSystem.DeleteFile(file); } - private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>(); - private string GetConfigurationFile(string key) { return Path.Combine(CommonApplicationPaths.ConfigurationDirectoryPath, key.ToLowerInvariant() + ".xml"); diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index ef2f59d30..04904fc4a 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -49,7 +51,6 @@ using MediaBrowser.Api; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; -using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; @@ -108,19 +109,23 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; using ServiceStack; using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; namespace Emby.Server.Implementations { /// <summary> - /// Class CompositionRoot + /// Class CompositionRoot. /// </summary> public abstract class ApplicationHost : IServerApplicationHost, IDisposable { + private SqliteUserRepository _userRepository; + + private SqliteDisplayPreferencesRepository _displayPreferencesRepository; + /// <summary> /// Gets a value indicating whether this instance can self restart. /// </summary> @@ -162,6 +167,7 @@ namespace Emby.Server.Implementations /// <value><c>true</c> if this instance has pending application restart; otherwise, <c>false</c>.</value> public bool HasPendingRestart { get; private set; } + /// <inheritdoc /> public bool IsShuttingDown { get; private set; } /// <summary> @@ -198,10 +204,10 @@ namespace Emby.Server.Implementations /// Gets or sets all concrete types. /// </summary> /// <value>All concrete types.</value> - public Type[] AllConcreteTypes { get; protected set; } + private Type[] _allConcreteTypes; /// <summary> - /// The disposable parts + /// The disposable parts. /// </summary> private readonly List<IDisposable> _disposableParts = new List<IDisposable>(); @@ -213,6 +219,7 @@ namespace Emby.Server.Implementations public IFileSystem FileSystemManager { get; set; } + /// <inheritdoc /> public PackageVersionClass SystemUpdateLevel { get @@ -291,8 +298,6 @@ namespace Emby.Server.Implementations /// <value>The user data repository.</value> private IUserDataManager UserDataManager { get; set; } - private IUserRepository UserRepository { get; set; } - internal SqliteItemRepository ItemRepository { get; set; } private INotificationManager NotificationManager { get; set; } @@ -313,8 +318,6 @@ namespace Emby.Server.Implementations private IMediaSourceManager MediaSourceManager { get; set; } - private IPlaylistManager PlaylistManager { get; set; } - private readonly IConfiguration _configuration; /// <summary> @@ -323,14 +326,6 @@ namespace Emby.Server.Implementations /// <value>The installation manager.</value> protected IInstallationManager InstallationManager { get; private set; } - /// <summary> - /// Gets or sets the zip client. - /// </summary> - /// <value>The zip client.</value> - protected IZipClient ZipClient { get; private set; } - - protected IHttpResultFactory HttpResultFactory { get; private set; } - protected IAuthService AuthService { get; private set; } public IStartupOptions StartupOptions { get; } @@ -386,7 +381,7 @@ namespace Emby.Server.Implementations fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem)); - NetworkManager.NetworkChanged += NetworkManager_NetworkChanged; + NetworkManager.NetworkChanged += OnNetworkChanged; } public string ExpandVirtualPath(string path) @@ -410,7 +405,7 @@ namespace Emby.Server.Implementations return ServerConfigurationManager.Configuration.LocalNetworkSubnets; } - private void NetworkManager_NetworkChanged(object sender, EventArgs e) + private void OnNetworkChanged(object sender, EventArgs e) { _validAddressResults.Clear(); } @@ -418,10 +413,10 @@ namespace Emby.Server.Implementations public string ApplicationVersion { get; } = typeof(ApplicationHost).Assembly.GetName().Version.ToString(3); /// <summary> - /// Gets the current application user agent + /// Gets the current application user agent. /// </summary> /// <value>The application user agent.</value> - public string ApplicationUserAgent => Name.Replace(' ','-') + '/' + ApplicationVersion; + public string ApplicationUserAgent => Name.Replace(' ', '-') + "/" + ApplicationVersion; /// <summary> /// Gets the email address for use within a comment section of a user agent field. @@ -429,14 +424,11 @@ namespace Emby.Server.Implementations /// </summary> public string ApplicationUserAgentAddress { get; } = "team@jellyfin.org"; - private string _productName; - /// <summary> - /// Gets the current application name + /// Gets the current application name. /// </summary> /// <value>The application name.</value> - public string ApplicationProductName - => _productName ?? (_productName = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName); + public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName; private DeviceId _deviceId; @@ -470,8 +462,8 @@ namespace Emby.Server.Implementations /// <summary> /// Creates an instance of type and resolves all constructor dependencies /// </summary> - /// /// <typeparam name="T">The type</typeparam> - /// <returns>T</returns> + /// /// <typeparam name="T">The type.</typeparam> + /// <returns>T.</returns> public T CreateInstance<T>() => ActivatorUtilities.CreateInstance<T>(_serviceProvider); @@ -510,16 +502,11 @@ namespace Emby.Server.Implementations { var currentType = typeof(T); - return AllConcreteTypes.Where(i => currentType.IsAssignableFrom(i)); + return _allConcreteTypes.Where(i => currentType.IsAssignableFrom(i)); } - /// <summary> - /// Gets the exports. - /// </summary> - /// <typeparam name="T">The type</typeparam> - /// <param name="manageLifetime">if set to <c>true</c> [manage lifetime].</param> - /// <returns>IEnumerable{``0}.</returns> - public IEnumerable<T> GetExports<T>(bool manageLifetime = true) + /// <inheritdoc /> + public IReadOnlyCollection<T> GetExports<T>(bool manageLifetime = true) { var parts = GetExportTypes<T>() .Select(CreateInstanceSafe) @@ -541,6 +528,7 @@ namespace Emby.Server.Implementations /// <summary> /// Runs the startup tasks. /// </summary> + /// <returns><see cref="Task" />.</returns> public async Task RunStartupTasksAsync() { Logger.LogInformation("Running startup tasks"); @@ -553,7 +541,7 @@ namespace Emby.Server.Implementations Logger.LogInformation("ServerId: {0}", SystemId); - var entryPoints = GetExports<IServerEntryPoint>().ToList(); + var entryPoints = GetExports<IServerEntryPoint>(); var stopWatch = new Stopwatch(); stopWatch.Start(); @@ -604,10 +592,15 @@ namespace Emby.Server.Implementations foreach (var plugin in Plugins) { - pluginBuilder.AppendLine(string.Format("{0} {1}", plugin.Name, plugin.Version)); + pluginBuilder.AppendLine( + string.Format( + CultureInfo.InvariantCulture, + "{0} {1}", + plugin.Name, + plugin.Version)); } - Logger.LogInformation("Plugins: {plugins}", pluginBuilder.ToString()); + Logger.LogInformation("Plugins: {Plugins}", pluginBuilder.ToString()); } DiscoverTypes(); @@ -625,11 +618,34 @@ namespace Emby.Server.Implementations var host = new WebHostBuilder() .UseKestrel(options => { - options.ListenAnyIP(HttpPort); + var addresses = ServerConfigurationManager + .Configuration + .LocalNetworkAddresses + .Select(NormalizeConfiguredLocalAddress) + .Where(i => i != null) + .ToList(); + if (addresses.Any()) + { + foreach (var address in addresses) + { + Logger.LogInformation("Kestrel listening on {ipaddr}", address); + options.Listen(address, HttpPort); - if (EnableHttps && Certificate != null) + if (EnableHttps && Certificate != null) + { + options.Listen(address, HttpsPort, listenOptions => listenOptions.UseHttps(Certificate)); + } + } + } + else { - options.ListenAnyIP(HttpsPort, listenOptions => { listenOptions.UseHttps(Certificate); }); + Logger.LogInformation("Kestrel listening on all interfaces"); + options.ListenAnyIP(HttpPort); + + if (EnableHttps && Certificate != null) + { + options.ListenAnyIP(HttpsPort, listenOptions => listenOptions.UseHttps(Certificate)); + } } }) .UseContentRoot(contentRoot) @@ -643,13 +659,22 @@ namespace Emby.Server.Implementations app.UseWebSockets(); app.UseResponseCompression(); + // TODO app.UseMiddleware<WebSocketMiddleware>(); app.Use(ExecuteWebsocketHandlerAsync); app.Use(ExecuteHttpHandlerAsync); }) .Build(); - await host.StartAsync().ConfigureAwait(false); + try + { + await host.StartAsync().ConfigureAwait(false); + } + catch + { + Logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in system.xml and try again."); + throw; + } } private async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next) @@ -676,11 +701,9 @@ namespace Emby.Server.Implementations var localPath = context.Request.Path.ToString(); var req = new WebSocketSharpRequest(request, response, request.Path, Logger); - await HttpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, CancellationToken.None).ConfigureAwait(false); + await HttpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted).ConfigureAwait(false); } - public static IStreamHelper StreamHelper { get; set; } - /// <summary> /// Registers resources that classes will depend on /// </summary> @@ -724,8 +747,7 @@ namespace Emby.Server.Implementations ProcessFactory = new ProcessFactory(); serviceCollection.AddSingleton(ProcessFactory); - ApplicationHost.StreamHelper = new StreamHelper(); - serviceCollection.AddSingleton(StreamHelper); + serviceCollection.AddSingleton(typeof(IStreamHelper), typeof(StreamHelper)); serviceCollection.AddSingleton(typeof(ICryptoProvider), typeof(CryptographyProvider)); @@ -734,18 +756,16 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(typeof(IInstallationManager), typeof(InstallationManager)); - ZipClient = new ZipClient(); - serviceCollection.AddSingleton(ZipClient); + serviceCollection.AddSingleton(typeof(IZipClient), typeof(ZipClient)); - HttpResultFactory = new HttpResultFactory(LoggerFactory, FileSystemManager, JsonSerializer, StreamHelper); - serviceCollection.AddSingleton(HttpResultFactory); + serviceCollection.AddSingleton(typeof(IHttpResultFactory), typeof(HttpResultFactory)); serviceCollection.AddSingleton<IServerApplicationHost>(this); serviceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths); serviceCollection.AddSingleton(ServerConfigurationManager); - LocalizationManager = new LocalizationManager(ServerConfigurationManager, JsonSerializer, LoggerFactory); + LocalizationManager = new LocalizationManager(ServerConfigurationManager, JsonSerializer, LoggerFactory.CreateLogger<LocalizationManager>()); await LocalizationManager.LoadAll().ConfigureAwait(false); serviceCollection.AddSingleton<ILocalizationManager>(LocalizationManager); @@ -754,18 +774,22 @@ namespace Emby.Server.Implementations UserDataManager = new UserDataManager(LoggerFactory, ServerConfigurationManager, () => UserManager); serviceCollection.AddSingleton(UserDataManager); - var displayPreferencesRepo = new SqliteDisplayPreferencesRepository(LoggerFactory, JsonSerializer, ApplicationPaths, FileSystemManager); - serviceCollection.AddSingleton<IDisplayPreferencesRepository>(displayPreferencesRepo); + _displayPreferencesRepository = new SqliteDisplayPreferencesRepository( + LoggerFactory.CreateLogger<SqliteDisplayPreferencesRepository>(), + ApplicationPaths, + FileSystemManager); + serviceCollection.AddSingleton<IDisplayPreferencesRepository>(_displayPreferencesRepository); - ItemRepository = new SqliteItemRepository(ServerConfigurationManager, this, JsonSerializer, LoggerFactory, LocalizationManager); + ItemRepository = new SqliteItemRepository(ServerConfigurationManager, this, LoggerFactory.CreateLogger<SqliteItemRepository>(), LocalizationManager); serviceCollection.AddSingleton<IItemRepository>(ItemRepository); AuthenticationRepository = GetAuthenticationRepository(); serviceCollection.AddSingleton(AuthenticationRepository); - UserRepository = GetUserRepository(); + _userRepository = GetUserRepository(); + + UserManager = new UserManager(LoggerFactory.CreateLogger<UserManager>(), _userRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, this, JsonSerializer, FileSystemManager); - UserManager = new UserManager(LoggerFactory, ServerConfigurationManager, UserRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, this, JsonSerializer, FileSystemManager); serviceCollection.AddSingleton(UserManager); LibraryManager = new LibraryManager(this, LoggerFactory, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager, () => UserViewManager); @@ -785,7 +809,7 @@ namespace Emby.Server.Implementations HttpServer = new HttpListenerHost( this, - LoggerFactory, + LoggerFactory.CreateLogger<HttpListenerHost>(), ServerConfigurationManager, _configuration, NetworkManager, @@ -798,7 +822,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(HttpServer); - ImageProcessor = GetImageProcessor(); + ImageProcessor = new ImageProcessor(LoggerFactory.CreateLogger<ImageProcessor>(), ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder); serviceCollection.AddSingleton(ImageProcessor); TVSeriesManager = new TVSeriesManager(UserManager, UserDataManager, LibraryManager, ServerConfigurationManager); @@ -831,8 +855,7 @@ namespace Emby.Server.Implementations CollectionManager = new CollectionManager(LibraryManager, ApplicationPaths, LocalizationManager, FileSystemManager, LibraryMonitor, LoggerFactory, ProviderManager); serviceCollection.AddSingleton(CollectionManager); - PlaylistManager = new PlaylistManager(LibraryManager, FileSystemManager, LibraryMonitor, LoggerFactory, UserManager, ProviderManager); - serviceCollection.AddSingleton(PlaylistManager); + serviceCollection.AddSingleton(typeof(IPlaylistManager), typeof(PlaylistManager)); LiveTvManager = new LiveTvManager(this, ServerConfigurationManager, LoggerFactory, ItemRepository, ImageProcessor, UserDataManager, DtoService, UserManager, LibraryManager, TaskManager, LocalizationManager, JsonSerializer, FileSystemManager, () => ChannelManager); serviceCollection.AddSingleton(LiveTvManager); @@ -873,7 +896,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IAuthorizationContext>(authContext); serviceCollection.AddSingleton<ISessionContext>(new SessionContext(UserManager, authContext, SessionManager)); - AuthService = new AuthService(UserManager, authContext, ServerConfigurationManager, SessionManager, NetworkManager); + AuthService = new AuthService(authContext, ServerConfigurationManager, SessionManager, NetworkManager); serviceCollection.AddSingleton(AuthService); SubtitleEncoder = new MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder(LibraryManager, LoggerFactory, ApplicationPaths, FileSystemManager, MediaEncoder, JsonSerializer, HttpClient, MediaSourceManager, ProcessFactory); @@ -881,7 +904,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(typeof(IResourceFileManager), typeof(ResourceFileManager)); - displayPreferencesRepo.Initialize(); + _displayPreferencesRepository.Initialize(); var userDataRepo = new SqliteUserDataRepository(LoggerFactory, ApplicationPaths); @@ -950,18 +973,15 @@ namespace Emby.Server.Implementations } } - private IImageProcessor GetImageProcessor() - { - return new ImageProcessor(LoggerFactory, ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder); - } - /// <summary> /// Gets the user repository. /// </summary> - /// <returns>Task{IUserRepository}.</returns> - private IUserRepository GetUserRepository() + /// <returns><see cref="Task{SqliteUserRepository}" />.</returns> + private SqliteUserRepository GetUserRepository() { - var repo = new SqliteUserRepository(LoggerFactory, ApplicationPaths, JsonSerializer); + var repo = new SqliteUserRepository( + LoggerFactory.CreateLogger<SqliteUserRepository>(), + ApplicationPaths); repo.Initialize(); @@ -1022,9 +1042,11 @@ namespace Emby.Server.Implementations .Select(x => Assembly.LoadFrom(x)) .SelectMany(x => x.ExportedTypes) .Where(x => x.IsClass && !x.IsAbstract && !x.IsInterface && !x.IsGenericType) - .ToList(); + .ToArray(); - types.AddRange(types); + 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) @@ -1034,8 +1056,8 @@ namespace Emby.Server.Implementations .Where(x => x != null) .ToArray(); - int oldLen = _plugins.Length; - Array.Resize<IPlugin>(ref _plugins, _plugins.Length + plugins.Length); + oldLen = _plugins.Length; + Array.Resize(ref _plugins, oldLen + plugins.Length); plugins.CopyTo(_plugins, oldLen); var entries = types.Where(x => x.IsAssignableFrom(typeof(IServerEntryPoint))) @@ -1044,8 +1066,8 @@ namespace Emby.Server.Implementations .Cast<IServerEntryPoint>() .ToList(); - await Task.WhenAll(StartEntryPoints(entries, true)); - await Task.WhenAll(StartEntryPoints(entries, false)); + await Task.WhenAll(StartEntryPoints(entries, true)).ConfigureAwait(false); + await Task.WhenAll(StartEntryPoints(entries, false)).ConfigureAwait(false); } /// <summary> @@ -1084,7 +1106,7 @@ namespace Emby.Server.Implementations GetExports<IMetadataSaver>(), GetExports<IExternalId>()); - ImageProcessor.AddParts(GetExports<IImageEnhancer>()); + ImageProcessor.ImageEnhancers = GetExports<IImageEnhancer>(); LiveTvManager.AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>()); @@ -1152,7 +1174,7 @@ namespace Emby.Server.Implementations { Logger.LogInformation("Loading assemblies"); - AllConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray(); + _allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray(); } private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies) @@ -1208,25 +1230,11 @@ namespace Emby.Server.Implementations private CertificateInfo GetCertificateInfo(bool generateCertificate) { - if (!string.IsNullOrWhiteSpace(ServerConfigurationManager.Configuration.CertificatePath)) - { - // Custom cert - return new CertificateInfo - { - Path = ServerConfigurationManager.Configuration.CertificatePath, - Password = ServerConfigurationManager.Configuration.CertificatePassword - }; - } - - // Generate self-signed cert - var certHost = GetHostnameFromExternalDns(ServerConfigurationManager.Configuration.WanDdns); - var certPath = Path.Combine(ServerConfigurationManager.ApplicationPaths.ProgramDataPath, "ssl", "cert_" + (certHost + "2").GetMD5().ToString("N") + ".pfx"); - const string Password = "embycert"; - + // Custom cert return new CertificateInfo { - Path = certPath, - Password = Password + Path = ServerConfigurationManager.Configuration.CertificatePath, + Password = ServerConfigurationManager.Configuration.CertificatePassword }; } @@ -1413,17 +1421,6 @@ namespace Emby.Server.Implementations { var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); - string wanAddress; - - if (string.IsNullOrEmpty(ServerConfigurationManager.Configuration.WanDdns)) - { - wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false); - } - else - { - wanAddress = GetWanApiUrl(ServerConfigurationManager.Configuration.WanDdns); - } - return new SystemInfo { HasPendingRestart = HasPendingRestart, @@ -1445,7 +1442,6 @@ namespace Emby.Server.Implementations OperatingSystemDisplayName = OperatingSystem.Name, CanSelfRestart = CanSelfRestart, CanLaunchWebBrowser = CanLaunchWebBrowser, - WanAddress = wanAddress, HasUpdateAvailable = HasUpdateAvailable, TranscodingTempPath = ApplicationPaths.TranscodingTempPath, ServerName = FriendlyName, @@ -1458,37 +1454,21 @@ namespace Emby.Server.Implementations }; } - public WakeOnLanInfo[] GetWakeOnLanInfo() - { - return NetworkManager.GetMacAddresses() - .Select(i => new WakeOnLanInfo - { - MacAddress = i - }) - .ToArray(); - } + public IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo() + => NetworkManager.GetMacAddresses() + .Select(i => new WakeOnLanInfo(i)) + .ToList(); public async Task<PublicSystemInfo> GetPublicSystemInfo(CancellationToken cancellationToken) { var localAddress = await GetLocalApiUrl(cancellationToken).ConfigureAwait(false); - string wanAddress; - - if (string.IsNullOrEmpty(ServerConfigurationManager.Configuration.WanDdns)) - { - wanAddress = await GetWanApiUrlFromExternal(cancellationToken).ConfigureAwait(false); - } - else - { - wanAddress = GetWanApiUrl(ServerConfigurationManager.Configuration.WanDdns); - } return new PublicSystemInfo { Version = ApplicationVersion, ProductName = ApplicationProductName, Id = SystemId, OperatingSystem = OperatingSystem.Id.ToString(), - WanAddress = wanAddress, ServerName = FriendlyName, LocalAddress = localAddress }; @@ -1520,33 +1500,6 @@ namespace Emby.Server.Implementations return null; } - public async Task<string> GetWanApiUrlFromExternal(CancellationToken cancellationToken) - { - const string Url = "http://ipv4.icanhazip.com"; - try - { - using (var response = await HttpClient.Get(new HttpRequestOptions - { - Url = Url, - LogErrorResponseBody = false, - LogErrors = false, - LogRequest = false, - BufferContent = false, - CancellationToken = cancellationToken - }).ConfigureAwait(false)) - { - string res = await response.ReadToEndAsync().ConfigureAwait(false); - return GetWanApiUrl(res.Trim()); - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Error getting WAN Ip address information"); - } - - return null; - } - /// <summary> /// Removes the scope id from IPv6 addresses. /// </summary> @@ -1589,32 +1542,6 @@ namespace Emby.Server.Implementations HttpPort.ToString(CultureInfo.InvariantCulture)); } - public string GetWanApiUrl(IPAddress ipAddress) - { - if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6) - { - var str = RemoveScopeId(ipAddress.ToString()); - - return GetWanApiUrl("[" + str + "]"); - } - - return GetWanApiUrl(ipAddress.ToString()); - } - - public string GetWanApiUrl(string host) - { - if (EnableHttps) - { - return string.Format("https://{0}:{1}", - host, - ServerConfigurationManager.Configuration.PublicHttpsPort.ToString(CultureInfo.InvariantCulture)); - } - - return string.Format("http://{0}:{1}", - host, - ServerConfigurationManager.Configuration.PublicPort.ToString(CultureInfo.InvariantCulture)); - } - public Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken) { return GetLocalIpAddressesInternal(true, 0, cancellationToken); @@ -1682,8 +1609,8 @@ namespace Emby.Server.Implementations private async Task<bool> IsIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken) { - if (address.Equals(IPAddress.Loopback) || - address.Equals(IPAddress.IPv6Loopback)) + if (address.Equals(IPAddress.Loopback) + || address.Equals(IPAddress.IPv6Loopback)) { return true; } @@ -1696,12 +1623,6 @@ namespace Emby.Server.Implementations return cachedResult; } -#if DEBUG - const bool LogPing = true; -#else - const bool LogPing = false; -#endif - try { using (var response = await HttpClient.SendAsync( @@ -1709,8 +1630,6 @@ namespace Emby.Server.Implementations { Url = apiUrl, LogErrorResponseBody = false, - LogErrors = LogPing, - LogRequest = LogPing, BufferContent = false, CancellationToken = cancellationToken }, HttpMethod.Post).ConfigureAwait(false)) @@ -1911,10 +1830,12 @@ namespace Emby.Server.Implementations } } - UserRepository.Dispose(); + _userRepository?.Dispose(); + _displayPreferencesRepository.Dispose(); } - UserRepository = null; + _userRepository = null; + _displayPreferencesRepository = null; _disposed = true; } diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index e9961e8bd..8e5f5b561 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading; @@ -206,7 +207,7 @@ namespace Emby.Server.Implementations.Channels try { - return GetChannelProvider(i).IsEnabledFor(user.Id.ToString("N")); + return GetChannelProvider(i).IsEnabledFor(user.Id.ToString("N", CultureInfo.InvariantCulture)); } catch { @@ -511,7 +512,7 @@ namespace Emby.Server.Implementations.Channels IncludeItemTypes = new[] { typeof(Channel).Name }, OrderBy = new ValueTuple<string, SortOrder>[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) } - }).Select(i => GetChannelFeatures(i.ToString("N"))).ToArray(); + }).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray(); } public ChannelFeatures GetChannelFeatures(string id) @@ -552,7 +553,7 @@ namespace Emby.Server.Implementations.Channels SupportsSortOrderToggle = features.SupportsSortOrderToggle, SupportsLatestMedia = supportsLatest, Name = channel.Name, - Id = channel.Id.ToString("N"), + Id = channel.Id.ToString("N", CultureInfo.InvariantCulture), SupportsContentDownloading = features.SupportsContentDownloading, AutoRefreshLevels = features.AutoRefreshLevels }; @@ -740,7 +741,7 @@ namespace Emby.Server.Implementations.Channels bool sortDescending, CancellationToken cancellationToken) { - var userId = user == null ? null : user.Id.ToString("N"); + var userId = user == null ? null : user.Id.ToString("N", CultureInfo.InvariantCulture); var cacheLength = CacheLength; var cachePath = GetChannelDataCachePath(channel, userId, externalFolderId, sortField, sortDescending); @@ -836,7 +837,7 @@ namespace Emby.Server.Implementations.Channels ChannelItemSortField? sortField, bool sortDescending) { - var channelId = GetInternalChannelId(channel.Name).ToString("N"); + var channelId = GetInternalChannelId(channel.Name).ToString("N", CultureInfo.InvariantCulture); var userCacheKey = string.Empty; @@ -846,10 +847,10 @@ namespace Emby.Server.Implementations.Channels userCacheKey = hasCacheKey.GetCacheKey(userId) ?? string.Empty; } - var filename = string.IsNullOrEmpty(externalFolderId) ? "root" : externalFolderId.GetMD5().ToString("N"); + var filename = string.IsNullOrEmpty(externalFolderId) ? "root" : externalFolderId.GetMD5().ToString("N", CultureInfo.InvariantCulture); filename += userCacheKey; - var version = ((channel.DataVersion ?? string.Empty) + "2").GetMD5().ToString("N"); + var version = ((channel.DataVersion ?? string.Empty) + "2").GetMD5().ToString("N", CultureInfo.InvariantCulture); if (sortField.HasValue) { @@ -860,7 +861,7 @@ namespace Emby.Server.Implementations.Channels filename += "-sortDescending"; } - filename = filename.GetMD5().ToString("N"); + filename = filename.GetMD5().ToString("N", CultureInfo.InvariantCulture); return Path.Combine(_config.ApplicationPaths.CachePath, "channels", diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index 2b99e0ddf..bb5057b1c 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading; @@ -182,7 +183,7 @@ namespace Emby.Server.Implementations.Collections public void AddToCollection(Guid collectionId, IEnumerable<Guid> ids) { - AddToCollection(collectionId, ids.Select(i => i.ToString("N")), true, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem))); + AddToCollection(collectionId, ids.Select(i => i.ToString("N", CultureInfo.InvariantCulture)), true, new MetadataRefreshOptions(new DirectoryService(_logger, _fileSystem))); } private void AddToCollection(Guid collectionId, IEnumerable<string> ids, bool fireEvent, MetadataRefreshOptions refreshOptions) diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs index c4fa68cac..c7f92b80b 100644 --- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -66,7 +66,7 @@ namespace Emby.Server.Implementations.Configuration { base.AddParts(factories); - UpdateTranscodingTempPath(); + UpdateTranscodePath(); } /// <summary> @@ -87,13 +87,13 @@ namespace Emby.Server.Implementations.Configuration /// <summary> /// Updates the transcoding temporary path. /// </summary> - private void UpdateTranscodingTempPath() + private void UpdateTranscodePath() { var encodingConfig = this.GetConfiguration<EncodingOptions>("encoding"); ((ServerApplicationPaths)ApplicationPaths).TranscodingTempPath = string.IsNullOrEmpty(encodingConfig.TranscodingTempPath) ? null : - Path.Combine(encodingConfig.TranscodingTempPath, "transcoding-temp"); + Path.Combine(encodingConfig.TranscodingTempPath, "transcodes"); } protected override void OnNamedConfigurationUpdated(string key, object configuration) @@ -102,7 +102,7 @@ namespace Emby.Server.Implementations.Configuration if (string.Equals(key, "encoding", StringComparison.OrdinalIgnoreCase)) { - UpdateTranscodingTempPath(); + UpdateTranscodePath(); } } diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index 9bc60972a..62408ee70 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -6,8 +6,8 @@ namespace Emby.Server.Implementations { public static readonly Dictionary<string, string> Configuration = new Dictionary<string, string> { - {"HttpListenerHost:DefaultRedirectPath", "web/index.html"}, - {"MusicBrainz:BaseUrl", "https://www.musicbrainz.org"} + { "HttpListenerHost:DefaultRedirectPath", "web/index.html" }, + { "MusicBrainz:BaseUrl", "https://www.musicbrainz.org" } }; } } diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index 6d7193ce2..23b77e268 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -1,14 +1,12 @@ using System; using System.Collections.Generic; -using System.Globalization; -using System.IO; using System.Security.Cryptography; -using System.Text; using MediaBrowser.Model.Cryptography; +using static MediaBrowser.Common.Cryptography.Constants; namespace Emby.Server.Implementations.Cryptography { - public class CryptographyProvider : ICryptoProvider + public class CryptographyProvider : ICryptoProvider, IDisposable { private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>() { @@ -28,59 +26,28 @@ namespace Emby.Server.Implementations.Cryptography "System.Security.Cryptography.SHA512" }; - public string DefaultHashMethod => "PBKDF2"; - private RandomNumberGenerator _randomNumberGenerator; - private const int _defaultIterations = 1000; + private bool _disposed = false; public CryptographyProvider() { - //FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto - //Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1 - //there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one - //Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1 + // FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto + // Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1 + // there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one + // Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1 _randomNumberGenerator = RandomNumberGenerator.Create(); } - public Guid GetMD5(string str) - { - return new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str))); - } - - public byte[] ComputeSHA1(byte[] bytes) - { - using (var provider = SHA1.Create()) - { - return provider.ComputeHash(bytes); - } - } - - public byte[] ComputeMD5(Stream str) - { - using (var provider = MD5.Create()) - { - return provider.ComputeHash(str); - } - } - - public byte[] ComputeMD5(byte[] bytes) - { - using (var provider = MD5.Create()) - { - return provider.ComputeHash(bytes); - } - } + public string DefaultHashMethod => "PBKDF2"; public IEnumerable<string> GetSupportedHashMethods() - { - return _supportedHashMethods; - } + => _supportedHashMethods; private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations) { - //downgrading for now as we need this library to be dotnetstandard compliant - //with this downgrade we'll add a check to make sure we're on the downgrade method at the moment + // downgrading for now as we need this library to be dotnetstandard compliant + // with this downgrade we'll add a check to make sure we're on the downgrade method at the moment if (method == DefaultHashMethod) { using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations)) @@ -93,20 +60,16 @@ namespace Emby.Server.Implementations.Cryptography } public byte[] ComputeHash(string hashMethod, byte[] bytes) - { - return ComputeHash(hashMethod, bytes, Array.Empty<byte>()); - } + => ComputeHash(hashMethod, bytes, Array.Empty<byte>()); public byte[] ComputeHashWithDefaultMethod(byte[] bytes) - { - return ComputeHash(DefaultHashMethod, bytes); - } + => ComputeHash(DefaultHashMethod, bytes); public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt) { if (hashMethod == DefaultHashMethod) { - return PBKDF2(hashMethod, bytes, salt, _defaultIterations); + return PBKDF2(hashMethod, bytes, salt, DefaultIterations); } else if (_supportedHashMethods.Contains(hashMethod)) { @@ -125,44 +88,46 @@ namespace Emby.Server.Implementations.Cryptography } } } - else - { - throw new CryptographicException($"Requested hash method is not supported: {hashMethod}"); - } + + throw new CryptographicException($"Requested hash method is not supported: {hashMethod}"); + } public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt) + => PBKDF2(DefaultHashMethod, bytes, salt, DefaultIterations); + + public byte[] GenerateSalt() + => GenerateSalt(DefaultSaltLength); + + public byte[] GenerateSalt(int length) + { + byte[] salt = new byte[length]; + _randomNumberGenerator.GetBytes(salt); + return salt; + } + + /// <inheritdoc /> + public void Dispose() { - return PBKDF2(DefaultHashMethod, bytes, salt, _defaultIterations); + Dispose(true); + GC.SuppressFinalize(this); } - public byte[] ComputeHash(PasswordHash hash) + protected virtual void Dispose(bool disposing) { - int iterations = _defaultIterations; - if (!hash.Parameters.ContainsKey("iterations")) + if (_disposed) { - hash.Parameters.Add("iterations", _defaultIterations.ToString(CultureInfo.InvariantCulture)); + return; } - else + + if (disposing) { - try - { - iterations = int.Parse(hash.Parameters["iterations"]); - } - catch (Exception e) - { - throw new InvalidDataException($"Couldn't successfully parse iterations value from string: {hash.Parameters["iterations"]}", e); - } + _randomNumberGenerator.Dispose(); } - return PBKDF2(hash.Id, hash.HashBytes, hash.SaltBytes, iterations); - } + _randomNumberGenerator = null; - public byte[] GenerateSalt() - { - byte[] salt = new byte[64]; - _randomNumberGenerator.GetBytes(salt); - return salt; + _disposed = true; } } } diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 010ad6384..4e392f6c9 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -53,10 +53,10 @@ namespace Emby.Server.Implementations.Data protected virtual int? CacheSize => null; /// <summary> - /// Gets the journal mode. + /// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" /> /// </summary> /// <value>The journal mode.</value> - protected virtual string JournalMode => "WAL"; + protected virtual string JournalMode => "TRUNCATE"; /// <summary> /// Gets the page size. @@ -124,7 +124,7 @@ namespace Emby.Server.Implementations.Data } WriteConnection.Execute("PRAGMA temp_store=" + (int)TempStore); - + // Configuration and pragmas can affect VACUUM so it needs to be last. WriteConnection.Execute("VACUUM"); @@ -218,7 +218,7 @@ namespace Emby.Server.Implementations.Data WriteLock.Wait(); try { - WriteConnection.Dispose(); + WriteConnection?.Dispose(); } finally { diff --git a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs index 01ef9851d..2cd4d65b3 100644 --- a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs @@ -1,44 +1,45 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; +using System.Text.Json; using System.Threading; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Json; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; using SQLitePCL.pretty; namespace Emby.Server.Implementations.Data { /// <summary> - /// Class SQLiteDisplayPreferencesRepository + /// Class SQLiteDisplayPreferencesRepository. /// </summary> public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository { private readonly IFileSystem _fileSystem; - public SqliteDisplayPreferencesRepository(ILoggerFactory loggerFactory, IJsonSerializer jsonSerializer, IApplicationPaths appPaths, IFileSystem fileSystem) - : base(loggerFactory.CreateLogger(nameof(SqliteDisplayPreferencesRepository))) + private readonly JsonSerializerOptions _jsonOptions; + + public SqliteDisplayPreferencesRepository(ILogger<SqliteDisplayPreferencesRepository> logger, IApplicationPaths appPaths, IFileSystem fileSystem) + : base(logger) { - _jsonSerializer = jsonSerializer; _fileSystem = fileSystem; + + _jsonOptions = JsonDefaults.GetOptions(); + DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db"); } /// <summary> - /// Gets the name of the repository + /// Gets the name of the repository. /// </summary> /// <value>The name.</value> public string Name => "SQLite"; - /// <summary> - /// The _json serializer - /// </summary> - private readonly IJsonSerializer _jsonSerializer; - public void Initialize() { try @@ -61,14 +62,14 @@ namespace Emby.Server.Implementations.Data /// <returns>Task.</returns> private void InitializeInternal() { - using (var connection = GetConnection()) + string[] queries = { - string[] queries = { - - "create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)", - "create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)" - }; + "create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)", + "create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)" + }; + using (var connection = GetConnection()) + { connection.RunQueries(queries); } } @@ -80,7 +81,6 @@ namespace Emby.Server.Implementations.Data /// <param name="userId">The user id.</param> /// <param name="client">The client.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> /// <exception cref="ArgumentNullException">item</exception> public void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, CancellationToken cancellationToken) { @@ -98,16 +98,15 @@ namespace Emby.Server.Implementations.Data using (var connection = GetConnection()) { - connection.RunInTransaction(db => - { - SaveDisplayPreferences(displayPreferences, userId, client, db); - }, TransactionMode); + connection.RunInTransaction( + db => SaveDisplayPreferences(displayPreferences, userId, client, db), + TransactionMode); } } private void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, IDatabaseConnection connection) { - var serialized = _jsonSerializer.SerializeToBytes(displayPreferences); + var serialized = JsonSerializer.SerializeToUtf8Bytes(displayPreferences, _jsonOptions); using (var statement = connection.PrepareStatement("replace into userdisplaypreferences (id, userid, client, data) values (@id, @userId, @client, @data)")) { @@ -126,7 +125,6 @@ namespace Emby.Server.Implementations.Data /// <param name="displayPreferences">The display preferences.</param> /// <param name="userId">The user id.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> /// <exception cref="ArgumentNullException">item</exception> public void SaveAllDisplayPreferences(IEnumerable<DisplayPreferences> displayPreferences, Guid userId, CancellationToken cancellationToken) { @@ -139,13 +137,15 @@ namespace Emby.Server.Implementations.Data using (var connection = GetConnection()) { - connection.RunInTransaction(db => - { - foreach (var displayPreference in displayPreferences) + connection.RunInTransaction( + db => { - SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db); - } - }, TransactionMode); + foreach (var displayPreference in displayPreferences) + { + SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db); + } + }, + TransactionMode); } } @@ -179,12 +179,12 @@ namespace Emby.Server.Implementations.Data return Get(row); } } - - return new DisplayPreferences - { - Id = guidId.ToString("N") - }; } + + return new DisplayPreferences + { + Id = guidId.ToString("N", CultureInfo.InvariantCulture) + }; } /// <summary> @@ -198,15 +198,13 @@ namespace Emby.Server.Implementations.Data var list = new List<DisplayPreferences>(); using (var connection = GetConnection(true)) + using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId")) { - using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId")) - { - statement.TryBind("@userId", userId.ToGuidBlob()); + statement.TryBind("@userId", userId.ToGuidBlob()); - foreach (var row in statement.ExecuteQuery()) - { - list.Add(Get(row)); - } + foreach (var row in statement.ExecuteQuery()) + { + list.Add(Get(row)); } } @@ -214,22 +212,12 @@ namespace Emby.Server.Implementations.Data } private DisplayPreferences Get(IReadOnlyList<IResultSetValue> row) - { - using (var stream = new MemoryStream(row[0].ToBlob())) - { - stream.Position = 0; - return _jsonSerializer.DeserializeFromStream<DisplayPreferences>(stream); - } - } + => JsonSerializer.Deserialize<DisplayPreferences>(row[0].ToBlob(), _jsonOptions); public void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, CancellationToken cancellationToken) - { - SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken); - } + => SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken); public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client) - { - return GetDisplayPreferences(displayPreferencesId, new Guid(userId), client); - } + => GetDisplayPreferences(displayPreferencesId, new Guid(userId), client); } } diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs index c0f27b25a..e7c394b54 100644 --- a/Emby.Server.Implementations/Data/SqliteExtensions.cs +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -18,10 +18,6 @@ namespace Emby.Server.Implementations.Data connection.RunInTransaction(conn => { - //foreach (var query in queries) - //{ - // conn.Execute(query); - //} conn.ExecuteAll(string.Join(";", queries)); }); } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 1cefcec7c..31a661c5d 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -5,8 +5,11 @@ using System.IO; using System.Linq; using System.Runtime.Serialization; using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using Emby.Server.Implementations.Playlists; +using MediaBrowser.Common.Json; using MediaBrowser.Controller; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; @@ -38,14 +41,6 @@ namespace Emby.Server.Implementations.Data { private const string ChaptersTableName = "Chapters2"; - private readonly TypeMapper _typeMapper; - - /// <summary> - /// Gets the json serializer. - /// </summary> - /// <value>The json serializer.</value> - private readonly IJsonSerializer _jsonSerializer; - /// <summary> /// The _app paths /// </summary> @@ -53,33 +48,31 @@ namespace Emby.Server.Implementations.Data private readonly IServerApplicationHost _appHost; private readonly ILocalizationManager _localization; + private readonly TypeMapper _typeMapper; + private readonly JsonSerializerOptions _jsonOptions; + /// <summary> /// Initializes a new instance of the <see cref="SqliteItemRepository"/> class. /// </summary> public SqliteItemRepository( IServerConfigurationManager config, IServerApplicationHost appHost, - IJsonSerializer jsonSerializer, - ILoggerFactory loggerFactory, + ILogger<SqliteItemRepository> logger, ILocalizationManager localization) - : base(loggerFactory.CreateLogger(nameof(SqliteItemRepository))) + : base(logger) { if (config == null) { throw new ArgumentNullException(nameof(config)); } - if (jsonSerializer == null) - { - throw new ArgumentNullException(nameof(jsonSerializer)); - } - - _appHost = appHost; _config = config; - _jsonSerializer = jsonSerializer; - _typeMapper = new TypeMapper(); + _appHost = appHost; _localization = localization; + _typeMapper = new TypeMapper(); + _jsonOptions = JsonDefaults.GetOptions(); + DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db"); } @@ -99,35 +92,114 @@ namespace Emby.Server.Implementations.Data /// </summary> public void Initialize(SqliteUserDataRepository userDataRepo, IUserManager userManager) { - using (var connection = GetConnection()) - { - const string createMediaStreamsTableCommand + const string CreateMediaStreamsTableCommand = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, ColorPrimaries TEXT NULL, ColorSpace TEXT NULL, ColorTransfer TEXT NULL, PRIMARY KEY (ItemId, StreamIndex))"; - string[] queries = { - "PRAGMA locking_mode=EXCLUSIVE", + string[] queries = + { + "PRAGMA locking_mode=EXCLUSIVE", - "create table if not exists TypedBaseItems (guid GUID primary key NOT NULL, type TEXT NOT NULL, data BLOB NULL, ParentId GUID NULL, Path TEXT NULL)", + "create table if not exists TypedBaseItems (guid GUID primary key NOT NULL, type TEXT NOT NULL, data BLOB NULL, ParentId GUID NULL, Path TEXT NULL)", - "create table if not exists AncestorIds (ItemId GUID NOT NULL, AncestorId GUID NOT NULL, AncestorIdText TEXT NOT NULL, PRIMARY KEY (ItemId, AncestorId))", - "create index if not exists idx_AncestorIds1 on AncestorIds(AncestorId)", - "create index if not exists idx_AncestorIds5 on AncestorIds(AncestorIdText,ItemId)", + "create table if not exists AncestorIds (ItemId GUID NOT NULL, AncestorId GUID NOT NULL, AncestorIdText TEXT NOT NULL, PRIMARY KEY (ItemId, AncestorId))", + "create index if not exists idx_AncestorIds1 on AncestorIds(AncestorId)", + "create index if not exists idx_AncestorIds5 on AncestorIds(AncestorIdText,ItemId)", - "create table if not exists ItemValues (ItemId GUID NOT NULL, Type INT NOT NULL, Value TEXT NOT NULL, CleanValue TEXT NOT NULL)", + "create table if not exists ItemValues (ItemId GUID NOT NULL, Type INT NOT NULL, Value TEXT NOT NULL, CleanValue TEXT NOT NULL)", - "create table if not exists People (ItemId GUID, Name TEXT NOT NULL, Role TEXT, PersonType TEXT, SortOrder int, ListOrder int)", + "create table if not exists People (ItemId GUID, Name TEXT NOT NULL, Role TEXT, PersonType TEXT, SortOrder int, ListOrder int)", - "drop index if exists idxPeopleItemId", - "create index if not exists idxPeopleItemId1 on People(ItemId,ListOrder)", - "create index if not exists idxPeopleName on People(Name)", + "drop index if exists idxPeopleItemId", + "create index if not exists idxPeopleItemId1 on People(ItemId,ListOrder)", + "create index if not exists idxPeopleName on People(Name)", - "create table if not exists " + ChaptersTableName + " (ItemId GUID, ChapterIndex INT NOT NULL, StartPositionTicks BIGINT NOT NULL, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))", + "create table if not exists " + ChaptersTableName + " (ItemId GUID, ChapterIndex INT NOT NULL, StartPositionTicks BIGINT NOT NULL, Name TEXT, ImagePath TEXT, PRIMARY KEY (ItemId, ChapterIndex))", - createMediaStreamsTableCommand, + CreateMediaStreamsTableCommand, + + "pragma shrink_memory" + }; - "pragma shrink_memory" - }; + string[] postQueries = + { + // obsolete + "drop index if exists idx_TypedBaseItems", + "drop index if exists idx_mediastreams", + "drop index if exists idx_mediastreams1", + "drop index if exists idx_"+ChaptersTableName, + "drop index if exists idx_UserDataKeys1", + "drop index if exists idx_UserDataKeys2", + "drop index if exists idx_TypeTopParentId3", + "drop index if exists idx_TypeTopParentId2", + "drop index if exists idx_TypeTopParentId4", + "drop index if exists idx_Type", + "drop index if exists idx_TypeTopParentId", + "drop index if exists idx_GuidType", + "drop index if exists idx_TopParentId", + "drop index if exists idx_TypeTopParentId6", + "drop index if exists idx_ItemValues2", + "drop index if exists Idx_ProviderIds", + "drop index if exists idx_ItemValues3", + "drop index if exists idx_ItemValues4", + "drop index if exists idx_ItemValues5", + "drop index if exists idx_UserDataKeys3", + "drop table if exists UserDataKeys", + "drop table if exists ProviderIds", + "drop index if exists Idx_ProviderIds1", + "drop table if exists Images", + "drop index if exists idx_Images", + "drop index if exists idx_TypeSeriesPresentationUniqueKey", + "drop index if exists idx_SeriesPresentationUniqueKey", + "drop index if exists idx_TypeSeriesPresentationUniqueKey2", + "drop index if exists idx_AncestorIds3", + "drop index if exists idx_AncestorIds4", + "drop index if exists idx_AncestorIds2", + + "create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)", + "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)", + + "create index if not exists idx_PresentationUniqueKey on TypedBaseItems(PresentationUniqueKey)", + "create index if not exists idx_GuidTypeIsFolderIsVirtualItem on TypedBaseItems(Guid,Type,IsFolder,IsVirtualItem)", + "create index if not exists idx_CleanNameType on TypedBaseItems(CleanName,Type)", + + // covering index + "create index if not exists idx_TopParentIdGuid on TypedBaseItems(TopParentId,Guid)", + + // series + "create index if not exists idx_TypeSeriesPresentationUniqueKey1 on TypedBaseItems(Type,SeriesPresentationUniqueKey,PresentationUniqueKey,SortName)", + + // series counts + // seriesdateplayed sort order + "create index if not exists idx_TypeSeriesPresentationUniqueKey3 on TypedBaseItems(SeriesPresentationUniqueKey,Type,IsFolder,IsVirtualItem)", + + // live tv programs + "create index if not exists idx_TypeTopParentIdStartDate on TypedBaseItems(Type,TopParentId,StartDate)", + + // covering index for getitemvalues + "create index if not exists idx_TypeTopParentIdGuid on TypedBaseItems(Type,TopParentId,Guid)", + + // used by movie suggestions + "create index if not exists idx_TypeTopParentIdGroup on TypedBaseItems(Type,TopParentId,PresentationUniqueKey)", + "create index if not exists idx_TypeTopParentId5 on TypedBaseItems(TopParentId,IsVirtualItem)", + + // latest items + "create index if not exists idx_TypeTopParentId9 on TypedBaseItems(TopParentId,Type,IsVirtualItem,PresentationUniqueKey,DateCreated)", + "create index if not exists idx_TypeTopParentId8 on TypedBaseItems(TopParentId,IsFolder,IsVirtualItem,PresentationUniqueKey,DateCreated)", + + // resume + "create index if not exists idx_TypeTopParentId7 on TypedBaseItems(TopParentId,MediaType,IsVirtualItem,PresentationUniqueKey)", + + // items by name + "create index if not exists idx_ItemValues6 on ItemValues(ItemId,Type,CleanValue)", + "create index if not exists idx_ItemValues7 on ItemValues(Type,CleanValue,ItemId)", + + // Used to update inherited tags + "create index if not exists idx_ItemValues8 on ItemValues(Type, ItemId, Value)", + }; + + using (var connection = GetConnection()) + { connection.RunQueries(queries); connection.RunInTransaction(db => @@ -235,83 +307,6 @@ namespace Emby.Server.Implementations.Data }, TransactionMode); - string[] postQueries = - { - // obsolete - "drop index if exists idx_TypedBaseItems", - "drop index if exists idx_mediastreams", - "drop index if exists idx_mediastreams1", - "drop index if exists idx_"+ChaptersTableName, - "drop index if exists idx_UserDataKeys1", - "drop index if exists idx_UserDataKeys2", - "drop index if exists idx_TypeTopParentId3", - "drop index if exists idx_TypeTopParentId2", - "drop index if exists idx_TypeTopParentId4", - "drop index if exists idx_Type", - "drop index if exists idx_TypeTopParentId", - "drop index if exists idx_GuidType", - "drop index if exists idx_TopParentId", - "drop index if exists idx_TypeTopParentId6", - "drop index if exists idx_ItemValues2", - "drop index if exists Idx_ProviderIds", - "drop index if exists idx_ItemValues3", - "drop index if exists idx_ItemValues4", - "drop index if exists idx_ItemValues5", - "drop index if exists idx_UserDataKeys3", - "drop table if exists UserDataKeys", - "drop table if exists ProviderIds", - "drop index if exists Idx_ProviderIds1", - "drop table if exists Images", - "drop index if exists idx_Images", - "drop index if exists idx_TypeSeriesPresentationUniqueKey", - "drop index if exists idx_SeriesPresentationUniqueKey", - "drop index if exists idx_TypeSeriesPresentationUniqueKey2", - "drop index if exists idx_AncestorIds3", - "drop index if exists idx_AncestorIds4", - "drop index if exists idx_AncestorIds2", - - "create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)", - "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)", - - "create index if not exists idx_PresentationUniqueKey on TypedBaseItems(PresentationUniqueKey)", - "create index if not exists idx_GuidTypeIsFolderIsVirtualItem on TypedBaseItems(Guid,Type,IsFolder,IsVirtualItem)", - "create index if not exists idx_CleanNameType on TypedBaseItems(CleanName,Type)", - - // covering index - "create index if not exists idx_TopParentIdGuid on TypedBaseItems(TopParentId,Guid)", - - // series - "create index if not exists idx_TypeSeriesPresentationUniqueKey1 on TypedBaseItems(Type,SeriesPresentationUniqueKey,PresentationUniqueKey,SortName)", - - // series counts - // seriesdateplayed sort order - "create index if not exists idx_TypeSeriesPresentationUniqueKey3 on TypedBaseItems(SeriesPresentationUniqueKey,Type,IsFolder,IsVirtualItem)", - - // live tv programs - "create index if not exists idx_TypeTopParentIdStartDate on TypedBaseItems(Type,TopParentId,StartDate)", - - // covering index for getitemvalues - "create index if not exists idx_TypeTopParentIdGuid on TypedBaseItems(Type,TopParentId,Guid)", - - // used by movie suggestions - "create index if not exists idx_TypeTopParentIdGroup on TypedBaseItems(Type,TopParentId,PresentationUniqueKey)", - "create index if not exists idx_TypeTopParentId5 on TypedBaseItems(TopParentId,IsVirtualItem)", - - // latest items - "create index if not exists idx_TypeTopParentId9 on TypedBaseItems(TopParentId,Type,IsVirtualItem,PresentationUniqueKey,DateCreated)", - "create index if not exists idx_TypeTopParentId8 on TypedBaseItems(TopParentId,IsFolder,IsVirtualItem,PresentationUniqueKey,DateCreated)", - - // resume - "create index if not exists idx_TypeTopParentId7 on TypedBaseItems(TopParentId,MediaType,IsVirtualItem,PresentationUniqueKey)", - - // items by name - "create index if not exists idx_ItemValues6 on ItemValues(ItemId,Type,CleanValue)", - "create index if not exists idx_ItemValues7 on ItemValues(Type,CleanValue,ItemId)", - - // Used to update inherited tags - "create index if not exists idx_ItemValues8 on ItemValues(Type, ItemId, Value)", - }; - connection.RunQueries(postQueries); } @@ -669,7 +664,7 @@ namespace Emby.Server.Implementations.Data if (TypeRequiresDeserialization(item.GetType())) { - saveItemStatement.TryBind("@data", _jsonSerializer.SerializeToBytes(item)); + saveItemStatement.TryBind("@data", JsonSerializer.SerializeToUtf8Bytes(item, _jsonOptions)); } else { @@ -696,7 +691,7 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBindNull("@EndDate"); } - saveItemStatement.TryBind("@ChannelId", item.ChannelId.Equals(Guid.Empty) ? null : item.ChannelId.ToString("N")); + saveItemStatement.TryBind("@ChannelId", item.ChannelId.Equals(Guid.Empty) ? null : item.ChannelId.ToString("N", CultureInfo.InvariantCulture)); if (item is IHasProgramAttributes hasProgramAttributes) { @@ -851,7 +846,7 @@ namespace Emby.Server.Implementations.Data } else { - saveItemStatement.TryBind("@TopParentId", topParent.Id.ToString("N")); + saveItemStatement.TryBind("@TopParentId", topParent.Id.ToString("N", CultureInfo.InvariantCulture)); } if (item is Trailer trailer && trailer.TrailerTypes.Length > 0) @@ -977,7 +972,7 @@ namespace Emby.Server.Implementations.Data } string artists = null; - if (item is IHasArtist hasArtists && hasArtists.Artists.Length > 0) + if (item is IHasArtist hasArtists && hasArtists.Artists.Count > 0) { artists = string.Join("|", hasArtists.Artists); } @@ -985,7 +980,7 @@ namespace Emby.Server.Implementations.Data string albumArtists = null; if (item is IHasAlbumArtist hasAlbumArtists - && hasAlbumArtists.AlbumArtists.Length > 0) + && hasAlbumArtists.AlbumArtists.Count > 0) { albumArtists = string.Join("|", hasAlbumArtists.AlbumArtists); } @@ -1298,18 +1293,13 @@ namespace Emby.Server.Implementations.Data if (TypeRequiresDeserialization(type)) { - using (var stream = new MemoryStream(reader[1].ToBlob())) + try { - stream.Position = 0; - - try - { - item = _jsonSerializer.DeserializeFromStream(stream, type) as BaseItem; - } - catch (SerializationException ex) - { - Logger.LogError(ex, "Error deserializing item"); - } + item = JsonSerializer.Deserialize(reader[1].ToBlob(), type, _jsonOptions) as BaseItem; + } + catch (JsonException ex) + { + Logger.LogError(ex, "Error deserializing item with JSON: {Data}", reader.GetString(1)); } } @@ -1999,14 +1989,14 @@ namespace Emby.Server.Implementations.Data throw new ArgumentNullException(nameof(chapters)); } + var idBlob = id.ToGuidBlob(); + using (var connection = GetConnection()) { connection.RunInTransaction(db => { - var idBlob = id.ToGuidBlob(); - - // First delete chapters - db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", idBlob); + // First delete chapters + db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", idBlob); InsertChapters(idBlob, chapters, db); @@ -2535,6 +2525,7 @@ namespace Emby.Server.Implementations.Data commandText += " where " + string.Join(" AND ", whereClauses); } + int count; using (var connection = GetConnection(true)) { using (var statement = PrepareStatement(connection, commandText)) @@ -2550,11 +2541,12 @@ namespace Emby.Server.Implementations.Data // Running this again will bind the params GetWhereClauses(query, statement); - var count = statement.ExecuteQuery().SelectScalarInt().First(); - LogQueryTime("GetCount", commandText, now); - return count; + count = statement.ExecuteQuery().SelectScalarInt().First(); } } + + LogQueryTime("GetCount", commandText, now); + return count; } public List<BaseItem> GetItemList(InternalItemsQuery query) @@ -2604,10 +2596,9 @@ namespace Emby.Server.Implementations.Data } } + var items = new List<BaseItem>(); using (var connection = GetConnection(true)) { - var items = new List<BaseItem>(); - using (var statement = PrepareStatement(connection, commandText)) { if (EnableJoinUserData(query)) @@ -2658,11 +2649,11 @@ namespace Emby.Server.Implementations.Data items = newList; } + } - LogQueryTime("GetItemList", commandText, now); + LogQueryTime("GetItemList", commandText, now); - return items; - } + return items; } private string FixUnicodeChars(string buffer) @@ -2726,7 +2717,7 @@ namespace Emby.Server.Implementations.Data if (elapsed >= SlowThreshold) { - Logger.LogWarning( + Logger.LogDebug( "{Method} query time (slow): {ElapsedMs}ms. Query: {Query}", methodName, elapsed, @@ -2748,15 +2739,13 @@ namespace Emby.Server.Implementations.Data var returnList = GetItemList(query); return new QueryResult<BaseItem> { - Items = returnList.ToArray(), + Items = returnList, TotalRecordCount = returnList.Count }; } var now = DateTime.UtcNow; - var list = new List<BaseItem>(); - // Hack for right now since we currently don't support filtering out these duplicates within a query if (query.Limit.HasValue && query.EnableGroupByMetadataKey) { @@ -2822,11 +2811,13 @@ namespace Emby.Server.Implementations.Data statementTexts.Add(commandText); } + var list = new List<BaseItem>(); + var result = new QueryResult<BaseItem>(); using (var connection = GetConnection(true)) { - return connection.RunInTransaction(db => + connection.RunInTransaction(db => { - var result = new QueryResult<BaseItem>(); + var statements = PrepareAll(db, statementTexts).ToList(); if (!isReturningZeroItems) @@ -2881,14 +2872,12 @@ namespace Emby.Server.Implementations.Data result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); } } - - LogQueryTime("GetItems", commandText, now); - - result.Items = list.ToArray(); - return result; - }, ReadTransactionMode); } + + LogQueryTime("GetItems", commandText, now); + result.Items = list; + return result; } private string GetOrderByText(InternalItemsQuery query) @@ -3054,10 +3043,9 @@ namespace Emby.Server.Implementations.Data } } + var list = new List<Guid>(); using (var connection = GetConnection(true)) { - var list = new List<Guid>(); - using (var statement = PrepareStatement(connection, commandText)) { if (EnableJoinUserData(query)) @@ -3076,11 +3064,10 @@ namespace Emby.Server.Implementations.Data list.Add(row[0].ReadGuidFromBlob()); } } - - LogQueryTime("GetItemList", commandText, now); - - return list; } + + LogQueryTime("GetItemList", commandText, now); + return list; } public List<Tuple<Guid, string>> GetItemIdsWithPath(InternalItemsQuery query) @@ -3142,6 +3129,7 @@ namespace Emby.Server.Implementations.Data { path = row.GetString(1); } + list.Add(new Tuple<Guid, string>(id, path)); } } @@ -3166,7 +3154,7 @@ namespace Emby.Server.Implementations.Data var returnList = GetItemIdsList(query); return new QueryResult<Guid> { - Items = returnList.ToArray(), + Items = returnList, TotalRecordCount = returnList.Count }; } @@ -3203,7 +3191,7 @@ namespace Emby.Server.Implementations.Data } } - var list = new List<Guid>(); + var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; var statementTexts = new List<string>(); @@ -3233,12 +3221,12 @@ namespace Emby.Server.Implementations.Data statementTexts.Add(commandText); } + var list = new List<Guid>(); + var result = new QueryResult<Guid>(); using (var connection = GetConnection(true)) { - return connection.RunInTransaction(db => + connection.RunInTransaction(db => { - var result = new QueryResult<Guid>(); - var statements = PrepareAll(db, statementTexts).ToList(); if (!isReturningZeroItems) @@ -3281,14 +3269,13 @@ namespace Emby.Server.Implementations.Data result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); } } - - LogQueryTime("GetItemIds", commandText, now); - - result.Items = list.ToArray(); - return result; - }, ReadTransactionMode); } + + LogQueryTime("GetItemIds", commandText, now); + + result.Items = list; + return result; } private bool IsAlphaNumeric(string str) @@ -3548,12 +3535,12 @@ namespace Emby.Server.Implementations.Data whereClauses.Add("ChannelId=@ChannelId"); if (statement != null) { - statement.TryBind("@ChannelId", query.ChannelIds[0].ToString("N")); + statement.TryBind("@ChannelId", query.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture)); } } else if (query.ChannelIds.Length > 1) { - var inClause = string.Join(",", query.ChannelIds.Select(i => "'" + i.ToString("N") + "'")); + var inClause = string.Join(",", query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); whereClauses.Add($"ChannelId in ({inClause})"); } @@ -4537,12 +4524,12 @@ namespace Emby.Server.Implementations.Data } if (statement != null) { - statement.TryBind("@TopParentId", queryTopParentIds[0].ToString("N")); + statement.TryBind("@TopParentId", queryTopParentIds[0].ToString("N", CultureInfo.InvariantCulture)); } } else if (queryTopParentIds.Length > 1) { - var val = string.Join(",", queryTopParentIds.Select(i => "'" + i.ToString("N") + "'")); + var val = string.Join(",", queryTopParentIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); if (enableItemsByName && includedItemByNameTypes.Count == 1) { @@ -4574,7 +4561,7 @@ namespace Emby.Server.Implementations.Data } if (query.AncestorIds.Length > 1) { - var inClause = string.Join(",", query.AncestorIds.Select(i => "'" + i.ToString("N") + "'")); + var inClause = string.Join(",", query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); whereClauses.Add(string.Format("Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause)); } if (!string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey)) @@ -4637,7 +4624,7 @@ namespace Emby.Server.Implementations.Data foreach (var folderId in query.BoxSetLibraryFolders) { - folderIdQueries.Add("data like '%" + folderId.ToString("N") + "%'"); + folderIdQueries.Add("data like '%" + folderId.ToString("N", CultureInfo.InvariantCulture) + "%'"); } whereClauses.Add("(" + string.Join(" OR ", folderIdQueries) + ")"); @@ -4864,22 +4851,25 @@ namespace Emby.Server.Implementations.Data private void UpdateInheritedTags(CancellationToken cancellationToken) { - using (var connection = GetConnection()) - { - connection.RunInTransaction(db => + string sql = string.Join( + ";", + new string[] { - connection.ExecuteAll(string.Join(";", new string[] - { - "delete from itemvalues where type = 6", + "delete from itemvalues where type = 6", - "insert into itemvalues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4", + "insert into itemvalues (ItemId, Type, Value, CleanValue) select ItemId, 6, Value, CleanValue from ItemValues where Type=4", - @"insert into itemvalues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue + @"insert into itemvalues (ItemId, Type, Value, CleanValue) select AncestorIds.itemid, 6, ItemValues.Value, ItemValues.CleanValue FROM AncestorIds LEFT JOIN ItemValues ON (AncestorIds.AncestorId = ItemValues.ItemId) where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type = 4 " + }); - })); + using (var connection = GetConnection()) + { + connection.RunInTransaction(db => + { + connection.ExecuteAll(sql); }, TransactionMode); } @@ -4933,23 +4923,23 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type { var idBlob = id.ToGuidBlob(); - // Delete people - ExecuteWithSingleParam(db, "delete from People where ItemId=@Id", idBlob); + // Delete people + ExecuteWithSingleParam(db, "delete from People where ItemId=@Id", idBlob); - // Delete chapters - ExecuteWithSingleParam(db, "delete from " + ChaptersTableName + " where ItemId=@Id", idBlob); + // Delete chapters + ExecuteWithSingleParam(db, "delete from " + ChaptersTableName + " where ItemId=@Id", idBlob); - // Delete media streams - ExecuteWithSingleParam(db, "delete from mediastreams where ItemId=@Id", idBlob); + // Delete media streams + ExecuteWithSingleParam(db, "delete from mediastreams where ItemId=@Id", idBlob); - // Delete ancestors - ExecuteWithSingleParam(db, "delete from AncestorIds where ItemId=@Id", idBlob); + // Delete ancestors + ExecuteWithSingleParam(db, "delete from AncestorIds where ItemId=@Id", idBlob); - // Delete item values - ExecuteWithSingleParam(db, "delete from ItemValues where ItemId=@Id", idBlob); + // Delete item values + ExecuteWithSingleParam(db, "delete from ItemValues where ItemId=@Id", idBlob); - // Delete the item - ExecuteWithSingleParam(db, "delete from TypedBaseItems where guid=@Id", idBlob); + // Delete the item + ExecuteWithSingleParam(db, "delete from TypedBaseItems where guid=@Id", idBlob); }, TransactionMode); } } @@ -4997,6 +4987,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type list.Add(row.GetString(0)); } } + return list; } } @@ -5161,7 +5152,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type var ancestorId = ancestorIds[i]; statement.TryBind("@AncestorId" + index, ancestorId.ToGuidBlob()); - statement.TryBind("@AncestorIdText" + index, ancestorId.ToString("N")); + statement.TryBind("@AncestorIdText" + index, ancestorId.ToString("N", CultureInfo.InvariantCulture)); } statement.Reset(); @@ -5247,10 +5238,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type commandText += " Group By CleanValue"; + var list = new List<string>(); using (var connection = GetConnection(true)) { - var list = new List<string>(); - using (var statement = PrepareStatement(connection, commandText)) { foreach (var row in statement.ExecuteQuery()) @@ -5262,10 +5252,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type } } - LogQueryTime("GetItemValueNames", commandText, now); - - return list; } + + LogQueryTime("GetItemValueNames", commandText, now); + return list; } private QueryResult<(BaseItem, ItemCounts)> GetItemValues(InternalItemsQuery query, int[] itemValueTypes, string returnType) @@ -5422,6 +5412,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type { statementTexts.Add(commandText); } + if (query.EnableTotalRecordCount) { var countText = "select " @@ -5433,98 +5424,98 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type statementTexts.Add(countText); } + var list = new List<(BaseItem, ItemCounts)>(); + var result = new QueryResult<(BaseItem, ItemCounts)>(); using (var connection = GetConnection(true)) { - return connection.RunInTransaction(db => - { - var list = new List<(BaseItem, ItemCounts)>(); - var result = new QueryResult<(BaseItem, ItemCounts)>(); - - var statements = PrepareAll(db, statementTexts).ToList(); - - if (!isReturningZeroItems) + connection.RunInTransaction( + db => { - using (var statement = statements[0]) + var statements = PrepareAll(db, statementTexts).ToList(); + + if (!isReturningZeroItems) { - statement.TryBind("@SelectType", returnType); - if (EnableJoinUserData(query)) + using (var statement = statements[0]) { - statement.TryBind("@UserId", query.User.InternalId); - } + statement.TryBind("@SelectType", returnType); + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.InternalId); + } - if (typeSubQuery != null) - { - GetWhereClauses(typeSubQuery, null); - } - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - GetWhereClauses(innerQuery, statement); - GetWhereClauses(outerQuery, statement); + if (typeSubQuery != null) + { + GetWhereClauses(typeSubQuery, null); + } - var hasEpisodeAttributes = HasEpisodeAttributes(query); - var hasProgramAttributes = HasProgramAttributes(query); - var hasServiceName = HasServiceName(query); - var hasStartDate = HasStartDate(query); - var hasTrailerTypes = HasTrailerTypes(query); - var hasArtistFields = HasArtistFields(query); - var hasSeriesFields = HasSeriesFields(query); + BindSimilarParams(query, statement); + BindSearchParams(query, statement); + GetWhereClauses(innerQuery, statement); + GetWhereClauses(outerQuery, statement); - foreach (var row in statement.ExecuteQuery()) - { - var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); - if (item != null) + var hasEpisodeAttributes = HasEpisodeAttributes(query); + var hasProgramAttributes = HasProgramAttributes(query); + var hasServiceName = HasServiceName(query); + var hasStartDate = HasStartDate(query); + var hasTrailerTypes = HasTrailerTypes(query); + var hasArtistFields = HasArtistFields(query); + var hasSeriesFields = HasSeriesFields(query); + + foreach (var row in statement.ExecuteQuery()) { - var countStartColumn = columns.Count - 1; + var item = GetItem(row, query, hasProgramAttributes, hasEpisodeAttributes, hasServiceName, hasStartDate, hasTrailerTypes, hasArtistFields, hasSeriesFields); + if (item != null) + { + var countStartColumn = columns.Count - 1; - list.Add((item, GetItemCounts(row, countStartColumn, typesToCount))); + list.Add((item, GetItemCounts(row, countStartColumn, typesToCount))); + } } } - - LogQueryTime("GetItemValues", commandText, now); } - } - if (query.EnableTotalRecordCount) - { - commandText = "select " - + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) - + GetFromText() - + GetJoinUserDataText(query) - + whereText; - - using (var statement = statements[statements.Count - 1]) + if (query.EnableTotalRecordCount) { - statement.TryBind("@SelectType", returnType); - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } + commandText = "select " + + string.Join(",", GetFinalColumnsToSelect(query, new[] { "count (distinct PresentationUniqueKey)" })) + + GetFromText() + + GetJoinUserDataText(query) + + whereText; - if (typeSubQuery != null) + using (var statement = statements[statements.Count - 1]) { - GetWhereClauses(typeSubQuery, null); - } - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - GetWhereClauses(innerQuery, statement); - GetWhereClauses(outerQuery, statement); + statement.TryBind("@SelectType", returnType); + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.InternalId); + } - result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); + if (typeSubQuery != null) + { + GetWhereClauses(typeSubQuery, null); + } + BindSimilarParams(query, statement); + BindSearchParams(query, statement); + GetWhereClauses(innerQuery, statement); + GetWhereClauses(outerQuery, statement); - LogQueryTime("GetItemValues", commandText, now); + result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); + } } - } - - if (result.TotalRecordCount == 0) - { - result.TotalRecordCount = list.Count; - } - result.Items = list.ToArray(); + }, + ReadTransactionMode); + } - return result; + LogQueryTime("GetItemValues", commandText, now); - }, ReadTransactionMode); + if (result.TotalRecordCount == 0) + { + result.TotalRecordCount = list.Count; } + + result.Items = list; + + return result; } private ItemCounts GetItemCounts(IReadOnlyList<IResultSetValue> reader, int countStartColumn, string[] typesToCount) @@ -5579,6 +5570,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type { counts.TrailerCount = value; } + counts.ItemCount += value; } @@ -5706,8 +5698,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type { var itemIdBlob = itemId.ToGuidBlob(); - // First delete chapters - db.Execute("delete from People where ItemId=@ItemId", itemIdBlob); + // First delete chapters + db.Execute("delete from People where ItemId=@ItemId", itemIdBlob); InsertPeople(itemIdBlob, people, db); @@ -5732,7 +5724,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type { if (isSubsequentRow) { - insertText.Append(","); + insertText.Append(','); } insertText.AppendFormat("(@ItemId, @Name{0}, @Role{0}, @PersonType{0}, @SortOrder{0}, @ListOrder{0})", i.ToString(CultureInfo.InvariantCulture)); @@ -5867,8 +5859,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type { var itemIdBlob = id.ToGuidBlob(); - // First delete chapters - db.Execute("delete from mediastreams where ItemId=@ItemId", itemIdBlob); + // First delete chapters + db.Execute("delete from mediastreams where ItemId=@ItemId", itemIdBlob); InsertMediaStreams(itemIdBlob, streams, db); diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index 4035bb99d..9d4855bcf 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.Data var userDatasTableExists = TableExists(connection, "UserDatas"); var userDataTableExists = TableExists(connection, "userdata"); - var users = userDatasTableExists ? null : userManager.Users.ToArray(); + var users = userDatasTableExists ? null : userManager.Users; connection.RunInTransaction(db => { @@ -84,7 +84,7 @@ namespace Emby.Server.Implementations.Data } } - private void ImportUserIds(IDatabaseConnection db, User[] users) + private void ImportUserIds(IDatabaseConnection db, IEnumerable<User> users) { var userIdsWithUserData = GetAllUserIdsWithUserData(db); diff --git a/Emby.Server.Implementations/Data/SqliteUserRepository.cs b/Emby.Server.Implementations/Data/SqliteUserRepository.cs index de2354eef..80fe278f8 100644 --- a/Emby.Server.Implementations/Data/SqliteUserRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserRepository.cs @@ -1,10 +1,11 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text.Json; +using MediaBrowser.Common.Json; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; using SQLitePCL.pretty; @@ -15,15 +16,14 @@ namespace Emby.Server.Implementations.Data /// </summary> public class SqliteUserRepository : BaseSqliteRepository, IUserRepository { - private readonly IJsonSerializer _jsonSerializer; + private readonly JsonSerializerOptions _jsonOptions; public SqliteUserRepository( - ILoggerFactory loggerFactory, - IServerApplicationPaths appPaths, - IJsonSerializer jsonSerializer) - : base(loggerFactory.CreateLogger(nameof(SqliteUserRepository))) + ILogger<SqliteUserRepository> logger, + IServerApplicationPaths appPaths) + : base(logger) { - _jsonSerializer = jsonSerializer; + _jsonOptions = JsonDefaults.GetOptions();; DbFilePath = Path.Combine(appPaths.DataPath, "users.db"); } @@ -35,9 +35,8 @@ namespace Emby.Server.Implementations.Data public string Name => "SQLite"; /// <summary> - /// Opens the connection to the database + /// Opens the connection to the database. /// </summary> - /// <returns>Task.</returns> public void Initialize() { using (var connection = GetConnection()) @@ -85,7 +84,7 @@ namespace Emby.Server.Implementations.Data } user.Password = null; - var serialized = _jsonSerializer.SerializeToBytes(user); + var serialized = JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions); connection.RunInTransaction(db => { @@ -109,7 +108,7 @@ namespace Emby.Server.Implementations.Data throw new ArgumentNullException(nameof(user)); } - var serialized = _jsonSerializer.SerializeToBytes(user); + var serialized = JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions); using (var connection = GetConnection()) { @@ -143,7 +142,7 @@ namespace Emby.Server.Implementations.Data throw new ArgumentNullException(nameof(user)); } - var serialized = _jsonSerializer.SerializeToBytes(user); + var serialized = JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions); using (var connection = GetConnection()) { @@ -180,14 +179,10 @@ namespace Emby.Server.Implementations.Data var id = row[0].ToInt64(); var guid = row[1].ReadGuidFromBlob(); - using (var stream = new MemoryStream(row[2].ToBlob())) - { - stream.Position = 0; - var user = _jsonSerializer.DeserializeFromStream<User>(stream); - user.InternalId = id; - user.Id = guid; - return user; - } + var user = JsonSerializer.Deserialize<User>(row[2].ToBlob(), _jsonOptions); + user.InternalId = id; + user.Id = guid; + return user; } /// <summary> diff --git a/Emby.Server.Implementations/Devices/DeviceId.cs b/Emby.Server.Implementations/Devices/DeviceId.cs index 495c3436a..7344dc72f 100644 --- a/Emby.Server.Implementations/Devices/DeviceId.cs +++ b/Emby.Server.Implementations/Devices/DeviceId.cs @@ -1,8 +1,8 @@ using System; +using System.Globalization; using System.IO; using System.Text; using MediaBrowser.Common.Configuration; -using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Devices @@ -67,7 +67,7 @@ namespace Emby.Server.Implementations.Devices private static string GetNewId() { - return Guid.NewGuid().ToString("N"); + return Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); } private string GetDeviceId() diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs index 7d6529a67..d1704b373 100644 --- a/Emby.Server.Implementations/Devices/DeviceManager.cs +++ b/Emby.Server.Implementations/Devices/DeviceManager.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Entities; @@ -195,7 +195,7 @@ namespace Emby.Server.Implementations.Devices private string GetDevicePath(string id) { - return Path.Combine(GetDevicesPath(), id.GetMD5().ToString("N")); + return Path.Combine(GetDevicesPath(), id.GetMD5().ToString("N", CultureInfo.InvariantCulture)); } public ContentUploadHistory GetCameraUploadHistory(string deviceId) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 2f1b60be9..6c0e32e05 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -79,27 +80,25 @@ namespace Emby.Server.Implementations.Dto return GetBaseItemDto(item, options, user, owner); } - public BaseItemDto[] GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null) - => GetBaseItemDtos(items, items.Count, options, user, owner); - - public BaseItemDto[] GetBaseItemDtos(IEnumerable<BaseItem> items, int itemCount, DtoOptions options, User user = null, BaseItem owner = null) + /// <inheritdoc /> + public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null) { - var returnItems = new BaseItemDto[itemCount]; - var programTuples = new List<Tuple<BaseItem, BaseItemDto>>(); - var channelTuples = new List<Tuple<BaseItemDto, LiveTvChannel>>(); + var returnItems = new BaseItemDto[items.Count]; + var programTuples = new List<(BaseItem, BaseItemDto)>(); + var channelTuples = new List<(BaseItemDto, LiveTvChannel)>(); - var index = 0; - foreach (var item in items) + for (int index = 0; index < items.Count; index++) { + var item = items[index]; var dto = GetBaseItemDtoInternal(item, options, user, owner); if (item is LiveTvChannel tvChannel) { - channelTuples.Add(new Tuple<BaseItemDto, LiveTvChannel>(dto, tvChannel)); + channelTuples.Add((dto, tvChannel)); } else if (item is LiveTvProgram) { - programTuples.Add(new Tuple<BaseItem, BaseItemDto>(item, dto)); + programTuples.Add((item, dto)); } if (item is IItemByName byName) @@ -120,7 +119,6 @@ namespace Emby.Server.Implementations.Dto } returnItems[index] = dto; - index++; } if (programTuples.Count > 0) @@ -139,33 +137,32 @@ namespace Emby.Server.Implementations.Dto public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null) { var dto = GetBaseItemDtoInternal(item, options, user, owner); - var tvChannel = item as LiveTvChannel; - if (tvChannel != null) + if (item is LiveTvChannel tvChannel) { - var list = new List<Tuple<BaseItemDto, LiveTvChannel>> { new Tuple<BaseItemDto, LiveTvChannel>(dto, tvChannel) }; + var list = new List<(BaseItemDto, LiveTvChannel)>(1) { (dto, tvChannel) }; _livetvManager().AddChannelInfo(list, options, user); } else if (item is LiveTvProgram) { - var list = new List<Tuple<BaseItem, BaseItemDto>> { new Tuple<BaseItem, BaseItemDto>(item, dto) }; + var list = new List<(BaseItem, BaseItemDto)>(1) { (item, dto) }; var task = _livetvManager().AddInfoToProgramDto(list, options.Fields, user); Task.WaitAll(task); } - var byName = item as IItemByName; - - if (byName != null) + if (item is IItemByName itemByName + && options.ContainsField(ItemFields.ItemCounts)) { - if (options.ContainsField(ItemFields.ItemCounts)) - { - SetItemByNameInfo(item, dto, GetTaggedItems(byName, user, new DtoOptions(false) - { - EnableImages = false - - }), user); - } - - return dto; + SetItemByNameInfo( + item, + dto, + GetTaggedItems( + itemByName, + user, + new DtoOptions(false) + { + EnableImages = false + }), + user); } return dto; @@ -173,12 +170,12 @@ namespace Emby.Server.Implementations.Dto private static IList<BaseItem> GetTaggedItems(IItemByName byName, User user, DtoOptions options) { - return byName.GetTaggedItems(new InternalItemsQuery(user) - { - Recursive = true, - DtoOptions = options - - }); + return byName.GetTaggedItems( + new InternalItemsQuery(user) + { + Recursive = true, + DtoOptions = options + }); } private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null) @@ -213,7 +210,7 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.DisplayPreferencesId)) { - dto.DisplayPreferencesId = item.DisplayPreferencesId.ToString("N"); + dto.DisplayPreferencesId = item.DisplayPreferencesId.ToString("N", CultureInfo.InvariantCulture); } if (user != null) @@ -221,15 +218,12 @@ namespace Emby.Server.Implementations.Dto AttachUserSpecificInfo(dto, item, user, options); } - var hasMediaSources = item as IHasMediaSources; - if (hasMediaSources != null) + if (item is IHasMediaSources + && options.ContainsField(ItemFields.MediaSources)) { - if (options.ContainsField(ItemFields.MediaSources)) - { - dto.MediaSources = _mediaSourceManager().GetStaticMediaSources(item, true, user).ToArray(); + dto.MediaSources = _mediaSourceManager().GetStaticMediaSources(item, true, user).ToArray(); - NormalizeMediaSourceContainers(dto); - } + NormalizeMediaSourceContainers(dto); } if (options.ContainsField(ItemFields.Studios)) @@ -444,7 +438,7 @@ namespace Emby.Server.Implementations.Dto /// <exception cref="ArgumentNullException">item</exception> public string GetDtoId(BaseItem item) { - return item.Id.ToString("N"); + return item.Id.ToString("N", CultureInfo.InvariantCulture); } private static void SetBookProperties(BaseItemDto dto, Book item) @@ -608,7 +602,7 @@ namespace Emby.Server.Implementations.Dto if (dictionary.TryGetValue(person.Name, out Person entity)) { baseItemPerson.PrimaryImageTag = GetImageCacheTag(entity, ImageType.Primary); - baseItemPerson.Id = entity.Id.ToString("N"); + baseItemPerson.Id = entity.Id.ToString("N", CultureInfo.InvariantCulture); list.Add(baseItemPerson); } } @@ -768,14 +762,12 @@ namespace Emby.Server.Implementations.Dto dto.CriticRating = item.CriticRating; - var hasDisplayOrder = item as IHasDisplayOrder; - if (hasDisplayOrder != null) + if (item is IHasDisplayOrder hasDisplayOrder) { dto.DisplayOrder = hasDisplayOrder.DisplayOrder; } - var hasCollectionType = item as IHasCollectionType; - if (hasCollectionType != null) + if (item is IHasCollectionType hasCollectionType) { dto.CollectionType = hasCollectionType.CollectionType; } @@ -885,15 +877,14 @@ namespace Emby.Server.Implementations.Dto //} } - var hasArtist = item as IHasArtist; - if (hasArtist != null) + if (item is IHasArtist hasArtist) { dto.Artists = hasArtist.Artists; //var artistItems = _libraryManager.GetArtists(new InternalItemsQuery //{ // EnableTotalRecordCount = false, - // ItemIds = new[] { item.Id.ToString("N") } + // ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) } //}); //dto.ArtistItems = artistItems.Items @@ -903,7 +894,7 @@ namespace Emby.Server.Implementations.Dto // return new NameIdPair // { // Name = artist.Name, - // Id = artist.Id.ToString("N") + // Id = artist.Id.ToString("N", CultureInfo.InvariantCulture) // }; // }) // .ToList(); @@ -946,7 +937,7 @@ namespace Emby.Server.Implementations.Dto //var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery //{ // EnableTotalRecordCount = false, - // ItemIds = new[] { item.Id.ToString("N") } + // ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) } //}); //dto.AlbumArtists = artistItems.Items @@ -956,7 +947,7 @@ namespace Emby.Server.Implementations.Dto // return new NameIdPair // { // Name = artist.Name, - // Id = artist.Id.ToString("N") + // Id = artist.Id.ToString("N", CultureInfo.InvariantCulture) // }; // }) // .ToList(); @@ -1044,7 +1035,7 @@ namespace Emby.Server.Implementations.Dto } else { - string id = item.Id.ToString("N"); + string id = item.Id.ToString("N", CultureInfo.InvariantCulture); mediaStreams = dto.MediaSources.Where(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)) .SelectMany(i => i.MediaStreams) .ToArray(); @@ -1073,17 +1064,24 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.LocalTrailerCount)) { + int trailerCount = 0; if (allExtras == null) { allExtras = item.GetExtras().ToArray(); } - dto.LocalTrailerCount = allExtras.Count(i => i.ExtraType.HasValue && i.ExtraType.Value == ExtraType.Trailer); + trailerCount += allExtras.Count(i => i.ExtraType.HasValue && i.ExtraType.Value == ExtraType.Trailer); + + if (item is IHasTrailers hasTrailers) + { + trailerCount += hasTrailers.GetTrailerCount(); + } + + dto.LocalTrailerCount = trailerCount; } // Add EpisodeInfo - var episode = item as Episode; - if (episode != null) + if (item is Episode episode) { dto.IndexNumberEnd = episode.IndexNumberEnd; dto.SeriesName = episode.SeriesName; @@ -1101,6 +1099,8 @@ namespace Emby.Server.Implementations.Dto Series episodeSeries = null; + // this block will add the series poster for episodes without a poster + // TODO maybe remove the if statement entirely //if (options.ContainsField(ItemFields.SeriesPrimaryImage)) { episodeSeries = episodeSeries ?? episode.Series; @@ -1121,8 +1121,7 @@ namespace Emby.Server.Implementations.Dto } // Add SeriesInfo - var series = item as Series; - if (series != null) + if (item is Series series) { dto.AirDays = series.AirDays; dto.AirTime = series.AirTime; @@ -1130,8 +1129,7 @@ namespace Emby.Server.Implementations.Dto } // Add SeasonInfo - var season = item as Season; - if (season != null) + if (item is Season season) { dto.SeriesName = season.SeriesName; dto.SeriesId = season.SeriesId; @@ -1147,6 +1145,8 @@ namespace Emby.Server.Implementations.Dto } } + // this block will add the series poster for seasons without a poster + // TODO maybe remove the if statement entirely //if (options.ContainsField(ItemFields.SeriesPrimaryImage)) { series = series ?? season.Series; @@ -1157,14 +1157,12 @@ namespace Emby.Server.Implementations.Dto } } - var musicVideo = item as MusicVideo; - if (musicVideo != null) + if (item is MusicVideo musicVideo) { SetMusicVideoProperties(dto, musicVideo); } - var book = item as Book; - if (book != null) + if (item is Book book) { SetBookProperties(dto, book); } @@ -1204,8 +1202,7 @@ namespace Emby.Server.Implementations.Dto } } - var photo = item as Photo; - if (photo != null) + if (item is Photo photo) { SetPhotoProperties(dto, photo); } @@ -1224,8 +1221,7 @@ namespace Emby.Server.Implementations.Dto private BaseItem GetImageDisplayParent(BaseItem currentItem, BaseItem originalItem) { - var musicAlbum = currentItem as MusicAlbum; - if (musicAlbum != null) + if (currentItem is MusicAlbum musicAlbum) { var artist = musicAlbum.GetMusicArtist(new DtoOptions(false)); if (artist != null) @@ -1363,7 +1359,7 @@ namespace Emby.Server.Implementations.Dto return null; } - var supportedEnhancers = _imageProcessor.GetSupportedEnhancers(item, ImageType.Primary); + var supportedEnhancers = _imageProcessor.GetSupportedEnhancers(item, ImageType.Primary).ToArray(); ImageDimensions size; diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index abbaef26b..ea4444268 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -21,7 +21,7 @@ <ItemGroup> <PackageReference Include="IPNetwork2" Version="2.4.0.126" /> - <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.0" /> + <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" /> <PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" /> @@ -32,9 +32,9 @@ <PackageReference Include="Microsoft.Extensions.Logging" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.2.0" /> - <PackageReference Include="ServiceStack.Text.Core" Version="5.5.0" /> - <PackageReference Include="sharpcompress" Version="0.23.0" /> - <PackageReference Include="SQLitePCL.pretty.netstandard" Version="1.0.0" /> + <PackageReference Include="ServiceStack.Text.Core" Version="5.6.0" /> + <PackageReference Include="sharpcompress" Version="0.24.0" /> + <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.0.1" /> </ItemGroup> <ItemGroup> @@ -42,17 +42,19 @@ </ItemGroup> <PropertyGroup> - <TargetFramework>netstandard2.0</TargetFramework> + <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Release' "> - <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <PropertyGroup> + <!-- We need at least C# 7.3 to compare tuples--> + <LangVersion>latest</LangVersion> </PropertyGroup> <!-- Code analysers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.3" /> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.4" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" /> </ItemGroup> diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 8369f4f59..9c0db2cf5 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -100,7 +100,7 @@ namespace Emby.Server.Implementations.EntryPoints _lastProgressMessageTimes[item.Id] = DateTime.UtcNow; var dict = new Dictionary<string, string>(); - dict["ItemId"] = item.Id.ToString("N"); + dict["ItemId"] = item.Id.ToString("N", CultureInfo.InvariantCulture); dict["Progress"] = progress.ToString(CultureInfo.InvariantCulture); try @@ -116,7 +116,7 @@ namespace Emby.Server.Implementations.EntryPoints foreach (var collectionFolder in collectionFolders) { var collectionFolderDict = new Dictionary<string, string>(); - collectionFolderDict["ItemId"] = collectionFolder.Id.ToString("N"); + collectionFolderDict["ItemId"] = collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture); collectionFolderDict["Progress"] = (collectionFolder.GetRefreshProgress() ?? 0).ToString(CultureInfo.InvariantCulture); try @@ -378,15 +378,15 @@ namespace Emby.Server.Implementations.EntryPoints return new LibraryUpdateInfo { - ItemsAdded = itemsAdded.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N")).Distinct().ToArray(), + ItemsAdded = itemsAdded.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(), - ItemsUpdated = itemsUpdated.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N")).Distinct().ToArray(), + ItemsUpdated = itemsUpdated.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(), - ItemsRemoved = itemsRemoved.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user, true)).Select(i => i.Id.ToString("N")).Distinct().ToArray(), + ItemsRemoved = itemsRemoved.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user, true)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(), - FoldersAddedTo = foldersAddedTo.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N")).Distinct().ToArray(), + FoldersAddedTo = foldersAddedTo.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(), - FoldersRemovedFrom = foldersRemovedFrom.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N")).Distinct().ToArray(), + FoldersRemovedFrom = foldersRemovedFrom.SelectMany(i => TranslatePhysicalItemToUserLibrary(i, user)).Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)).Distinct().ToArray(), CollectionFolders = GetTopParentIds(newAndRemoved, allUserRootChildren).ToArray() }; @@ -422,7 +422,7 @@ namespace Emby.Server.Implementations.EntryPoints var collectionFolders = _libraryManager.GetCollectionFolders(item, allUserRootChildren); foreach (var folder in allUserRootChildren) { - list.Add(folder.Id.ToString("N")); + list.Add(folder.Id.ToString("N", CultureInfo.InvariantCulture)); } } diff --git a/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs b/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs index b7565adec..b2328121e 100644 --- a/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs +++ b/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs @@ -50,9 +50,7 @@ namespace Emby.Server.Implementations.EntryPoints public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress) { - var users = _userManager.Users.ToList(); - - foreach (var user in users) + foreach (var user in _userManager.Users) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs b/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs index 091dd6a45..141e72958 100644 --- a/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Plugins; @@ -134,7 +135,7 @@ namespace Emby.Server.Implementations.EntryPoints /// <param name="e">The e.</param> void userManager_UserDeleted(object sender, GenericEventArgs<User> e) { - SendMessageToUserSession(e.Argument, "UserDeleted", e.Argument.Id.ToString("N")); + SendMessageToUserSession(e.Argument, "UserDeleted", e.Argument.Id.ToString("N", CultureInfo.InvariantCulture)); } void _userManager_UserPolicyUpdated(object sender, GenericEventArgs<User> e) diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs index a5badacee..bae3422ff 100644 --- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -8,7 +9,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; @@ -125,12 +125,12 @@ namespace Emby.Server.Implementations.EntryPoints .Select(i => { var dto = _userDataManager.GetUserDataDto(i, user); - dto.ItemId = i.Id.ToString("N"); + dto.ItemId = i.Id.ToString("N", CultureInfo.InvariantCulture); return dto; }) .ToArray(); - var userIdString = userId.ToString("N"); + var userIdString = userId.ToString("N", CultureInfo.InvariantCulture); return new UserDataChangeInfo { diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs index 331b5e29d..0e6083773 100644 --- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs +++ b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs @@ -1,12 +1,10 @@ using System; using System.Collections.Concurrent; +using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; @@ -19,7 +17,7 @@ using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.HttpClientManager { /// <summary> - /// Class HttpClientManager + /// Class HttpClientManager. /// </summary> public class HttpClientManager : IHttpClient { @@ -44,19 +42,9 @@ namespace Emby.Server.Implementations.HttpClientManager IFileSystem fileSystem, Func<string> defaultUserAgentFn) { - if (appPaths == null) - { - throw new ArgumentNullException(nameof(appPaths)); - } - - if (logger == null) - { - throw new ArgumentNullException(nameof(logger)); - } - - _logger = logger; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _fileSystem = fileSystem; - _appPaths = appPaths; + _appPaths = appPaths ?? throw new ArgumentNullException(nameof(appPaths)); _defaultUserAgentFn = defaultUserAgentFn; } @@ -95,7 +83,16 @@ namespace Emby.Server.Implementations.HttpClientManager var request = new HttpRequestMessage(method, url); - AddRequestHeaders(request, options); + foreach (var header in options.RequestHeaders) + { + request.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + + if (options.EnableDefaultUserAgent + && !request.Headers.TryGetValues(HeaderNames.UserAgent, out _)) + { + request.Headers.Add(HeaderNames.UserAgent, _defaultUserAgentFn()); + } switch (options.DecompressionMethod) { @@ -117,7 +114,7 @@ namespace Emby.Server.Implementations.HttpClientManager request.Headers.Add(HeaderNames.Connection, "Keep-Alive"); } - //request.Headers.Add(HeaderNames.CacheControl, "no-cache"); + // request.Headers.Add(HeaderNames.CacheControl, "no-cache"); /* if (!string.IsNullOrWhiteSpace(userInfo)) @@ -133,26 +130,6 @@ namespace Emby.Server.Implementations.HttpClientManager return request; } - private void AddRequestHeaders(HttpRequestMessage request, HttpRequestOptions options) - { - var hasUserAgent = false; - - foreach (var header in options.RequestHeaders) - { - if (string.Equals(header.Key, HeaderNames.UserAgent, StringComparison.OrdinalIgnoreCase)) - { - hasUserAgent = true; - } - - request.Headers.Add(header.Key, header.Value); - } - - if (!hasUserAgent && options.EnableDefaultUserAgent) - { - request.Headers.Add(HeaderNames.UserAgent, _defaultUserAgentFn()); - } - } - /// <summary> /// Gets the response internal. /// </summary> @@ -195,7 +172,7 @@ namespace Emby.Server.Implementations.HttpClientManager } var url = options.Url; - var urlHash = url.ToLowerInvariant().GetMD5().ToString("N"); + var urlHash = url.ToUpperInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture); var responseCachePath = Path.Combine(_appPaths.CachePath, "httpclient", urlHash); @@ -238,7 +215,13 @@ namespace Emby.Server.Implementations.HttpClientManager { Directory.CreateDirectory(Path.GetDirectoryName(responseCachePath)); - using (var fileStream = _fileSystem.GetFileStream(responseCachePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.None, true)) + using (var fileStream = new FileStream( + responseCachePath, + FileMode.Create, + FileAccess.Write, + FileShare.None, + StreamDefaults.DefaultFileStreamBufferSize, + true)) { await response.Content.CopyToAsync(fileStream).ConfigureAwait(false); @@ -277,16 +260,11 @@ namespace Emby.Server.Implementations.HttpClientManager } } - if (options.LogRequest) - { - _logger.LogDebug("HttpClientManager {0}: {1}", httpMethod.ToString(), options.Url); - } - options.CancellationToken.ThrowIfCancellationRequested(); var response = await client.SendAsync( httpWebRequest, - options.BufferContent ? HttpCompletionOption.ResponseContentRead : HttpCompletionOption.ResponseHeadersRead, + options.BufferContent || options.CacheMode == CacheMode.Unconditional ? HttpCompletionOption.ResponseContentRead : HttpCompletionOption.ResponseHeadersRead, options.CancellationToken).ConfigureAwait(false); await EnsureSuccessStatusCode(response, options).ConfigureAwait(false); @@ -307,138 +285,6 @@ namespace Emby.Server.Implementations.HttpClientManager public Task<HttpResponseInfo> Post(HttpRequestOptions options) => SendAsync(options, HttpMethod.Post); - /// <summary> - /// Downloads the contents of a given url into a temporary location - /// </summary> - /// <param name="options">The options.</param> - /// <returns>Task{System.String}.</returns> - public async Task<string> GetTempFile(HttpRequestOptions options) - { - var response = await GetTempFileResponse(options).ConfigureAwait(false); - return response.TempFilePath; - } - - public async Task<HttpResponseInfo> GetTempFileResponse(HttpRequestOptions options) - { - ValidateParams(options); - - Directory.CreateDirectory(_appPaths.TempDirectory); - - var tempFile = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".tmp"); - - if (options.Progress == null) - { - throw new ArgumentException("Options did not have a Progress value.", nameof(options)); - } - - options.CancellationToken.ThrowIfCancellationRequested(); - - var httpWebRequest = GetRequestMessage(options, HttpMethod.Get); - - options.Progress.Report(0); - - if (options.LogRequest) - { - _logger.LogDebug("HttpClientManager.GetTempFileResponse url: {0}", options.Url); - } - - var client = GetHttpClient(options.Url); - - try - { - options.CancellationToken.ThrowIfCancellationRequested(); - - using (var response = (await client.SendAsync(httpWebRequest, options.CancellationToken).ConfigureAwait(false))) - { - await EnsureSuccessStatusCode(response, options).ConfigureAwait(false); - - options.CancellationToken.ThrowIfCancellationRequested(); - - using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) - using (var fs = _fileSystem.GetFileStream(tempFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) - { - await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false); - } - - options.Progress.Report(100); - - var responseInfo = new HttpResponseInfo(response.Headers, response.Content.Headers) - { - TempFilePath = tempFile, - StatusCode = response.StatusCode, - ContentType = response.Content.Headers.ContentType?.MediaType, - ContentLength = response.Content.Headers.ContentLength - }; - - return responseInfo; - } - } - catch (Exception ex) - { - if (File.Exists(tempFile)) - { - File.Delete(tempFile); - } - - throw GetException(ex, options); - } - } - - private Exception GetException(Exception ex, HttpRequestOptions options) - { - if (ex is HttpException) - { - return ex; - } - - var webException = ex as WebException - ?? ex.InnerException as WebException; - - if (webException != null) - { - if (options.LogErrors) - { - _logger.LogError(webException, "Error {Status} getting response from {Url}", webException.Status, options.Url); - } - - var exception = new HttpException(webException.Message, webException); - - using (var response = webException.Response as HttpWebResponse) - { - if (response != null) - { - exception.StatusCode = response.StatusCode; - } - } - - if (!exception.StatusCode.HasValue) - { - if (webException.Status == WebExceptionStatus.NameResolutionFailure || - webException.Status == WebExceptionStatus.ConnectFailure) - { - exception.IsTimedOut = true; - } - } - - return exception; - } - - var operationCanceledException = ex as OperationCanceledException - ?? ex.InnerException as OperationCanceledException; - - if (operationCanceledException != null) - { - return GetCancellationException(options, options.CancellationToken, operationCanceledException); - } - - if (options.LogErrors) - { - _logger.LogError(ex, "Error getting response from {Url}", options.Url); - } - - return ex; - } - private void ValidateParams(HttpRequestOptions options) { if (string.IsNullOrEmpty(options.Url)) @@ -470,35 +316,6 @@ namespace Emby.Server.Implementations.HttpClientManager return url; } - /// <summary> - /// Throws the cancellation exception. - /// </summary> - /// <param name="options">The options.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <param name="exception">The exception.</param> - /// <returns>Exception.</returns> - private Exception GetCancellationException(HttpRequestOptions options, CancellationToken cancellationToken, OperationCanceledException exception) - { - // If the HttpClient's timeout is reached, it will cancel the Task internally - if (!cancellationToken.IsCancellationRequested) - { - var msg = string.Format("Connection to {0} timed out", options.Url); - - if (options.LogErrors) - { - _logger.LogError(msg); - } - - // Throw an HttpException so that the caller doesn't think it was cancelled by user code - return new HttpException(msg, exception) - { - IsTimedOut = true - }; - } - - return exception; - } - private async Task EnsureSuccessStatusCode(HttpResponseMessage response, HttpRequestOptions options) { if (response.IsSuccessStatusCode) @@ -506,8 +323,11 @@ namespace Emby.Server.Implementations.HttpClientManager return; } - var msg = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - _logger.LogError("HTTP request failed with message: {Message}", msg); + if (options.LogErrorResponseBody) + { + var msg = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + _logger.LogError("HTTP request failed with message: {Message}", msg); + } throw new HttpException(response.ReasonPhrase) { diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs index ec41cc0a9..2c7e81361 100644 --- a/Emby.Server.Implementations/HttpServer/FileWriter.cs +++ b/Emby.Server.Implementations/HttpServer/FileWriter.cs @@ -1,50 +1,43 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Net; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; -using Emby.Server.Implementations.IO; using MediaBrowser.Model.IO; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.HttpServer { public class FileWriter : IHttpResult { - private readonly IStreamHelper _streamHelper; - private ILogger Logger { get; set; } - private readonly IFileSystem _fileSystem; - - private string RangeHeader { get; set; } - private bool IsHeadRequest { get; set; } - - private long RangeStart { get; set; } - private long RangeEnd { get; set; } - private long RangeLength { get; set; } - public long TotalContentLength { get; set; } + private static readonly CultureInfo UsCulture = CultureInfo.ReadOnly(new CultureInfo("en-US")); - public Action OnComplete { get; set; } - public Action OnError { get; set; } - private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - public List<Cookie> Cookies { get; private set; } + private static readonly string[] _skipLogExtensions = { + ".js", + ".html", + ".css" + }; - public FileShareMode FileShare { get; set; } + private readonly IStreamHelper _streamHelper; + private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; /// <summary> /// The _options /// </summary> private readonly IDictionary<string, string> _options = new Dictionary<string, string>(); + /// <summary> - /// Gets the options. + /// The _requested ranges /// </summary> - /// <value>The options.</value> - public IDictionary<string, string> Headers => _options; - - public string Path { get; set; } + private List<KeyValuePair<long, long?>> _requestedRanges; public FileWriter(string path, string contentType, string rangeHeader, ILogger logger, IFileSystem fileSystem, IStreamHelper streamHelper) { @@ -57,7 +50,7 @@ namespace Emby.Server.Implementations.HttpServer _fileSystem = fileSystem; Path = path; - Logger = logger; + _logger = logger; RangeHeader = rangeHeader; Headers[HeaderNames.ContentType] = contentType; @@ -80,39 +73,34 @@ namespace Emby.Server.Implementations.HttpServer Cookies = new List<Cookie>(); } - /// <summary> - /// Sets the range values. - /// </summary> - private void SetRangeValues() - { - var requestedRange = RequestedRanges[0]; + private string RangeHeader { get; set; } - // If the requested range is "0-", we can optimize by just doing a stream copy - if (!requestedRange.Value.HasValue) - { - RangeEnd = TotalContentLength - 1; - } - else - { - RangeEnd = requestedRange.Value.Value; - } + private bool IsHeadRequest { get; set; } - RangeStart = requestedRange.Key; - RangeLength = 1 + RangeEnd - RangeStart; + private long RangeStart { get; set; } - // Content-Length is the length of what we're serving, not the original content - var lengthString = RangeLength.ToString(CultureInfo.InvariantCulture); - Headers[HeaderNames.ContentLength] = lengthString; - var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; - Headers[HeaderNames.ContentRange] = rangeString; + private long RangeEnd { get; set; } - Logger.LogDebug("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString); - } + private long RangeLength { get; set; } + + public long TotalContentLength { get; set; } + + public Action OnComplete { get; set; } + + public Action OnError { get; set; } + + public List<Cookie> Cookies { get; private set; } + + public FileShareMode FileShare { get; set; } /// <summary> - /// The _requested ranges + /// Gets the options. /// </summary> - private List<KeyValuePair<long, long?>> _requestedRanges; + /// <value>The options.</value> + public IDictionary<string, string> Headers => _options; + + public string Path { get; set; } + /// <summary> /// Gets the requested ranges. /// </summary> @@ -139,6 +127,7 @@ namespace Emby.Server.Implementations.HttpServer { start = long.Parse(vals[0], UsCulture); } + if (!string.IsNullOrEmpty(vals[1])) { end = long.Parse(vals[1], UsCulture); @@ -152,13 +141,50 @@ namespace Emby.Server.Implementations.HttpServer } } - private static readonly string[] SkipLogExtensions = { - ".js", - ".html", - ".css" - }; + public string ContentType { get; set; } + + public IRequest RequestContext { get; set; } + + public object Response { get; set; } + + public int Status { get; set; } + + public HttpStatusCode StatusCode + { + get => (HttpStatusCode)Status; + set => Status = (int)value; + } + + /// <summary> + /// Sets the range values. + /// </summary> + private void SetRangeValues() + { + var requestedRange = RequestedRanges[0]; + + // If the requested range is "0-", we can optimize by just doing a stream copy + if (!requestedRange.Value.HasValue) + { + RangeEnd = TotalContentLength - 1; + } + else + { + RangeEnd = requestedRange.Value.Value; + } + + RangeStart = requestedRange.Key; + RangeLength = 1 + RangeEnd - RangeStart; + + // Content-Length is the length of what we're serving, not the original content + var lengthString = RangeLength.ToString(CultureInfo.InvariantCulture); + Headers[HeaderNames.ContentLength] = lengthString; + var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; + Headers[HeaderNames.ContentRange] = rangeString; - public async Task WriteToAsync(IResponse response, CancellationToken cancellationToken) + _logger.LogDebug("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString); + } + + public async Task WriteToAsync(HttpResponse response, CancellationToken cancellationToken) { try { @@ -176,16 +202,16 @@ namespace Emby.Server.Implementations.HttpServer { var extension = System.IO.Path.GetExtension(path); - if (extension == null || !SkipLogExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) + if (extension == null || !_skipLogExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) { - Logger.LogDebug("Transmit file {0}", path); + _logger.LogDebug("Transmit file {0}", path); } offset = 0; count = 0; } - await response.TransmitFile(path, offset, count, FileShare, _fileSystem, _streamHelper, cancellationToken).ConfigureAwait(false); + await TransmitFile(response.Body, path, offset, count, FileShare, cancellationToken).ConfigureAwait(false); } finally { @@ -193,18 +219,32 @@ namespace Emby.Server.Implementations.HttpServer } } - public string ContentType { get; set; } - - public IRequest RequestContext { get; set; } + public async Task TransmitFile(Stream stream, string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) + { + var fileOpenOptions = FileOpenOptions.SequentialScan; - public object Response { get; set; } + // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + fileOpenOptions |= FileOpenOptions.Asynchronous; + } - public int Status { get; set; } + using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShareMode, fileOpenOptions)) + { + if (offset > 0) + { + fs.Position = offset; + } - public HttpStatusCode StatusCode - { - get => (HttpStatusCode)Status; - set => Status = (int)value; + if (count > 0) + { + await _streamHelper.CopyToAsync(fs, stream, count, cancellationToken).ConfigureAwait(false); + } + else + { + await fs.CopyToAsync(stream, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false); + } + } } } } diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index d8938964f..e4f98acb9 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -5,9 +5,9 @@ using System.IO; using System.Linq; using System.Net.Sockets; using System.Reflection; -using System.Text; using System.Threading; using System.Threading.Tasks; +using Emby.Server.Implementations.Configuration; using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Services; using MediaBrowser.Common.Extensions; @@ -30,11 +30,7 @@ namespace Emby.Server.Implementations.HttpServer { public class HttpListenerHost : IHttpServer, IDisposable { - private string DefaultRedirectPath { get; set; } - public string[] UrlPrefixes { get; private set; } - - public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected; - + private readonly ILogger _logger; private readonly IServerConfigurationManager _config; private readonly INetworkManager _networkManager; private readonly IServerApplicationHost _appHost; @@ -42,18 +38,16 @@ namespace Emby.Server.Implementations.HttpServer private readonly IXmlSerializer _xmlSerializer; private readonly IHttpListener _socketListener; private readonly Func<Type, Func<string, object>> _funcParseFn; - - public Action<IRequest, IResponse, object>[] ResponseFilters { get; set; } - + private readonly string _defaultRedirectPath; + private readonly string _baseUrlPrefix; private readonly Dictionary<Type, Type> ServiceOperationsMap = new Dictionary<Type, Type>(); - public static HttpListenerHost Instance { get; protected set; } - private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>(); private readonly List<IWebSocketConnection> _webSocketConnections = new List<IWebSocketConnection>(); + private bool _disposed = false; public HttpListenerHost( IServerApplicationHost applicationHost, - ILoggerFactory loggerFactory, + ILogger<HttpListenerHost> logger, IServerConfigurationManager config, IConfiguration configuration, INetworkManager networkManager, @@ -62,9 +56,10 @@ namespace Emby.Server.Implementations.HttpServer IHttpListener socketListener) { _appHost = applicationHost; - Logger = loggerFactory.CreateLogger("HttpServer"); + _logger = logger; _config = config; - DefaultRedirectPath = configuration["HttpListenerHost:DefaultRedirectPath"]; + _defaultRedirectPath = configuration["HttpListenerHost:DefaultRedirectPath"]; + _baseUrlPrefix = _config.Configuration.BaseUrl; _networkManager = networkManager; _jsonSerializer = jsonSerializer; _xmlSerializer = xmlSerializer; @@ -74,26 +69,48 @@ namespace Emby.Server.Implementations.HttpServer _funcParseFn = t => s => JsvReader.GetParseFn(t)(s); Instance = this; - ResponseFilters = Array.Empty<Action<IRequest, IResponse, object>>(); + ResponseFilters = Array.Empty<Action<IRequest, HttpResponse, object>>(); } + public Action<IRequest, HttpResponse, object>[] ResponseFilters { get; set; } + + public static HttpListenerHost Instance { get; protected set; } + + public string[] UrlPrefixes { get; private set; } + public string GlobalResponse { get; set; } - protected ILogger Logger { get; } + public ServiceController ServiceController { get; private set; } + + public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected; public object CreateInstance(Type type) { return _appHost.CreateInstance(type); } + private static string NormalizeUrlPath(string path) + { + if (path.StartsWith("/")) + { + // If the path begins with a leading slash, just return it as-is + return path; + } + else + { + // If the path does not begin with a leading slash, append one for consistency + return "/" + path; + } + } + /// <summary> /// Applies the request filters. Returns whether or not the request has been handled /// and no more processing should be done. /// </summary> /// <returns></returns> - public void ApplyRequestFilters(IRequest req, IResponse res, object requestDto) + public void ApplyRequestFilters(IRequest req, HttpResponse res, object requestDto) { - //Exec all RequestFilter attributes with Priority < 0 + // Exec all RequestFilter attributes with Priority < 0 var attributes = GetRequestFilterAttributes(requestDto.GetType()); int count = attributes.Count; @@ -104,7 +121,7 @@ namespace Emby.Server.Implementations.HttpServer attribute.RequestFilter(req, res, requestDto); } - //Exec remaining RequestFilter attributes with Priority >= 0 + // Exec remaining RequestFilter attributes with Priority >= 0 for (; i < count && attributes[i].Priority >= 0; i++) { var attribute = attributes[i]; @@ -145,7 +162,7 @@ namespace Emby.Server.Implementations.HttpServer return; } - var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, Logger) + var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger) { OnReceive = ProcessWebSocketMessageReceived, Url = e.Url, @@ -207,7 +224,7 @@ namespace Emby.Server.Implementations.HttpServer } } - private async Task ErrorHandler(Exception ex, IRequest httpReq, bool logExceptionStackTrace, bool logExceptionMessage) + private async Task ErrorHandler(Exception ex, IRequest httpReq, bool logExceptionStackTrace) { try { @@ -215,16 +232,16 @@ namespace Emby.Server.Implementations.HttpServer if (logExceptionStackTrace) { - Logger.LogError(ex, "Error processing request"); + _logger.LogError(ex, "Error processing request"); } - else if (logExceptionMessage) + else { - Logger.LogError(ex.Message); + _logger.LogError("Error processing request: {Message}", ex.Message); } var httpRes = httpReq.Response; - if (httpRes.OriginalResponse.HasStarted) + if (httpRes.HasStarted) { return; } @@ -232,12 +249,14 @@ namespace Emby.Server.Implementations.HttpServer var statusCode = GetStatusCode(ex); httpRes.StatusCode = statusCode; - httpRes.ContentType = "text/html"; - await Write(httpRes, NormalizeExceptionMessage(ex.Message)).ConfigureAwait(false); + var errContent = NormalizeExceptionMessage(ex.Message); + httpRes.ContentType = "text/plain"; + httpRes.ContentLength = errContent.Length; + await httpRes.WriteAsync(errContent).ConfigureAwait(false); } 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)"); } } @@ -275,9 +294,9 @@ namespace Emby.Server.Implementations.HttpServer { connection.Dispose(); } - catch + catch (Exception ex) { - + _logger.LogError(ex, "Error disposing connection"); } } } @@ -431,7 +450,7 @@ namespace Emby.Server.Implementations.HttpServer { httpRes.StatusCode = 503; httpRes.ContentType = "text/plain"; - await Write(httpRes, "Server shutting down").ConfigureAwait(false); + await httpRes.WriteAsync("Server shutting down", cancellationToken).ConfigureAwait(false); return; } @@ -439,7 +458,7 @@ namespace Emby.Server.Implementations.HttpServer { httpRes.StatusCode = 400; httpRes.ContentType = "text/plain"; - await Write(httpRes, "Invalid host").ConfigureAwait(false); + await httpRes.WriteAsync("Invalid host", cancellationToken).ConfigureAwait(false); return; } @@ -447,7 +466,7 @@ namespace Emby.Server.Implementations.HttpServer { httpRes.StatusCode = 403; httpRes.ContentType = "text/plain"; - await Write(httpRes, "Forbidden").ConfigureAwait(false); + await httpRes.WriteAsync("Forbidden", cancellationToken).ConfigureAwait(false); return; } @@ -460,101 +479,28 @@ namespace Emby.Server.Implementations.HttpServer if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase)) { httpRes.StatusCode = 200; - httpRes.AddHeader("Access-Control-Allow-Origin", "*"); - httpRes.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); - httpRes.AddHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization"); + httpRes.Headers.Add("Access-Control-Allow-Origin", "*"); + httpRes.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); + httpRes.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization"); httpRes.ContentType = "text/plain"; - await Write(httpRes, string.Empty).ConfigureAwait(false); + await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false); return; } urlToLog = GetUrlToLog(urlString); - Logger.LogDebug("HTTP {HttpMethod} {Url} UserAgent: {UserAgent} \nHeaders: {@Headers}", urlToLog, httpReq.UserAgent ?? string.Empty, httpReq.HttpMethod, httpReq.Headers); - - if (string.Equals(localPath, "/emby/", StringComparison.OrdinalIgnoreCase) || - string.Equals(localPath, "/mediabrowser/", StringComparison.OrdinalIgnoreCase)) - { - RedirectToUrl(httpRes, DefaultRedirectPath); - return; - } - - if (string.Equals(localPath, "/emby", StringComparison.OrdinalIgnoreCase) || - string.Equals(localPath, "/mediabrowser", StringComparison.OrdinalIgnoreCase)) - { - RedirectToUrl(httpRes, "emby/" + DefaultRedirectPath); - return; - } - - if (localPath.IndexOf("mediabrowser/web", StringComparison.OrdinalIgnoreCase) != -1) - { - httpRes.StatusCode = 200; - httpRes.ContentType = "text/html"; - var newUrl = urlString.Replace("mediabrowser", "emby", StringComparison.OrdinalIgnoreCase) - .Replace("/dashboard/", "/web/", StringComparison.OrdinalIgnoreCase); - - if (!string.Equals(newUrl, urlString, StringComparison.OrdinalIgnoreCase)) - { - await Write(httpRes, - "<!doctype html><html><head><title>Emby</title></head><body>Please update your Emby bookmark to <a href=\"" + - newUrl + "\">" + newUrl + "</a></body></html>").ConfigureAwait(false); - return; - } - } - - if (localPath.IndexOf("dashboard/", StringComparison.OrdinalIgnoreCase) != -1 && - localPath.IndexOf("web/dashboard", StringComparison.OrdinalIgnoreCase) == -1) - { - httpRes.StatusCode = 200; - httpRes.ContentType = "text/html"; - var newUrl = urlString.Replace("mediabrowser", "emby", StringComparison.OrdinalIgnoreCase) - .Replace("/dashboard/", "/web/", StringComparison.OrdinalIgnoreCase); - - if (!string.Equals(newUrl, urlString, StringComparison.OrdinalIgnoreCase)) - { - await Write(httpRes, - "<!doctype html><html><head><title>Emby</title></head><body>Please update your Emby bookmark to <a href=\"" + - newUrl + "\">" + newUrl + "</a></body></html>").ConfigureAwait(false); - return; - } - } - - if (string.Equals(localPath, "/web", StringComparison.OrdinalIgnoreCase)) - { - RedirectToUrl(httpRes, DefaultRedirectPath); - return; - } - - if (string.Equals(localPath, "/web/", StringComparison.OrdinalIgnoreCase)) - { - RedirectToUrl(httpRes, "../" + DefaultRedirectPath); - return; - } - if (string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(localPath, _baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase) + || string.Equals(localPath, _baseUrlPrefix, StringComparison.OrdinalIgnoreCase) + || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase) + || string.IsNullOrEmpty(localPath) + || !localPath.StartsWith(_baseUrlPrefix)) { - RedirectToUrl(httpRes, DefaultRedirectPath); + // Always redirect back to the default path if the base prefix is invalid or missing + _logger.LogDebug("Normalizing a URL at {0}", localPath); + httpRes.Redirect(_baseUrlPrefix + "/" + _defaultRedirectPath); return; } - if (string.IsNullOrEmpty(localPath)) - { - RedirectToUrl(httpRes, "/" + DefaultRedirectPath); - return; - } - - if (!string.Equals(httpReq.QueryString["r"], "0", StringComparison.OrdinalIgnoreCase)) - { - if (localPath.EndsWith("web/dashboard.html", StringComparison.OrdinalIgnoreCase)) - { - RedirectToUrl(httpRes, "index.html#!/dashboard.html"); - } - - if (localPath.EndsWith("web/home.html", StringComparison.OrdinalIgnoreCase)) - { - RedirectToUrl(httpRes, "index.html"); - } - } - if (!string.IsNullOrEmpty(GlobalResponse)) { // We don't want the address pings in ApplicationHost to fail @@ -562,35 +508,34 @@ namespace Emby.Server.Implementations.HttpServer { httpRes.StatusCode = 503; httpRes.ContentType = "text/html"; - await Write(httpRes, GlobalResponse).ConfigureAwait(false); + await httpRes.WriteAsync(GlobalResponse, cancellationToken).ConfigureAwait(false); return; } } var handler = GetServiceHandler(httpReq); - if (handler != null) { - await handler.ProcessRequestAsync(this, httpReq, httpRes, Logger, cancellationToken).ConfigureAwait(false); + await handler.ProcessRequestAsync(this, httpReq, httpRes, _logger, cancellationToken).ConfigureAwait(false); } else { - await ErrorHandler(new FileNotFoundException(), httpReq, false, false).ConfigureAwait(false); + await ErrorHandler(new FileNotFoundException(), httpReq, false).ConfigureAwait(false); } } catch (Exception ex) when (ex is SocketException || ex is IOException || ex is OperationCanceledException) { - await ErrorHandler(ex, httpReq, false, false).ConfigureAwait(false); + await ErrorHandler(ex, httpReq, false).ConfigureAwait(false); } catch (SecurityException ex) { - await ErrorHandler(ex, httpReq, false, true).ConfigureAwait(false); + await ErrorHandler(ex, httpReq, false).ConfigureAwait(false); } catch (Exception ex) { var logException = !string.Equals(ex.GetType().Name, "SocketException", StringComparison.OrdinalIgnoreCase); - await ErrorHandler(ex, httpReq, logException, false).ConfigureAwait(false); + await ErrorHandler(ex, httpReq, logException).ConfigureAwait(false); } finally { @@ -598,11 +543,7 @@ namespace Emby.Server.Implementations.HttpServer var elapsed = stopWatch.Elapsed; if (elapsed.TotalMilliseconds > 500) { - Logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog); - } - else - { - Logger.LogDebug("HTTP Response {StatusCode} to {RemoteIp}. Time: {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog); + _logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog); } } } @@ -619,18 +560,11 @@ namespace Emby.Server.Implementations.HttpServer return new ServiceHandler(restPath, contentType); } - Logger.LogError("Could not find handler for {PathInfo}", pathInfo); + _logger.LogError("Could not find handler for {PathInfo}", pathInfo); return null; } - private static Task Write(IResponse response, string text) - { - var bOutput = Encoding.UTF8.GetBytes(text); - response.OriginalResponse.ContentLength = bOutput.Length; - return response.OutputStream.WriteAsync(bOutput, 0, bOutput.Length); - } - - private void RedirectToSecureUrl(IHttpRequest httpReq, IResponse httpRes, string url) + private void RedirectToSecureUrl(IHttpRequest httpReq, HttpResponse httpRes, string url) { if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) { @@ -640,23 +574,11 @@ namespace Emby.Server.Implementations.HttpServer Scheme = "https" }; url = builder.Uri.ToString(); - - RedirectToUrl(httpRes, url); - } - else - { - RedirectToUrl(httpRes, url); } - } - public static void RedirectToUrl(IResponse httpRes, string url) - { - httpRes.StatusCode = 302; - httpRes.AddHeader("Location", url); + httpRes.Redirect(url); } - public ServiceController ServiceController { get; private set; } - /// <summary> /// Adds the rest handlers. /// </summary> @@ -672,9 +594,9 @@ namespace Emby.Server.Implementations.HttpServer var types = services.Select(r => r.GetType()); ServiceController.Init(this, types); - ResponseFilters = new Action<IRequest, IResponse, object>[] + ResponseFilters = new Action<IRequest, HttpResponse, object>[] { - new ResponseFilter(Logger).FilterResponse + new ResponseFilter(_logger).FilterResponse }; } @@ -685,22 +607,21 @@ namespace Emby.Server.Implementations.HttpServer foreach (var route in clone) { - routes.Add(new RouteAttribute(NormalizeEmbyRoutePath(route.Path), route.Verbs) + routes.Add(new RouteAttribute(NormalizeCustomRoutePath(route.Path), route.Verbs) { Notes = route.Notes, Priority = route.Priority, Summary = route.Summary }); - routes.Add(new RouteAttribute(NormalizeMediaBrowserRoutePath(route.Path), route.Verbs) + routes.Add(new RouteAttribute(NormalizeEmbyRoutePath(route.Path), route.Verbs) { Notes = route.Notes, Priority = route.Priority, Summary = route.Summary }); - // needed because apps add /emby, and some users also add /emby, thereby double prefixing - routes.Add(new RouteAttribute(DoubleNormalizeEmbyRoutePath(route.Path), route.Verbs) + routes.Add(new RouteAttribute(NormalizeMediaBrowserRoutePath(route.Path), route.Verbs) { Notes = route.Notes, Priority = route.Priority, @@ -741,55 +662,41 @@ namespace Emby.Server.Implementations.HttpServer return _socketListener.ProcessWebSocketRequest(context); } - //TODO Add Jellyfin Route Path Normalizer - private static string NormalizeEmbyRoutePath(string path) + private string NormalizeEmbyRoutePath(string path) { - if (path.StartsWith("/", StringComparison.OrdinalIgnoreCase)) - { - return "/emby" + path; - } - - return "emby/" + path; + _logger.LogDebug("Normalizing /emby route"); + return _baseUrlPrefix + "/emby" + NormalizeUrlPath(path); } - private static string NormalizeMediaBrowserRoutePath(string path) + private string NormalizeMediaBrowserRoutePath(string path) { - if (path.StartsWith("/", StringComparison.OrdinalIgnoreCase)) - { - return "/mediabrowser" + path; - } - - return "mediabrowser/" + path; + _logger.LogDebug("Normalizing /mediabrowser route"); + return _baseUrlPrefix + "/mediabrowser" + NormalizeUrlPath(path); } - private static string DoubleNormalizeEmbyRoutePath(string path) + private string NormalizeCustomRoutePath(string path) { - if (path.StartsWith("/", StringComparison.OrdinalIgnoreCase)) - { - return "/emby/emby" + path; - } - - return "emby/emby/" + path; + _logger.LogDebug("Normalizing custom route {0}", path); + return _baseUrlPrefix + NormalizeUrlPath(path); } - private bool _disposed; - private readonly object _disposeLock = new object(); + /// <inheritdoc /> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } protected virtual void Dispose(bool disposing) { if (_disposed) return; - lock (_disposeLock) + if (disposing) { - if (_disposed) return; - - _disposed = true; - - if (disposing) - { - Stop(); - } + Stop(); } + + _disposed = true; } /// <summary> @@ -803,7 +710,7 @@ namespace Emby.Server.Implementations.HttpServer return Task.CompletedTask; } - Logger.LogDebug("Websocket message received: {0}", result.MessageType); + _logger.LogDebug("Websocket message received: {0}", result.MessageType); IEnumerable<Task> GetTasks() { @@ -815,10 +722,5 @@ namespace Emby.Server.Implementations.HttpServer return Task.WhenAll(GetTasks()); } - - public void Dispose() - { - Dispose(true); - } } } diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs index 08f424757..3e731366e 100644 --- a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs +++ b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs @@ -2,6 +2,7 @@ using System; using System.Globalization; using System.Text; using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; @@ -9,7 +10,7 @@ namespace Emby.Server.Implementations.HttpServer { public class ResponseFilter { - private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US")); private readonly ILogger _logger; public ResponseFilter(ILogger logger) @@ -23,12 +24,12 @@ namespace Emby.Server.Implementations.HttpServer /// <param name="req">The req.</param> /// <param name="res">The res.</param> /// <param name="dto">The dto.</param> - public void FilterResponse(IRequest req, IResponse res, object dto) + public void FilterResponse(IRequest req, HttpResponse res, object dto) { // Try to prevent compatibility view - res.AddHeader("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Authorization"); - res.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); - res.AddHeader("Access-Control-Allow-Origin", "*"); + res.Headers.Add("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, Content-Type, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, X-Emby-Authorization"); + res.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); + res.Headers.Add("Access-Control-Allow-Origin", "*"); if (dto is Exception exception) { @@ -39,7 +40,7 @@ namespace Emby.Server.Implementations.HttpServer var error = exception.Message.Replace(Environment.NewLine, " "); error = RemoveControlCharacters(error); - res.AddHeader("X-Application-Error-Code", error); + res.Headers.Add("X-Application-Error-Code", error); } } @@ -54,12 +55,11 @@ namespace Emby.Server.Implementations.HttpServer if (hasHeaders.Headers.TryGetValue(HeaderNames.ContentLength, out string contentLength) && !string.IsNullOrEmpty(contentLength)) { - var length = long.Parse(contentLength, UsCulture); + var length = long.Parse(contentLength, _usCulture); if (length > 0) { - res.OriginalResponse.ContentLength = length; - res.SendChunked = false; + res.ContentLength = length; } } } @@ -72,9 +72,12 @@ namespace Emby.Server.Implementations.HttpServer /// <returns>System.String.</returns> public static string RemoveControlCharacters(string inString) { - if (inString == null) return null; + if (inString == null) + { + return null; + } - var newString = new StringBuilder(); + var newString = new StringBuilder(inString.Length); foreach (var ch in inString) { @@ -83,6 +86,7 @@ namespace Emby.Server.Implementations.HttpServer newString.Append(ch); } } + return newString.ToString(); } } diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index 1027883ed..93a61fe67 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -3,7 +3,6 @@ using System.Linq; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; @@ -13,28 +12,23 @@ namespace Emby.Server.Implementations.HttpServer.Security { public class AuthService : IAuthService { + private readonly IAuthorizationContext _authorizationContext; + private readonly ISessionManager _sessionManager; private readonly IServerConfigurationManager _config; + private readonly INetworkManager _networkManager; - public AuthService(IUserManager userManager, IAuthorizationContext authorizationContext, IServerConfigurationManager config, ISessionManager sessionManager, INetworkManager networkManager) + public AuthService( + IAuthorizationContext authorizationContext, + IServerConfigurationManager config, + ISessionManager sessionManager, + INetworkManager networkManager) { - AuthorizationContext = authorizationContext; + _authorizationContext = authorizationContext; _config = config; - SessionManager = sessionManager; - UserManager = userManager; - NetworkManager = networkManager; + _sessionManager = sessionManager; + _networkManager = networkManager; } - public IUserManager UserManager { get; private set; } - public IAuthorizationContext AuthorizationContext { get; private set; } - public ISessionManager SessionManager { get; private set; } - public INetworkManager NetworkManager { get; private set; } - - /// <summary> - /// Redirect the client to a specific URL if authentication failed. - /// If this property is null, simply `401 Unauthorized` is returned. - /// </summary> - public string HtmlRedirect { get; set; } - public void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues) { ValidateUser(request, authAttribtues); @@ -43,7 +37,7 @@ namespace Emby.Server.Implementations.HttpServer.Security private void ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues) { // This code is executed before the service - var auth = AuthorizationContext.GetAuthorizationInfo(request); + var auth = _authorizationContext.GetAuthorizationInfo(request); if (!IsExemptFromAuthenticationToken(authAttribtues, request)) { @@ -57,7 +51,7 @@ namespace Emby.Server.Implementations.HttpServer.Security var user = auth.User; - if (user == null & !auth.UserId.Equals(Guid.Empty)) + if (user == null && auth.UserId != Guid.Empty) { throw new SecurityException("User with Id " + auth.UserId + " not found"); } @@ -80,7 +74,7 @@ namespace Emby.Server.Implementations.HttpServer.Security !string.IsNullOrEmpty(auth.Client) && !string.IsNullOrEmpty(auth.Device)) { - SessionManager.LogSessionActivity(auth.Client, + _sessionManager.LogSessionActivity(auth.Client, auth.Version, auth.DeviceId, auth.Device, @@ -89,7 +83,9 @@ namespace Emby.Server.Implementations.HttpServer.Security } } - private void ValidateUserAccess(User user, IRequest request, + private void ValidateUserAccess( + User user, + IRequest request, IAuthenticationAttributes authAttribtues, AuthorizationInfo auth) { @@ -101,7 +97,7 @@ namespace Emby.Server.Implementations.HttpServer.Security }; } - if (!user.Policy.EnableRemoteAccess && !NetworkManager.IsInLocalNetwork(request.RemoteIp)) + if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(request.RemoteIp)) { throw new SecurityException("User account has been disabled.") { @@ -109,11 +105,11 @@ namespace Emby.Server.Implementations.HttpServer.Security }; } - if (!user.Policy.IsAdministrator && - !authAttribtues.EscapeParentalControl && - !user.IsParentalScheduleAllowed()) + if (!user.Policy.IsAdministrator + && !authAttribtues.EscapeParentalControl + && !user.IsParentalScheduleAllowed()) { - request.Response.AddHeader("X-Application-Error-Code", "ParentalControl"); + request.Response.Headers.Add("X-Application-Error-Code", "ParentalControl"); throw new SecurityException("This user account is not allowed access at this time.") { @@ -183,6 +179,7 @@ namespace Emby.Server.Implementations.HttpServer.Security }; } } + if (roles.Contains("delete", StringComparer.OrdinalIgnoreCase)) { if (user == null || !user.Policy.EnableContentDeletion) @@ -193,6 +190,7 @@ namespace Emby.Server.Implementations.HttpServer.Security }; } } + if (roles.Contains("download", StringComparer.OrdinalIgnoreCase)) { if (user == null || !user.Policy.EnableContentDownloading) diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs index 276312a30..457448604 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; @@ -89,7 +90,7 @@ namespace Emby.Server.Implementations.HttpServer.Security AccessToken = token }); - var tokenInfo = result.Items.Length > 0 ? result.Items[0] : null; + var tokenInfo = result.Items.Count > 0 ? result.Items[0] : null; if (tokenInfo != null) { @@ -190,17 +191,23 @@ namespace Emby.Server.Implementations.HttpServer.Security /// <returns>Dictionary{System.StringSystem.String}.</returns> private Dictionary<string, string> GetAuthorization(string authorizationHeader) { - if (authorizationHeader == null) return null; + if (authorizationHeader == null) + { + return null; + } var parts = authorizationHeader.Split(new[] { ' ' }, 2); // There should be at least to parts - if (parts.Length != 2) return null; + if (parts.Length != 2) + { + return null; + } var acceptedNames = new[] { "MediaBrowser", "Emby" }; // It has to be a digest request - if (!acceptedNames.Contains(parts[0] ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + if (!acceptedNames.Contains(parts[0], StringComparer.OrdinalIgnoreCase)) { return null; } @@ -232,7 +239,7 @@ namespace Emby.Server.Implementations.HttpServer.Security return value; } - return System.Net.WebUtility.HtmlEncode(value); + return WebUtility.HtmlEncode(value); } } } diff --git a/Emby.Server.Implementations/IO/IsoManager.cs b/Emby.Server.Implementations/IO/IsoManager.cs index f0a15097c..94e92c2a6 100644 --- a/Emby.Server.Implementations/IO/IsoManager.cs +++ b/Emby.Server.Implementations/IO/IsoManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -8,12 +9,12 @@ using MediaBrowser.Model.IO; namespace Emby.Server.Implementations.IO { /// <summary> - /// Class IsoManager + /// Class IsoManager. /// </summary> public class IsoManager : IIsoManager { /// <summary> - /// The _mounters + /// The _mounters. /// </summary> private readonly List<IIsoMounter> _mounters = new List<IIsoMounter>(); @@ -22,9 +23,7 @@ namespace Emby.Server.Implementations.IO /// </summary> /// <param name="isoPath">The iso path.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>IsoMount.</returns> - /// <exception cref="ArgumentNullException">isoPath</exception> - /// <exception cref="ArgumentException"></exception> + /// <returns><see creaf="IsoMount" />.</returns> public Task<IIsoMount> Mount(string isoPath, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(isoPath)) @@ -36,7 +35,11 @@ namespace Emby.Server.Implementations.IO if (mounter == null) { - throw new ArgumentException(string.Format("No mounters are able to mount {0}", isoPath)); + throw new ArgumentException( + string.Format( + CultureInfo.InvariantCulture, + "No mounters are able to mount {0}", + isoPath)); } return mounter.Mount(isoPath, cancellationToken); @@ -60,16 +63,5 @@ namespace Emby.Server.Implementations.IO { _mounters.AddRange(mounters); } - - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> - public void Dispose() - { - foreach (var mounter in _mounters) - { - mounter.Dispose(); - } - } } } diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 8517abed6..ae8371a32 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Diagnostics; using System.IO; using System.Linq; @@ -555,7 +556,7 @@ namespace Emby.Server.Implementations.IO throw new ArgumentNullException(nameof(file2)); } - var temp1 = Path.Combine(_tempPath, Guid.NewGuid().ToString("N")); + var temp1 = Path.Combine(_tempPath, Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)); // Copying over will fail against hidden files SetHidden(file1, false); diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs index 46f209b4b..6afcf567a 100644 --- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading; @@ -20,14 +21,6 @@ namespace Emby.Server.Implementations.Images public abstract class BaseDynamicImageProvider<T> : IHasItemChangeMonitor, IForcedProvider, ICustomMetadataProvider<T>, IHasOrder where T : BaseItem { - protected virtual IReadOnlyCollection<ImageType> SupportedImages { get; } - = new ImageType[] { ImageType.Primary }; - - protected IFileSystem FileSystem { get; private set; } - protected IProviderManager ProviderManager { get; private set; } - protected IApplicationPaths ApplicationPaths { get; private set; } - protected IImageProcessor ImageProcessor { get; set; } - protected BaseDynamicImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor) { ApplicationPaths = applicationPaths; @@ -36,6 +29,24 @@ namespace Emby.Server.Implementations.Images ImageProcessor = imageProcessor; } + protected IFileSystem FileSystem { get; } + + protected IProviderManager ProviderManager { get; } + + protected IApplicationPaths ApplicationPaths { get; } + + protected IImageProcessor ImageProcessor { get; set; } + + protected virtual IReadOnlyCollection<ImageType> SupportedImages { get; } + = new ImageType[] { ImageType.Primary }; + + /// <inheritdoc /> + public string Name => "Dynamic Image Provider"; + + protected virtual int MaxImageAgeDays => 7; + + public int Order => 0; + protected virtual bool Supports(BaseItem _) => true; public async Task<ItemUpdateType> FetchAsync(T item, MetadataRefreshOptions options, CancellationToken cancellationToken) @@ -84,12 +95,13 @@ namespace Emby.Server.Implementations.Images return FetchToFileInternal(item, items, imageType, cancellationToken); } - protected async Task<ItemUpdateType> FetchToFileInternal(BaseItem item, + protected async Task<ItemUpdateType> FetchToFileInternal( + BaseItem item, IReadOnlyList<BaseItem> itemsWithImages, ImageType imageType, CancellationToken cancellationToken) { - var outputPathWithoutExtension = Path.Combine(ApplicationPaths.TempDirectory, Guid.NewGuid().ToString("N")); + var outputPathWithoutExtension = Path.Combine(ApplicationPaths.TempDirectory, Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)); Directory.CreateDirectory(Path.GetDirectoryName(outputPathWithoutExtension)); string outputPath = CreateImage(item, itemsWithImages, outputPathWithoutExtension, imageType, 0); @@ -180,8 +192,6 @@ namespace Emby.Server.Implementations.Images return outputPath; } - public string Name => "Dynamic Image Provider"; - protected virtual string CreateImage(BaseItem item, IReadOnlyCollection<BaseItem> itemsWithImages, string outputPathWithoutExtension, @@ -213,8 +223,6 @@ namespace Emby.Server.Implementations.Images throw new ArgumentException("Unexpected image type", nameof(imageType)); } - protected virtual int MaxImageAgeDays => 7; - public bool HasChanged(BaseItem item, IDirectoryService directoryServicee) { if (!Supports(item)) @@ -262,15 +270,9 @@ namespace Emby.Server.Implementations.Images protected virtual bool HasChangedByDate(BaseItem item, ItemImageInfo image) { var age = DateTime.UtcNow - image.DateModified; - if (age.TotalDays <= MaxImageAgeDays) - { - return false; - } - return true; + return age.TotalDays > MaxImageAgeDays; } - public int Order => 0; - protected string CreateSingleImage(IEnumerable<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType) { var image = itemsWithImages diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index f1ae2fc9c..8bdb38784 100644 --- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -57,7 +57,6 @@ namespace Emby.Server.Implementations.Library } var filename = fileInfo.Name; - var path = fileInfo.FullName; // Ignore hidden files on UNIX if (Environment.OSVersion.Platform != PlatformID.Win32NT diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index fe09b07ff..c95b00ede 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -2,24 +2,30 @@ using System; using System.Linq; using System.Text; using System.Threading.Tasks; +using MediaBrowser.Common.Cryptography; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Cryptography; +using static MediaBrowser.Common.HexHelper; namespace Emby.Server.Implementations.Library { public class DefaultAuthenticationProvider : IAuthenticationProvider, IRequiresResolvedUser { private readonly ICryptoProvider _cryptographyProvider; - public DefaultAuthenticationProvider(ICryptoProvider crypto) + + public DefaultAuthenticationProvider(ICryptoProvider cryptographyProvider) { - _cryptographyProvider = crypto; + _cryptographyProvider = cryptographyProvider; } + /// <inheritdoc /> public string Name => "Default"; + /// <inheritdoc /> public bool IsEnabled => true; + /// <inheritdoc /> // This is dumb and an artifact of the backwards way auth providers were designed. // This version of authenticate was never meant to be called, but needs to be here for interface compat // Only the providers that don't provide local user support use this @@ -28,17 +34,18 @@ namespace Emby.Server.Implementations.Library throw new NotImplementedException(); } - // This is the verson that we need to use for local users. Because reasons. + /// <inheritdoc /> + // This is the version that we need to use for local users. Because reasons. public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser) { bool success = false; if (resolvedUser == null) { - throw new Exception("Invalid username or password"); + throw new ArgumentNullException(nameof(resolvedUser)); } - // As long as jellyfin supports passwordless users, we need this little block here to accomodate - if (IsPasswordEmpty(resolvedUser, password)) + // As long as jellyfin supports passwordless users, we need this little block here to accommodate + if (!HasPassword(resolvedUser) && string.IsNullOrEmpty(password)) { return Task.FromResult(new ProviderAuthenticationResult { @@ -46,41 +53,27 @@ namespace Emby.Server.Implementations.Library }); } - ConvertPasswordFormat(resolvedUser); byte[] passwordbytes = Encoding.UTF8.GetBytes(password); - PasswordHash readyHash = new PasswordHash(resolvedUser.Password); - byte[] calculatedHash; - string calculatedHashString; - if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id) || _cryptographyProvider.DefaultHashMethod == readyHash.Id) + PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password); + if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id) + || _cryptographyProvider.DefaultHashMethod == readyHash.Id) { - if (string.IsNullOrEmpty(readyHash.Salt)) - { - calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes); - calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty); - } - else - { - calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.SaltBytes); - calculatedHashString = BitConverter.ToString(calculatedHash).Replace("-", string.Empty); - } + byte[] calculatedHash = _cryptographyProvider.ComputeHash(readyHash.Id, passwordbytes, readyHash.Salt); - if (calculatedHashString == readyHash.Hash) + if (calculatedHash.SequenceEqual(readyHash.Hash)) { success = true; - // throw new Exception("Invalid username or password"); } } else { - throw new Exception(string.Format($"Requested crypto method not available in provider: {readyHash.Id}")); + throw new AuthenticationException($"Requested crypto method not available in provider: {readyHash.Id}"); } - // var success = string.Equals(GetPasswordHash(resolvedUser), GetHashedString(resolvedUser, password), StringComparison.OrdinalIgnoreCase); - if (!success) { - throw new Exception("Invalid username or password"); + throw new AuthenticationException("Invalid username or password"); } return Task.FromResult(new ProviderAuthenticationResult @@ -89,89 +82,31 @@ namespace Emby.Server.Implementations.Library }); } - // This allows us to move passwords forward to the newformat without breaking. They are still insecure, unsalted, and dumb before a password change - // but at least they are in the new format. - private void ConvertPasswordFormat(User user) - { - if (string.IsNullOrEmpty(user.Password)) - { - return; - } - - if (!user.Password.Contains("$")) - { - string hash = user.Password; - user.Password = string.Format("$SHA1${0}", hash); - } - - if (user.EasyPassword != null && !user.EasyPassword.Contains("$")) - { - string hash = user.EasyPassword; - user.EasyPassword = string.Format("$SHA1${0}", hash); - } - } - - public Task<bool> HasPassword(User user) - { - var hasConfiguredPassword = !IsPasswordEmpty(user, GetPasswordHash(user)); - return Task.FromResult(hasConfiguredPassword); - } - - private bool IsPasswordEmpty(User user, string password) - { - return (string.IsNullOrEmpty(user.Password) && string.IsNullOrEmpty(password)); - } + /// <inheritdoc /> + public bool HasPassword(User user) + => !string.IsNullOrEmpty(user.Password); + /// <inheritdoc /> public Task ChangePassword(User user, string newPassword) { - ConvertPasswordFormat(user); - // This is needed to support changing a no password user to a password user - if (string.IsNullOrEmpty(user.Password)) + if (string.IsNullOrEmpty(newPassword)) { - PasswordHash newPasswordHash = new PasswordHash(_cryptographyProvider); - newPasswordHash.SaltBytes = _cryptographyProvider.GenerateSalt(); - newPasswordHash.Salt = PasswordHash.ConvertToByteString(newPasswordHash.SaltBytes); - newPasswordHash.Id = _cryptographyProvider.DefaultHashMethod; - newPasswordHash.Hash = GetHashedStringChangeAuth(newPassword, newPasswordHash); - user.Password = newPasswordHash.ToString(); + user.Password = null; return Task.CompletedTask; } - PasswordHash passwordHash = new PasswordHash(user.Password); - if (passwordHash.Id == "SHA1" && string.IsNullOrEmpty(passwordHash.Salt)) - { - passwordHash.SaltBytes = _cryptographyProvider.GenerateSalt(); - passwordHash.Salt = PasswordHash.ConvertToByteString(passwordHash.SaltBytes); - passwordHash.Id = _cryptographyProvider.DefaultHashMethod; - passwordHash.Hash = GetHashedStringChangeAuth(newPassword, passwordHash); - } - else if (newPassword != null) - { - passwordHash.Hash = GetHashedString(user, newPassword); - } - - if (string.IsNullOrWhiteSpace(passwordHash.Hash)) - { - throw new ArgumentNullException(nameof(passwordHash.Hash)); - } - - user.Password = passwordHash.ToString(); + PasswordHash newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword); + user.Password = newPasswordHash.ToString(); return Task.CompletedTask; } - public string GetPasswordHash(User user) - { - return user.Password; - } - + /// <inheritdoc /> public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) { - ConvertPasswordFormat(user); - if (newPassword != null) { - newPasswordHash = string.Format("$SHA1${0}", GetHashedString(user, newPassword)); + newPasswordHash = _cryptographyProvider.CreatePasswordHash(newPassword).ToString(); } if (string.IsNullOrWhiteSpace(newPasswordHash)) @@ -182,21 +117,12 @@ namespace Emby.Server.Implementations.Library user.EasyPassword = newPasswordHash; } + /// <inheritdoc /> public string GetEasyPasswordHash(User user) { - // This should be removed in the future. This was added to let user login after - // Jellyfin 10.3.3 failed to save a well formatted PIN. - ConvertPasswordFormat(user); - return string.IsNullOrEmpty(user.EasyPassword) ? null - : (new PasswordHash(user.EasyPassword)).Hash; - } - - public string GetHashedStringChangeAuth(string newPassword, PasswordHash passwordHash) - { - passwordHash.HashBytes = Encoding.UTF8.GetBytes(newPassword); - return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash)); + : ToHexString(PasswordHash.Parse(user.EasyPassword).Hash); } /// <summary> @@ -204,28 +130,36 @@ namespace Emby.Server.Implementations.Library /// </summary> public string GetHashedString(User user, string str) { - PasswordHash passwordHash; if (string.IsNullOrEmpty(user.Password)) { - passwordHash = new PasswordHash(_cryptographyProvider); - } - else - { - ConvertPasswordFormat(user); - passwordHash = new PasswordHash(user.Password); + return _cryptographyProvider.CreatePasswordHash(str).ToString(); } - if (passwordHash.SaltBytes != null) - { - // the password is modern format with PBKDF and we should take advantage of that - passwordHash.HashBytes = Encoding.UTF8.GetBytes(str); - return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash)); - } - else + // TODO: make use of iterations parameter? + PasswordHash passwordHash = PasswordHash.Parse(user.Password); + return new PasswordHash( + passwordHash.Id, + _cryptographyProvider.ComputeHash( + passwordHash.Id, + Encoding.UTF8.GetBytes(str), + passwordHash.Salt), + passwordHash.Salt, + passwordHash.Parameters.ToDictionary(x => x.Key, y => y.Value)).ToString(); + } + + public byte[] GetHashed(User user, string str) + { + if (string.IsNullOrEmpty(user.Password)) { - // the password has no salt and should be called with the older method for safety - return PasswordHash.ConvertToByteString(_cryptographyProvider.ComputeHash(passwordHash.Id, Encoding.UTF8.GetBytes(str))); + return _cryptographyProvider.CreatePasswordHash(str).Hash; } + + // TODO: make use of iterations parameter? + PasswordHash passwordHash = PasswordHash.Parse(user.Password); + return _cryptographyProvider.ComputeHash( + passwordHash.Id, + Encoding.UTF8.GetBytes(str), + passwordHash.Salt); } } } diff --git a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs index e218749d9..fa6bbcf91 100644 --- a/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultPasswordResetProvider.cs @@ -1,132 +1,131 @@ -using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Extensions;
-using MediaBrowser.Controller.Authentication;
-using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Cryptography;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Users;
-
-namespace Emby.Server.Implementations.Library
-{
- public class DefaultPasswordResetProvider : IPasswordResetProvider
- {
- public string Name => "Default Password Reset Provider";
-
- public bool IsEnabled => true;
-
- private readonly string _passwordResetFileBase;
- private readonly string _passwordResetFileBaseDir;
- private readonly string _passwordResetFileBaseName = "passwordreset";
-
- private readonly IJsonSerializer _jsonSerializer;
- private readonly IUserManager _userManager;
- private readonly ICryptoProvider _crypto;
-
- public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IUserManager userManager, ICryptoProvider cryptoProvider)
- {
- _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath;
- _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, _passwordResetFileBaseName);
- _jsonSerializer = jsonSerializer;
- _userManager = userManager;
- _crypto = cryptoProvider;
- }
-
- public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
- {
- SerializablePasswordReset spr;
- HashSet<string> usersreset = new HashSet<string>();
- foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{_passwordResetFileBaseName}*"))
- {
- using (var str = File.OpenRead(resetfile))
- {
- spr = await _jsonSerializer.DeserializeFromStreamAsync<SerializablePasswordReset>(str).ConfigureAwait(false);
- }
-
- if (spr.ExpirationDate < DateTime.Now)
- {
- File.Delete(resetfile);
- }
- else if (spr.Pin.Replace("-", "").Equals(pin.Replace("-", ""), StringComparison.InvariantCultureIgnoreCase))
- {
- var resetUser = _userManager.GetUserByName(spr.UserName);
- if (resetUser == null)
- {
- throw new Exception($"User with a username of {spr.UserName} not found");
- }
-
- await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false);
- usersreset.Add(resetUser.Name);
- File.Delete(resetfile);
- }
- }
-
- if (usersreset.Count < 1)
- {
- throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}");
- }
- else
- {
- return new PinRedeemResult
- {
- Success = true,
- UsersReset = usersreset.ToArray()
- };
- }
- }
-
- public async Task<ForgotPasswordResult> StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork)
- {
- string pin = string.Empty;
- using (var cryptoRandom = System.Security.Cryptography.RandomNumberGenerator.Create())
- {
- byte[] bytes = new byte[4];
- cryptoRandom.GetBytes(bytes);
- pin = BitConverter.ToString(bytes);
- }
-
- DateTime expireTime = DateTime.Now.AddMinutes(30);
- string filePath = _passwordResetFileBase + user.InternalId + ".json";
- SerializablePasswordReset spr = new SerializablePasswordReset
- {
- ExpirationDate = expireTime,
- Pin = pin,
- PinFile = filePath,
- UserName = user.Name
- };
-
- try
- {
- using (FileStream fileStream = File.OpenWrite(filePath))
- {
- _jsonSerializer.SerializeToStream(spr, fileStream);
- await fileStream.FlushAsync().ConfigureAwait(false);
- }
- }
- catch (Exception e)
- {
- throw new Exception($"Error serializing or writing password reset for {user.Name} to location: {filePath}", e);
- }
-
- return new ForgotPasswordResult
- {
- Action = ForgotPasswordAction.PinCode,
- PinExpirationDate = expireTime,
- PinFile = filePath
- };
- }
-
- private class SerializablePasswordReset : PasswordPinCreationResult
- {
- public string Pin { get; set; }
-
- public string UserName { get; set; }
- }
- }
-}
+using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Users; + +namespace Emby.Server.Implementations.Library +{ + public class DefaultPasswordResetProvider : IPasswordResetProvider + { + private const string BaseResetFileName = "passwordreset"; + + private readonly IJsonSerializer _jsonSerializer; + private readonly IUserManager _userManager; + + private readonly string _passwordResetFileBase; + private readonly string _passwordResetFileBaseDir; + + public DefaultPasswordResetProvider( + IServerConfigurationManager configurationManager, + IJsonSerializer jsonSerializer, + IUserManager userManager) + { + _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath; + _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName); + _jsonSerializer = jsonSerializer; + _userManager = userManager; + } + + /// <inheritdoc /> + public string Name => "Default Password Reset Provider"; + + /// <inheritdoc /> + public bool IsEnabled => true; + + /// <inheritdoc /> + public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin) + { + SerializablePasswordReset spr; + List<string> usersreset = new List<string>(); + foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*")) + { + using (var str = File.OpenRead(resetfile)) + { + spr = await _jsonSerializer.DeserializeFromStreamAsync<SerializablePasswordReset>(str).ConfigureAwait(false); + } + + if (spr.ExpirationDate < DateTime.Now) + { + File.Delete(resetfile); + } + else if (string.Equals( + spr.Pin.Replace("-", string.Empty), + pin.Replace("-", string.Empty), + StringComparison.InvariantCultureIgnoreCase)) + { + var resetUser = _userManager.GetUserByName(spr.UserName); + if (resetUser == null) + { + throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found"); + } + + await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false); + usersreset.Add(resetUser.Name); + File.Delete(resetfile); + } + } + + if (usersreset.Count < 1) + { + throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}"); + } + else + { + return new PinRedeemResult + { + Success = true, + UsersReset = usersreset.ToArray() + }; + } + } + + /// <inheritdoc /> + public async Task<ForgotPasswordResult> StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork) + { + string pin = string.Empty; + using (var cryptoRandom = RandomNumberGenerator.Create()) + { + byte[] bytes = new byte[4]; + cryptoRandom.GetBytes(bytes); + pin = BitConverter.ToString(bytes); + } + + DateTime expireTime = DateTime.Now.AddMinutes(30); + string filePath = _passwordResetFileBase + user.InternalId + ".json"; + SerializablePasswordReset spr = new SerializablePasswordReset + { + ExpirationDate = expireTime, + Pin = pin, + PinFile = filePath, + UserName = user.Name + }; + + using (FileStream fileStream = File.OpenWrite(filePath)) + { + _jsonSerializer.SerializeToStream(spr, fileStream); + await fileStream.FlushAsync().ConfigureAwait(false); + } + + return new ForgotPasswordResult + { + Action = ForgotPasswordAction.PinCode, + PinExpirationDate = expireTime, + PinFile = filePath + }; + } + + private class SerializablePasswordReset : PasswordPinCreationResult + { + public string Pin { get; set; } + + public string UserName { get; set; } + } + } +} diff --git a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs index 45a33a296..a3c879f12 100644 --- a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs +++ b/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Library; @@ -26,7 +27,7 @@ namespace Emby.Server.Implementations.Library EnableStreamSharing = false; _closeFn = closeFn; ConsumerCount = 1; - UniqueId = Guid.NewGuid().ToString("N"); + UniqueId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); } public Task Close() diff --git a/Emby.Server.Implementations/Library/InvalidAuthProvider.cs b/Emby.Server.Implementations/Library/InvalidAuthProvider.cs index 25d233137..6956369dc 100644 --- a/Emby.Server.Implementations/Library/InvalidAuthProvider.cs +++ b/Emby.Server.Implementations/Library/InvalidAuthProvider.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Text; using System.Threading.Tasks; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Entities; @@ -16,12 +13,12 @@ namespace Emby.Server.Implementations.Library public Task<ProviderAuthenticationResult> Authenticate(string username, string password) { - throw new SecurityException("User Account cannot login with this provider. The Normal provider for this user cannot be found"); + throw new AuthenticationException("User Account cannot login with this provider. The Normal provider for this user cannot be found"); } - public Task<bool> HasPassword(User user) + public bool HasPassword(User user) { - return Task.FromResult(true); + return true; } public Task ChangePassword(User user, string newPassword) @@ -31,7 +28,7 @@ namespace Emby.Server.Implementations.Library public void ChangeEasyPassword(User user, string newPassword, string newPasswordHash) { - // Nothing here + // Nothing here } public string GetPasswordHash(User user) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 4b5063ada..87e951f25 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -779,12 +779,23 @@ namespace Emby.Server.Implementations.Library { var userRootPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath; + _logger.LogDebug("Creating userRootPath at {path}", userRootPath); Directory.CreateDirectory(userRootPath); - var tmpItem = GetItemById(GetNewItemId(userRootPath, typeof(UserRootFolder))) as UserRootFolder; + var newItemId = GetNewItemId(userRootPath, typeof(UserRootFolder)); + UserRootFolder tmpItem = null; + try + { + tmpItem = GetItemById(newItemId) as UserRootFolder; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating UserRootFolder {path}", newItemId); + } if (tmpItem == null) { + _logger.LogDebug("Creating new userRootFolder with DeepCopy"); tmpItem = ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(userRootPath))).DeepCopy<Folder, UserRootFolder>(); } @@ -796,6 +807,7 @@ namespace Emby.Server.Implementations.Library } _userRootFolder = tmpItem; + _logger.LogDebug("Setting userRootFolder: {folder}", _userRootFolder); } } } @@ -1146,8 +1158,10 @@ namespace Emby.Server.Implementations.Library public List<VirtualFolderInfo> GetVirtualFolders(bool includeRefreshState) { + _logger.LogDebug("Getting topLibraryFolders"); var topLibraryFolders = GetUserRootFolder().Children.ToList(); + _logger.LogDebug("Getting refreshQueue"); var refreshQueue = includeRefreshState ? _providerManagerFactory().GetRefreshQueue() : null; return _fileSystem.GetDirectoryPaths(ConfigurationManager.ApplicationPaths.DefaultUserViewsPath) @@ -1187,12 +1201,12 @@ namespace Emby.Server.Implementations.Library if (libraryFolder != null && libraryFolder.HasImage(ImageType.Primary)) { - info.PrimaryImageItemId = libraryFolder.Id.ToString("N"); + info.PrimaryImageItemId = libraryFolder.Id.ToString("N", CultureInfo.InvariantCulture); } if (libraryFolder != null) { - info.ItemId = libraryFolder.Id.ToString("N"); + info.ItemId = libraryFolder.Id.ToString("N", CultureInfo.InvariantCulture); info.LibraryOptions = GetLibraryOptions(libraryFolder); if (refreshQueue != null) @@ -1441,7 +1455,7 @@ namespace Emby.Server.Implementations.Library return new QueryResult<BaseItem> { - Items = list.ToArray() + Items = list }; } @@ -1977,8 +1991,7 @@ namespace Emby.Server.Implementations.Library public LibraryOptions GetLibraryOptions(BaseItem item) { - var collectionFolder = item as CollectionFolder; - if (collectionFolder == null) + if (!(item is CollectionFolder collectionFolder)) { collectionFolder = GetCollectionFolders(item) .OfType<CollectionFolder>() @@ -2135,12 +2148,12 @@ namespace Emby.Server.Implementations.Library string viewType, string sortName) { - var parentIdString = parentId.Equals(Guid.Empty) ? null : parentId.ToString("N"); - var idValues = "38_namedview_" + name + user.Id.ToString("N") + (parentIdString ?? string.Empty) + (viewType ?? string.Empty); + var parentIdString = parentId.Equals(Guid.Empty) ? null : parentId.ToString("N", CultureInfo.InvariantCulture); + var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType ?? string.Empty); var id = GetNewItemId(idValues, typeof(UserView)); - var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N")); + var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N", CultureInfo.InvariantCulture)); var item = GetItemById(id) as UserView; @@ -2271,7 +2284,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(name)); } - var parentIdString = parentId.Equals(Guid.Empty) ? null : parentId.ToString("N"); + var parentIdString = parentId.Equals(Guid.Empty) ? null : parentId.ToString("N", CultureInfo.InvariantCulture); var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType ?? string.Empty); if (!string.IsNullOrEmpty(uniqueId)) { @@ -2280,7 +2293,7 @@ namespace Emby.Server.Implementations.Library var id = GetNewItemId(idValues, typeof(UserView)); - var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N")); + var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N", CultureInfo.InvariantCulture)); var item = GetItemById(id) as UserView; diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs index c3082a78a..33e6f2434 100644 --- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs +++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs @@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.Library var now = DateTime.UtcNow; MediaInfo mediaInfo = null; - var cacheFilePath = string.IsNullOrEmpty(cacheKey) ? null : Path.Combine(_appPaths.CachePath, "mediainfo", cacheKey.GetMD5().ToString("N") + ".json"); + var cacheFilePath = string.IsNullOrEmpty(cacheKey) ? null : Path.Combine(_appPaths.CachePath, "mediainfo", cacheKey.GetMD5().ToString("N", CultureInfo.InvariantCulture) + ".json"); if (!string.IsNullOrEmpty(cacheKey)) { diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 24ab8e761..d83e1fc02 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -269,7 +269,7 @@ namespace Emby.Server.Implementations.Library private static void SetKeyProperties(IMediaSourceProvider provider, MediaSourceInfo mediaSource) { - var prefix = provider.GetType().FullName.GetMD5().ToString("N") + LiveStreamIdDelimeter; + var prefix = provider.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + LiveStreamIdDelimeter; if (!string.IsNullOrEmpty(mediaSource.OpenToken) && !mediaSource.OpenToken.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { @@ -626,7 +626,7 @@ namespace Emby.Server.Implementations.Library var now = DateTime.UtcNow; MediaInfo mediaInfo = null; - var cacheFilePath = string.IsNullOrEmpty(cacheKey) ? null : Path.Combine(_appPaths.CachePath, "mediainfo", cacheKey.GetMD5().ToString("N") + ".json"); + var cacheFilePath = string.IsNullOrEmpty(cacheKey) ? null : Path.Combine(_appPaths.CachePath, "mediainfo", cacheKey.GetMD5().ToString("N", CultureInfo.InvariantCulture) + ".json"); if (!string.IsNullOrEmpty(cacheKey)) { @@ -854,7 +854,7 @@ namespace Emby.Server.Implementations.Library var keys = key.Split(new[] { LiveStreamIdDelimeter }, 2); - var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N"), keys[0], StringComparison.OrdinalIgnoreCase)); + var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase)); var splitIndex = key.IndexOf(LiveStreamIdDelimeter); var keyId = key.Substring(splitIndex + 1); diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index dfa1edaff..36adc0b9c 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -152,7 +152,7 @@ namespace Emby.Server.Implementations.Library /// <returns>System.String.</returns> private static string GetCacheKey(long internalUserId, Guid itemId) { - return internalUserId.ToString(CultureInfo.InvariantCulture) + "-" + itemId.ToString("N"); + return internalUserId.ToString(CultureInfo.InvariantCulture) + "-" + itemId.ToString("N", CultureInfo.InvariantCulture); } public UserItemData GetUserData(User user, BaseItem item) diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index 1701ced42..52b2f56ff 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -7,24 +8,22 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Cryptography; using MediaBrowser.Common.Events; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Events; @@ -32,6 +31,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Users; using Microsoft.Extensions.Logging; +using static MediaBrowser.Common.HexHelper; namespace Emby.Server.Implementations.Library { @@ -41,34 +41,19 @@ namespace Emby.Server.Implementations.Library public class UserManager : IUserManager { /// <summary> - /// Gets the users. - /// </summary> - /// <value>The users.</value> - public IEnumerable<User> Users => _users; - - private User[] _users; - - /// <summary> /// The _logger /// </summary> private readonly ILogger _logger; - /// <summary> - /// Gets or sets the configuration manager. - /// </summary> - /// <value>The configuration manager.</value> - private IServerConfigurationManager ConfigurationManager { get; set; } + private readonly object _policySyncLock = new object(); /// <summary> /// Gets the active user repository /// </summary> /// <value>The user repository.</value> - private IUserRepository UserRepository { get; set; } - public event EventHandler<GenericEventArgs<User>> UserPasswordChanged; - + private readonly IUserRepository _userRepository; private readonly IXmlSerializer _xmlSerializer; private readonly IJsonSerializer _jsonSerializer; - private readonly INetworkManager _networkManager; private readonly Func<IImageProcessor> _imageProcessorFactory; @@ -76,6 +61,8 @@ namespace Emby.Server.Implementations.Library private readonly IServerApplicationHost _appHost; private readonly IFileSystem _fileSystem; + private ConcurrentDictionary<Guid, User> _users; + private IAuthenticationProvider[] _authenticationProviders; private DefaultAuthenticationProvider _defaultAuthenticationProvider; @@ -85,8 +72,7 @@ namespace Emby.Server.Implementations.Library private DefaultPasswordResetProvider _defaultPasswordResetProvider; public UserManager( - ILoggerFactory loggerFactory, - IServerConfigurationManager configurationManager, + ILogger<UserManager> logger, IUserRepository userRepository, IXmlSerializer xmlSerializer, INetworkManager networkManager, @@ -96,8 +82,8 @@ namespace Emby.Server.Implementations.Library IJsonSerializer jsonSerializer, IFileSystem fileSystem) { - _logger = loggerFactory.CreateLogger(nameof(UserManager)); - UserRepository = userRepository; + _logger = logger; + _userRepository = userRepository; _xmlSerializer = xmlSerializer; _networkManager = networkManager; _imageProcessorFactory = imageProcessorFactory; @@ -105,8 +91,51 @@ namespace Emby.Server.Implementations.Library _appHost = appHost; _jsonSerializer = jsonSerializer; _fileSystem = fileSystem; - ConfigurationManager = configurationManager; - _users = Array.Empty<User>(); + _users = null; + } + + public event EventHandler<GenericEventArgs<User>> UserPasswordChanged; + + /// <summary> + /// Occurs when [user updated]. + /// </summary> + public event EventHandler<GenericEventArgs<User>> UserUpdated; + + public event EventHandler<GenericEventArgs<User>> UserPolicyUpdated; + + public event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated; + + public event EventHandler<GenericEventArgs<User>> UserLockedOut; + + public event EventHandler<GenericEventArgs<User>> UserCreated; + + /// <summary> + /// Occurs when [user deleted]. + /// </summary> + public event EventHandler<GenericEventArgs<User>> UserDeleted; + + /// <inheritdoc /> + public IEnumerable<User> Users => _users.Values; + + /// <inheritdoc /> + public IEnumerable<Guid> UsersIds => _users.Keys; + + /// <summary> + /// Called when [user updated]. + /// </summary> + /// <param name="user">The user.</param> + private void OnUserUpdated(User user) + { + UserUpdated?.Invoke(this, new GenericEventArgs<User> { Argument = user }); + } + + /// <summary> + /// Called when [user deleted]. + /// </summary> + /// <param name="user">The user.</param> + private void OnUserDeleted(User user) + { + UserDeleted?.Invoke(this, new GenericEventArgs<User> { Argument = user }); } public NameIdPair[] GetAuthenticationProviders() @@ -137,7 +166,7 @@ namespace Emby.Server.Implementations.Library .ToArray(); } - public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders,IEnumerable<IPasswordResetProvider> passwordResetProviders) + public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders) { _authenticationProviders = authenticationProviders.ToArray(); @@ -150,54 +179,21 @@ namespace Emby.Server.Implementations.Library _defaultPasswordResetProvider = passwordResetProviders.OfType<DefaultPasswordResetProvider>().First(); } - #region UserUpdated Event /// <summary> - /// Occurs when [user updated]. - /// </summary> - public event EventHandler<GenericEventArgs<User>> UserUpdated; - public event EventHandler<GenericEventArgs<User>> UserPolicyUpdated; - public event EventHandler<GenericEventArgs<User>> UserConfigurationUpdated; - public event EventHandler<GenericEventArgs<User>> UserLockedOut; - - /// <summary> - /// Called when [user updated]. - /// </summary> - /// <param name="user">The user.</param> - private void OnUserUpdated(User user) - { - UserUpdated?.Invoke(this, new GenericEventArgs<User> { Argument = user }); - } - #endregion - - #region UserDeleted Event - /// <summary> - /// Occurs when [user deleted]. - /// </summary> - public event EventHandler<GenericEventArgs<User>> UserDeleted; - /// <summary> - /// Called when [user deleted]. - /// </summary> - /// <param name="user">The user.</param> - private void OnUserDeleted(User user) - { - UserDeleted?.Invoke(this, new GenericEventArgs<User> { Argument = user }); - } - #endregion - - /// <summary> - /// Gets a User by Id + /// Gets a User by Id. /// </summary> /// <param name="id">The id.</param> /// <returns>User.</returns> - /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentException"></exception> public User GetUserById(Guid id) { if (id == Guid.Empty) { - throw new ArgumentException(nameof(id), "Guid can't be empty"); + throw new ArgumentException("Guid can't be empty", nameof(id)); } - return Users.FirstOrDefault(u => u.Id == id); + _users.TryGetValue(id, out User user); + return user; } /// <summary> @@ -206,15 +202,13 @@ namespace Emby.Server.Implementations.Library /// <param name="id">The identifier.</param> /// <returns>User.</returns> public User GetUserById(string id) - { - return GetUserById(new Guid(id)); - } + => GetUserById(new Guid(id)); public User GetUserByName(string name) { if (string.IsNullOrWhiteSpace(name)) { - throw new ArgumentNullException(nameof(name)); + throw new ArgumentException("Invalid username", nameof(name)); } return Users.FirstOrDefault(u => string.Equals(u.Name, name, StringComparison.OrdinalIgnoreCase)); @@ -222,8 +216,9 @@ namespace Emby.Server.Implementations.Library public void Initialize() { - var users = LoadUsers(); - _users = users.ToArray(); + LoadUsers(); + + var users = Users; // If there are no local users with admin rights, make them all admins if (!users.Any(i => i.Policy.IsAdministrator)) @@ -240,14 +235,12 @@ namespace Emby.Server.Implementations.Library { // This is some regex that matches only on unicode "word" characters, as well as -, _ and @ // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness - // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.) + // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.) return Regex.IsMatch(username, @"^[\w\-'._@]*$"); } private static bool IsValidUsernameCharacter(char i) - { - return IsValidUsername(i.ToString()); - } + => IsValidUsername(i.ToString(CultureInfo.InvariantCulture)); public string MakeValidUsername(string username) { @@ -266,6 +259,7 @@ namespace Emby.Server.Implementations.Library builder.Append(c); } } + return builder.ToString(); } @@ -276,35 +270,31 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(username)); } - var user = Users - .FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); + var user = Users.FirstOrDefault(i => string.Equals(username, i.Name, StringComparison.OrdinalIgnoreCase)); var success = false; - string updatedUsername = null; IAuthenticationProvider authenticationProvider = null; if (user != null) { var authResult = await AuthenticateLocalUser(username, password, hashedPassword, user, remoteEndPoint).ConfigureAwait(false); - authenticationProvider = authResult.Item1; - updatedUsername = authResult.Item2; - success = authResult.Item3; + authenticationProvider = authResult.authenticationProvider; + success = authResult.success; } else { // user is null var authResult = await AuthenticateLocalUser(username, password, hashedPassword, null, remoteEndPoint).ConfigureAwait(false); - authenticationProvider = authResult.Item1; - updatedUsername = authResult.Item2; - success = authResult.Item3; + authenticationProvider = authResult.authenticationProvider; + string updatedUsername = authResult.username; + success = authResult.success; - if (success && authenticationProvider != null && !(authenticationProvider is DefaultAuthenticationProvider)) + if (success + && authenticationProvider != null + && !(authenticationProvider is DefaultAuthenticationProvider)) { // We should trust the user that the authprovider says, not what was typed - if (updatedUsername != username) - { - username = updatedUsername; - } + username = updatedUsername; // Search the database for the user again; the authprovider might have created it user = Users @@ -331,22 +321,26 @@ namespace Emby.Server.Implementations.Library if (user == null) { - throw new SecurityException("Invalid username or password entered."); + throw new AuthenticationException("Invalid username or password entered."); } if (user.Policy.IsDisabled) { - throw new SecurityException(string.Format("The {0} account is currently disabled. Please consult with your administrator.", user.Name)); + throw new AuthenticationException( + string.Format( + CultureInfo.InvariantCulture, + "The {0} account is currently disabled. Please consult with your administrator.", + user.Name)); } if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint)) { - throw new SecurityException("Forbidden."); + throw new AuthenticationException("Forbidden."); } if (!user.IsParentalScheduleAllowed()) { - throw new SecurityException("User is not allowed access at this time."); + throw new AuthenticationException("User is not allowed access at this time."); } // Update LastActivityDate and LastLoginDate, then save @@ -357,11 +351,12 @@ namespace Emby.Server.Implementations.Library user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow; UpdateUser(user); } - UpdateInvalidLoginAttemptCount(user, 0); + + ResetInvalidLoginAttemptCount(user); } else { - UpdateInvalidLoginAttemptCount(user, user.Policy.InvalidLoginAttemptCount + 1); + IncrementInvalidLoginAttemptCount(user); } _logger.LogInformation("Authentication request for {0} {1}.", user.Name, success ? "has succeeded" : "has been denied"); @@ -381,7 +376,7 @@ namespace Emby.Server.Implementations.Library private IAuthenticationProvider GetAuthenticationProvider(User user) { - return GetAuthenticationProviders(user).First(); + return GetAuthenticationProviders(user)[0]; } private IPasswordResetProvider GetPasswordResetProvider(User user) @@ -391,7 +386,7 @@ namespace Emby.Server.Implementations.Library private IAuthenticationProvider[] GetAuthenticationProviders(User user) { - var authenticationProviderId = user == null ? null : user.Policy.AuthenticationProviderId; + var authenticationProviderId = user?.Policy.AuthenticationProviderId; var providers = _authenticationProviders.Where(i => i.IsEnabled).ToArray(); @@ -429,139 +424,112 @@ namespace Emby.Server.Implementations.Library return providers; } - private async Task<Tuple<string, bool>> AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser) + private async Task<(string username, bool success)> AuthenticateWithProvider(IAuthenticationProvider provider, string username, string password, User resolvedUser) { try { - var requiresResolvedUser = provider as IRequiresResolvedUser; - ProviderAuthenticationResult authenticationResult = null; - if (requiresResolvedUser != null) - { - authenticationResult = await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false); - } - else - { - authenticationResult = await provider.Authenticate(username, password).ConfigureAwait(false); - } - if(authenticationResult.Username != username) + var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser + ? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false) + : await provider.Authenticate(username, password).ConfigureAwait(false); + + if (authenticationResult.Username != username) { _logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Username); username = authenticationResult.Username; } - return new Tuple<string, bool>(username, true); + return (username, true); } - catch (Exception ex) + catch (AuthenticationException ex) { - _logger.LogError(ex, "Error authenticating with provider {provider}", provider.Name); + _logger.LogError(ex, "Error authenticating with provider {Provider}", provider.Name); - return new Tuple<string, bool>(username, false); + return (username, false); } } - private async Task<Tuple<IAuthenticationProvider, string, bool>> AuthenticateLocalUser(string username, string password, string hashedPassword, User user, string remoteEndPoint) + private async Task<(IAuthenticationProvider authenticationProvider, string username, bool success)> AuthenticateLocalUser( + string username, + string password, + string hashedPassword, + User user, + string remoteEndPoint) { - string updatedUsername = null; bool success = false; IAuthenticationProvider authenticationProvider = null; - if (password != null && user != null) + foreach (var provider in GetAuthenticationProviders(user)) { - // Doesn't look like this is even possible to be used, because of password == null checks below - hashedPassword = _defaultAuthenticationProvider.GetHashedString(user, password); - } + var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); + var updatedUsername = providerAuthResult.username; + success = providerAuthResult.success; - if (password == null) - { - // legacy - success = string.Equals(GetAuthenticationProvider(user).GetPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); - } - else - { - foreach (var provider in GetAuthenticationProviders(user)) + if (success) { - var providerAuthResult = await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false); - updatedUsername = providerAuthResult.Item1; - success = providerAuthResult.Item2; - - if (success) - { - authenticationProvider = provider; - username = updatedUsername; - break; - } + authenticationProvider = provider; + username = updatedUsername; + break; } } - if (user != null) + if (!success + && _networkManager.IsInLocalNetwork(remoteEndPoint) + && user.Configuration.EnableLocalPassword) { - if (!success && _networkManager.IsInLocalNetwork(remoteEndPoint) && user.Configuration.EnableLocalPassword) - { - if (password == null) - { - // legacy - success = string.Equals(GetAuthenticationProvider(user).GetEasyPasswordHash(user), hashedPassword.Replace("-", string.Empty), StringComparison.OrdinalIgnoreCase); - } - else - { - success = string.Equals(GetAuthenticationProvider(user).GetEasyPasswordHash(user), _defaultAuthenticationProvider.GetHashedString(user, password), StringComparison.OrdinalIgnoreCase); - } - } + success = string.Equals( + GetLocalPasswordHash(user), + _defaultAuthenticationProvider.GetHashedString(user, password), + StringComparison.OrdinalIgnoreCase); } - return new Tuple<IAuthenticationProvider, string, bool>(authenticationProvider, username, success); + return (authenticationProvider, username, success); } - private void UpdateInvalidLoginAttemptCount(User user, int newValue) + private string GetLocalPasswordHash(User user) { - if (user.Policy.InvalidLoginAttemptCount == newValue || newValue <= 0) - { - return; - } - - user.Policy.InvalidLoginAttemptCount = newValue; - - // Check for users without a value here and then fill in the default value - // also protect from an always lockout if misconfigured - if (user.Policy.LoginAttemptsBeforeLockout == null || user.Policy.LoginAttemptsBeforeLockout == 0) - { - user.Policy.LoginAttemptsBeforeLockout = user.Policy.IsAdministrator ? 5 : 3; - } - - var maxCount = user.Policy.LoginAttemptsBeforeLockout; + return string.IsNullOrEmpty(user.EasyPassword) + ? null + : ToHexString(PasswordHash.Parse(user.EasyPassword).Hash); + } - var fireLockout = false; + private void ResetInvalidLoginAttemptCount(User user) + { + user.Policy.InvalidLoginAttemptCount = 0; + UpdateUserPolicy(user, user.Policy, false); + } - // -1 can be used to specify no lockout value - if (maxCount != -1 && newValue >= maxCount) + private void IncrementInvalidLoginAttemptCount(User user) + { + int invalidLogins = ++user.Policy.InvalidLoginAttemptCount; + int maxInvalidLogins = user.Policy.LoginAttemptsBeforeLockout; + if (maxInvalidLogins > 0 + && invalidLogins >= maxInvalidLogins) { - _logger.LogDebug("Disabling user {0} due to {1} unsuccessful login attempts.", user.Name, newValue); user.Policy.IsDisabled = true; - - fireLockout = true; + UserLockedOut?.Invoke(this, new GenericEventArgs<User>(user)); + _logger.LogWarning( + "Disabling user {UserName} due to {Attempts} unsuccessful login attempts.", + user.Name, + invalidLogins); } UpdateUserPolicy(user, user.Policy, false); - - if (fireLockout) - { - UserLockedOut?.Invoke(this, new GenericEventArgs<User>(user)); - } } /// <summary> - /// Loads the users from the repository + /// Loads the users from the repository. /// </summary> - /// <returns>IEnumerable{User}.</returns> - private List<User> LoadUsers() + private void LoadUsers() { - var users = UserRepository.RetrieveAllUsers(); + var users = _userRepository.RetrieveAllUsers(); // There always has to be at least one user. if (users.Count != 0) { - return users; + _users = new ConcurrentDictionary<Guid, User>( + users.Select(x => new KeyValuePair<Guid, User>(x.Id, x))); + return; } var defaultName = Environment.UserName; @@ -576,14 +544,15 @@ namespace Emby.Server.Implementations.Library user.DateLastSaved = DateTime.UtcNow; - UserRepository.CreateUser(user); + _userRepository.CreateUser(user); user.Policy.IsAdministrator = true; user.Policy.EnableContentDeletion = true; user.Policy.EnableRemoteControlOfOtherUsers = true; UpdateUserPolicy(user, user.Policy, false); - return new List<User> { user }; + _users = new ConcurrentDictionary<Guid, User>(); + _users[user.Id] = user; } public UserDto GetUserDto(User user, string remoteEndPoint = null) @@ -593,7 +562,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(user)); } - bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user).Result; + bool hasConfiguredPassword = GetAuthenticationProvider(user).HasPassword(user); bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(GetAuthenticationProvider(user).GetEasyPasswordHash(user)); bool hasPassword = user.Configuration.EnableLocalPassword && !string.IsNullOrEmpty(remoteEndPoint) && _networkManager.IsInLocalNetwork(remoteEndPoint) ? @@ -614,7 +583,7 @@ namespace Emby.Server.Implementations.Library Policy = user.Policy }; - if (!hasPassword && Users.Count() == 1) + if (!hasPassword && _users.Count == 1) { dto.EnableAutoLogin = true; } @@ -689,22 +658,26 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(user)); } - if (string.IsNullOrEmpty(newName)) + if (string.IsNullOrWhiteSpace(newName)) { - throw new ArgumentNullException(nameof(newName)); + throw new ArgumentException("Invalid username", nameof(newName)); } - if (Users.Any(u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase))) + if (user.Name.Equals(newName, StringComparison.OrdinalIgnoreCase)) { - throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", newName)); + throw new ArgumentException("The new and old names must be different."); } - if (user.Name.Equals(newName, StringComparison.Ordinal)) + if (Users.Any( + u => u.Id != user.Id && u.Name.Equals(newName, StringComparison.OrdinalIgnoreCase))) { - throw new ArgumentException("The new and old names must be different."); + throw new ArgumentException(string.Format( + CultureInfo.InvariantCulture, + "A user with the name '{0}' already exists.", + newName)); } - await user.Rename(newName); + await user.Rename(newName).ConfigureAwait(false); OnUserUpdated(user); } @@ -722,23 +695,30 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(user)); } - if (user.Id.Equals(Guid.Empty) || !Users.Any(u => u.Id.Equals(user.Id))) + if (user.Id == Guid.Empty) { - throw new ArgumentException(string.Format("User with name '{0}' and Id {1} does not exist.", user.Name, user.Id)); + throw new ArgumentException("Id can't be empty.", nameof(user)); + } + + if (!_users.ContainsKey(user.Id)) + { + throw new ArgumentException( + string.Format( + CultureInfo.InvariantCulture, + "A user '{0}' with Id {1} does not exist.", + user.Name, + user.Id), + nameof(user)); } user.DateModified = DateTime.UtcNow; user.DateLastSaved = DateTime.UtcNow; - UserRepository.UpdateUser(user); + _userRepository.UpdateUser(user); OnUserUpdated(user); } - public event EventHandler<GenericEventArgs<User>> UserCreated; - - private readonly SemaphoreSlim _userListLock = new SemaphoreSlim(1, 1); - /// <summary> /// Creates the user. /// </summary> @@ -746,7 +726,7 @@ namespace Emby.Server.Implementations.Library /// <returns>User.</returns> /// <exception cref="ArgumentNullException">name</exception> /// <exception cref="ArgumentException"></exception> - public async Task<User> CreateUser(string name) + public User CreateUser(string name) { if (string.IsNullOrWhiteSpace(name)) { @@ -763,28 +743,17 @@ namespace Emby.Server.Implementations.Library throw new ArgumentException(string.Format("A user with the name '{0}' already exists.", name)); } - await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); - - try - { - var user = InstantiateNewUser(name); + var user = InstantiateNewUser(name); - var list = Users.ToList(); - list.Add(user); - _users = list.ToArray(); + _users[user.Id] = user; - user.DateLastSaved = DateTime.UtcNow; + user.DateLastSaved = DateTime.UtcNow; - UserRepository.CreateUser(user); + _userRepository.CreateUser(user); - EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs<User> { Argument = user }, _logger); + EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs<User> { Argument = user }, _logger); - return user; - } - finally - { - _userListLock.Release(); - } + return user; } /// <summary> @@ -794,57 +763,59 @@ namespace Emby.Server.Implementations.Library /// <returns>Task.</returns> /// <exception cref="ArgumentNullException">user</exception> /// <exception cref="ArgumentException"></exception> - public async Task DeleteUser(User user) + public void DeleteUser(User user) { if (user == null) { throw new ArgumentNullException(nameof(user)); } - var allUsers = Users.ToList(); - - if (allUsers.FirstOrDefault(u => u.Id == user.Id) == null) + if (!_users.ContainsKey(user.Id)) { - throw new ArgumentException(string.Format("The user cannot be deleted because there is no user with the Name {0} and Id {1}.", user.Name, user.Id)); + throw new ArgumentException(string.Format( + CultureInfo.InvariantCulture, + "The user cannot be deleted because there is no user with the Name {0} and Id {1}.", + user.Name, + user.Id)); } - if (allUsers.Count == 1) + if (_users.Count == 1) { - throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one user in the system.", user.Name)); + throw new ArgumentException(string.Format( + CultureInfo.InvariantCulture, + "The user '{0}' cannot be deleted because there must be at least one user in the system.", + user.Name)); } - if (user.Policy.IsAdministrator && allUsers.Count(i => i.Policy.IsAdministrator) == 1) + if (user.Policy.IsAdministrator + && Users.Count(i => i.Policy.IsAdministrator) == 1) { - throw new ArgumentException(string.Format("The user '{0}' cannot be deleted because there must be at least one admin user in the system.", user.Name)); + throw new ArgumentException( + string.Format( + CultureInfo.InvariantCulture, + "The user '{0}' cannot be deleted because there must be at least one admin user in the system.", + user.Name), + nameof(user)); } - await _userListLock.WaitAsync(CancellationToken.None).ConfigureAwait(false); + var configPath = GetConfigurationFilePath(user); + + _userRepository.DeleteUser(user); try { - var configPath = GetConfigurationFilePath(user); - - UserRepository.DeleteUser(user); - - try - { - _fileSystem.DeleteFile(configPath); - } - catch (IOException ex) - { - _logger.LogError(ex, "Error deleting file {path}", configPath); - } - - DeleteUserPolicy(user); - - _users = allUsers.Where(i => i.Id != user.Id).ToArray(); - - OnUserDeleted(user); + _fileSystem.DeleteFile(configPath); } - finally + catch (IOException ex) { - _userListLock.Release(); + _logger.LogError(ex, "Error deleting file {path}", configPath); } + + DeleteUserPolicy(user); + + _users.TryRemove(user.Id, out _); + + OnUserDeleted(user); } /// <summary> @@ -901,8 +872,7 @@ namespace Emby.Server.Implementations.Library Name = name, Id = Guid.NewGuid(), DateCreated = DateTime.UtcNow, - DateModified = DateTime.UtcNow, - UsesIdForConfigurationPath = true + DateModified = DateTime.UtcNow }; } @@ -984,7 +954,6 @@ namespace Emby.Server.Implementations.Library }; } - private readonly object _policySyncLock = new object(); public void UpdateUserPolicy(Guid userId, UserPolicy userPolicy) { var user = GetUserById(userId); diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index e9ce682ee..88e2a8fa6 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading; using MediaBrowser.Controller.Channels; @@ -117,7 +118,7 @@ namespace Emby.Server.Implementations.Library if (!query.IncludeHidden) { - list = list.Where(i => !user.Configuration.MyMediaExcludes.Contains(i.Id.ToString("N"))).ToList(); + list = list.Where(i => !user.Configuration.MyMediaExcludes.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))).ToList(); } var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList(); @@ -127,7 +128,7 @@ namespace Emby.Server.Implementations.Library return list .OrderBy(i => { - var index = orders.IndexOf(i.Id.ToString("N")); + var index = orders.IndexOf(i.Id.ToString("N", CultureInfo.InvariantCulture)); if (index == -1) { @@ -136,7 +137,7 @@ namespace Emby.Server.Implementations.Library { if (!view.DisplayParentId.Equals(Guid.Empty)) { - index = orders.IndexOf(view.DisplayParentId.ToString("N")); + index = orders.IndexOf(view.DisplayParentId.ToString("N", CultureInfo.InvariantCulture)); } } } @@ -223,7 +224,7 @@ namespace Emby.Server.Implementations.Library return list; } - private List<BaseItem> GetItemsForLatestItems(User user, LatestItemsQuery request, DtoOptions options) + private IReadOnlyList<BaseItem> GetItemsForLatestItems(User user, LatestItemsQuery request, DtoOptions options) { var parentId = request.ParentId; @@ -235,24 +236,22 @@ namespace Emby.Server.Implementations.Library if (!parentId.Equals(Guid.Empty)) { var parentItem = _libraryManager.GetItemById(parentId); - var parentItemChannel = parentItem as Channel; - if (parentItemChannel != null) + if (parentItem is Channel) { - return _channelManager.GetLatestChannelItemsInternal(new InternalItemsQuery(user) - { - ChannelIds = new[] { parentId }, - IsPlayed = request.IsPlayed, - StartIndex = request.StartIndex, - Limit = request.Limit, - IncludeItemTypes = request.IncludeItemTypes, - EnableTotalRecordCount = false - - - }, CancellationToken.None).Result.Items.ToList(); + return _channelManager.GetLatestChannelItemsInternal( + new InternalItemsQuery(user) + { + ChannelIds = new[] { parentId }, + IsPlayed = request.IsPlayed, + StartIndex = request.StartIndex, + Limit = request.Limit, + IncludeItemTypes = request.IncludeItemTypes, + EnableTotalRecordCount = false + }, + CancellationToken.None).GetAwaiter().GetResult().Items; } - var parent = parentItem as Folder; - if (parent != null) + if (parentItem is Folder parent) { parents.Add(parent); } @@ -269,7 +268,7 @@ namespace Emby.Server.Implementations.Library { parents = _libraryManager.GetUserRootFolder().GetChildren(user, true) .Where(i => i is Folder) - .Where(i => !user.Configuration.LatestItemsExcludes.Contains(i.Id.ToString("N"))) + .Where(i => !user.Configuration.LatestItemsExcludes.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))) .ToList(); } diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs index 294348660..b584cc649 100644 --- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -91,7 +92,7 @@ namespace Emby.Server.Implementations.Library.Validators continue; } - _logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N"), item.Name, item.GetType().Name); + _logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name); _libraryManager.DeleteItem(item, new DeleteOptions { diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs index 7899cf01b..d00c6cde1 100644 --- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs @@ -1,7 +1,7 @@ using System; +using System.Globalization; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -96,7 +96,7 @@ namespace Emby.Server.Implementations.Library.Validators foreach (var item in deadEntities) { - _logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N"), item.Name, item.GetType().Name); + _logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name); _libraryManager.DeleteItem(item, new DeleteOptions { diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs index da4645a11..93ded9e7b 100644 --- a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Entities; @@ -76,7 +77,7 @@ namespace Emby.Server.Implementations.Library.Validators foreach (var item in deadEntities) { - _logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N"), item.Name, item.GetType().Name); + _logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name); _libraryManager.DeleteItem(item, new DeleteOptions { diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 7b210d231..da0013f12 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -102,7 +102,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _streamHelper = streamHelper; _seriesTimerProvider = new SeriesTimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers.json")); - _timerProvider = new TimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "timers.json"), _logger); + _timerProvider = new TimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "timers.json")); _timerProvider.TimerFired += _timerProvider_TimerFired; _config.NamedConfigurationUpdated += _config_NamedConfigurationUpdated; @@ -681,7 +681,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - timer.Id = Guid.NewGuid().ToString("N"); + timer.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); LiveTvProgram programInfo = null; @@ -713,7 +713,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public async Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken) { - info.Id = Guid.NewGuid().ToString("N"); + info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); // populate info.seriesID var program = GetProgramInfoFromCache(info.ProgramId); @@ -1059,7 +1059,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var json = _jsonSerializer.SerializeToString(mediaSource); mediaSource = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json); - mediaSource.Id = Guid.NewGuid().ToString("N") + "_" + mediaSource.Id; + mediaSource.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture) + "_" + mediaSource.Id; //if (mediaSource.DateLiveStreamOpened.HasValue && enableStreamSharing) //{ @@ -2529,7 +2529,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var timer = new TimerInfo { ChannelId = channelId, - Id = (seriesTimer.Id + parent.ExternalId).GetMD5().ToString("N"), + Id = (seriesTimer.Id + parent.ExternalId).GetMD5().ToString("N", CultureInfo.InvariantCulture), StartDate = parent.StartDate, EndDate = parent.EndDate.Value, ProgramId = parent.ExternalId, diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 9a9bae215..cc9c8e5d2 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -87,8 +87,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV CreateNoWindow = true, UseShellExecute = false, - // Must consume both stdout and stderr or deadlocks may occur - //RedirectStandardOutput = true, RedirectStandardError = true, RedirectStandardInput = true, @@ -120,9 +118,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV cancellationToken.Register(Stop); - // MUST read both stdout and stderr asynchronously or a deadlock may occurr - //process.BeginOutputReadLine(); - onStarted(); // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback @@ -138,11 +133,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV string videoArgs; if (EncodeVideo(mediaSource)) { - var maxBitrate = 25000000; + const int MaxBitrate = 25000000; videoArgs = string.Format( - "-codec:v:0 libx264 -force_key_frames \"expr:gte(t,n_forced*5)\" {0} -pix_fmt yuv420p -preset superfast -crf 23 -b:v {1} -maxrate {1} -bufsize ({1}*2) -vsync -1 -profile:v high -level 41", - GetOutputSizeParam(), - maxBitrate.ToString(CultureInfo.InvariantCulture)); + CultureInfo.InvariantCulture, + "-codec:v:0 libx264 -force_key_frames \"expr:gte(t,n_forced*5)\" {0} -pix_fmt yuv420p -preset superfast -crf 23 -b:v {1} -maxrate {1} -bufsize ({1}*2) -vsync -1 -profile:v high -level 41", + GetOutputSizeParam(), + MaxBitrate); } else { @@ -151,18 +147,17 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV videoArgs += " -fflags +genpts"; - var durationParam = " -t " + _mediaEncoder.GetTimeParameter(duration.Ticks); - durationParam = string.Empty; - var flags = new List<string>(); if (mediaSource.IgnoreDts) { flags.Add("+igndts"); } + if (mediaSource.IgnoreIndex) { flags.Add("+ignidx"); } + if (mediaSource.GenPtsInput) { flags.Add("+genpts"); @@ -172,11 +167,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (flags.Count > 0) { - inputModifier += " -fflags " + string.Join("", flags.ToArray()); + inputModifier += " -fflags " + string.Join(string.Empty, flags); } - var videoStream = mediaSource.VideoStream; - if (mediaSource.ReadAtNativeFramerate) { inputModifier += " -re"; @@ -200,13 +193,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var outputParam = string.Empty; - var commandLineArgs = string.Format("-i \"{0}\"{5} {2} -map_metadata -1 -threads 0 {3}{4}{6} -y \"{1}\"", + var commandLineArgs = string.Format( + CultureInfo.InvariantCulture, + "-i \"{0}\" {2} -map_metadata -1 -threads 0 {3}{4}{5} -y \"{1}\"", inputTempFile, targetFile, videoArgs, GetAudioArgs(mediaSource), subtitleArgs, - durationParam, outputParam); return inputModifier + " " + commandLineArgs; @@ -214,9 +208,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private static string GetAudioArgs(MediaSourceInfo mediaSource) { - var mediaStreams = mediaSource.MediaStreams ?? new List<MediaStream>(); - var inputAudioCodec = mediaStreams.Where(i => i.Type == MediaStreamType.Audio).Select(i => i.Codec).FirstOrDefault() ?? string.Empty; - return "-codec:a:0 copy"; //var audioChannels = 2; @@ -257,7 +248,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { _logger.LogInformation("Stopping ffmpeg recording process for {path}", _targetPath); - //process.Kill(); _process.StandardInput.WriteLine("q"); } catch (Exception ex) @@ -309,44 +299,26 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { _hasExited = true; - DisposeLogStream(); + _logFileStream?.Dispose(); + _logFileStream = null; - try - { - 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("Recording for {path} failed. Exit code {ExitCode}", _targetPath, exitCode))); - } - } - catch + if (exitCode == 0) { - _logger.LogError("FFMpeg recording exited with an error for {path}.", _targetPath); - _taskCompletionSource.TrySetException(new Exception(string.Format("Recording for {path} failed", _targetPath))); + _taskCompletionSource.TrySetResult(true); } - } - - private void DisposeLogStream() - { - if (_logFileStream != null) + else { - try - { - _logFileStream.Dispose(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error disposing recording log stream"); - } - - _logFileStream = null; + _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/ItemDataProvider.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs index 9c45ee36a..9055a70a6 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs @@ -10,67 +10,64 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public class ItemDataProvider<T> where T : class { - private readonly object _fileDataLock = new object(); - private List<T> _items; private readonly IJsonSerializer _jsonSerializer; - protected readonly ILogger Logger; private readonly string _dataPath; - protected readonly Func<T, T, bool> EqualityComparer; + private readonly object _fileDataLock = new object(); + private T[] _items; - public ItemDataProvider(IJsonSerializer jsonSerializer, ILogger logger, string dataPath, Func<T, T, bool> equalityComparer) + public ItemDataProvider( + IJsonSerializer jsonSerializer, + ILogger logger, + string dataPath, + Func<T, T, bool> equalityComparer) { + _jsonSerializer = jsonSerializer; Logger = logger; _dataPath = dataPath; EqualityComparer = equalityComparer; - _jsonSerializer = jsonSerializer; } - public IReadOnlyList<T> GetAll() - { - lock (_fileDataLock) - { - if (_items == null) - { - if (!File.Exists(_dataPath)) - { - return new List<T>(); - } - - Logger.LogInformation("Loading live tv data from {0}", _dataPath); - _items = GetItemsFromFile(_dataPath); - } + protected ILogger Logger { get; } - return _items.ToList(); - } - } + protected Func<T, T, bool> EqualityComparer { get; } - private List<T> GetItemsFromFile(string path) + private void EnsureLoaded() { - try + if (_items != null) { - return _jsonSerializer.DeserializeFromFile<List<T>>(path); + return; } - catch (Exception ex) + + if (File.Exists(_dataPath)) { - Logger.LogError(ex, "Error deserializing {Path}", path); + Logger.LogInformation("Loading live tv data from {Path}", _dataPath); + + try + { + _items = _jsonSerializer.DeserializeFromFile<T[]>(_dataPath); + return; + } + catch (Exception ex) + { + Logger.LogError(ex, "Error deserializing {Path}", _dataPath); + } } - return new List<T>(); + _items = Array.Empty<T>(); } - private void UpdateList(List<T> newList) + private void SaveList() { - if (newList == null) - { - throw new ArgumentNullException(nameof(newList)); - } - Directory.CreateDirectory(Path.GetDirectoryName(_dataPath)); + _jsonSerializer.SerializeToFile(_items, _dataPath); + } + public IReadOnlyList<T> GetAll() + { lock (_fileDataLock) { - _jsonSerializer.SerializeToFile(newList, _dataPath); - _items = newList; + EnsureLoaded(); + return (T[])_items.Clone(); } } @@ -81,18 +78,20 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV throw new ArgumentNullException(nameof(item)); } - var list = GetAll().ToList(); - - var index = list.FindIndex(i => EqualityComparer(i, item)); - - if (index == -1) + lock (_fileDataLock) { - throw new ArgumentException("item not found"); - } + EnsureLoaded(); - list[index] = item; + var index = Array.FindIndex(_items, i => EqualityComparer(i, item)); + if (index == -1) + { + throw new ArgumentException("item not found"); + } - UpdateList(list); + _items[index] = item; + + SaveList(); + } } public virtual void Add(T item) @@ -102,37 +101,58 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV throw new ArgumentNullException(nameof(item)); } - var list = GetAll().ToList(); - - if (list.Any(i => EqualityComparer(i, item))) + lock (_fileDataLock) { - throw new ArgumentException("item already exists"); - } + EnsureLoaded(); - list.Add(item); + if (_items.Any(i => EqualityComparer(i, item))) + { + throw new ArgumentException("item already exists", nameof(item)); + } - UpdateList(list); + int oldLen = _items.Length; + var newList = new T[oldLen + 1]; + _items.CopyTo(newList, 0); + newList[oldLen] = item; + _items = newList; + + SaveList(); + } } - public void AddOrUpdate(T item) + public virtual void AddOrUpdate(T item) { - var list = GetAll().ToList(); - - if (!list.Any(i => EqualityComparer(i, item))) - { - Add(item); - } - else + lock (_fileDataLock) { - Update(item); + EnsureLoaded(); + + int index = Array.FindIndex(_items, i => EqualityComparer(i, item)); + if (index == -1) + { + int oldLen = _items.Length; + var newList = new T[oldLen + 1]; + _items.CopyTo(newList, 0); + newList[oldLen] = item; + _items = newList; + } + else + { + _items[index] = item; + } + + SaveList(); } } public virtual void Delete(T item) { - var list = GetAll().Where(i => !EqualityComparer(i, item)).ToList(); + lock (_fileDataLock) + { + EnsureLoaded(); + _items = _items.Where(i => !EqualityComparer(i, item)).ToArray(); - UpdateList(list); + SaveList(); + } } } } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs index 3c807a8ea..d09b281d4 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs @@ -14,21 +14,19 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public class TimerManager : ItemDataProvider<TimerInfo> { private readonly ConcurrentDictionary<string, Timer> _timers = new ConcurrentDictionary<string, Timer>(StringComparer.OrdinalIgnoreCase); - private readonly ILogger _logger; - public event EventHandler<GenericEventArgs<TimerInfo>> TimerFired; - - public TimerManager(IJsonSerializer jsonSerializer, ILogger logger, string dataPath, ILogger logger1) + public TimerManager(IJsonSerializer jsonSerializer, ILogger logger, string dataPath) : base(jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase)) { - _logger = logger1; } + public event EventHandler<GenericEventArgs<TimerInfo>> TimerFired; + public void RestartTimers() { StopTimers(); - foreach (var item in GetAll().ToList()) + foreach (var item in GetAll()) { AddOrUpdateSystemTimer(item); } @@ -64,16 +62,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return; } - var list = GetAll().ToList(); + base.AddOrUpdate(item); + } - if (!list.Any(i => EqualityComparer(i, item))) - { - base.Add(item); - } - else - { - base.Update(item); - } + public override void AddOrUpdate(TimerInfo item) + { + base.AddOrUpdate(item); + AddOrUpdateSystemTimer(item); } public override void Add(TimerInfo item) @@ -89,8 +84,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private static bool ShouldStartTimer(TimerInfo item) { - if (item.Status == RecordingStatus.Completed || - item.Status == RecordingStatus.Cancelled) + if (item.Status == RecordingStatus.Completed + || item.Status == RecordingStatus.Cancelled) { return false; } @@ -126,12 +121,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (_timers.TryAdd(item.Id, timer)) { - _logger.LogInformation("Creating recording timer for {id}, {name}. Timer will fire in {minutes} minutes", item.Id, item.Name, dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture)); + Logger.LogInformation( + "Creating recording timer for {Id}, {Name}. Timer will fire in {Minutes} minutes", + item.Id, + item.Name, + dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture)); } else { timer.Dispose(); - _logger.LogWarning("Timer already exists for item {id}", item.Id); + Logger.LogWarning("Timer already exists for item {Id}", item.Id); } } diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index f5dffc22a..9a4c91d0b 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -17,7 +17,6 @@ using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Net; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.LiveTv.Listings { @@ -41,6 +40,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings private string UserAgent => _appHost.ApplicationUserAgent; + /// <inheritdoc /> + public string Name => "Schedules Direct"; + + /// <inheritdoc /> + public string Type => nameof(SchedulesDirect); + private static List<string> GetScheduleRequestDates(DateTime startDateUtc, DateTime endDateUtc) { var dates = new List<string>(); @@ -103,7 +108,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings httpOptions.RequestHeaders["token"] = token; using (var response = await Post(httpOptions, true, info).ConfigureAwait(false)) - using (var reader = new StreamReader(response.Content)) { var dailySchedules = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Day>>(response.Content).ConfigureAwait(false); _logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId); @@ -122,7 +126,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings httpOptions.RequestContent = "[\"" + string.Join("\", \"", programsID) + "\"]"; using (var innerResponse = await Post(httpOptions, true, info).ConfigureAwait(false)) - using (var innerReader = new StreamReader(innerResponse.Content)) { var programDetails = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ProgramDetails>>(innerResponse.Content).ConfigureAwait(false); var programDict = programDetails.ToDictionary(p => p.programID, y => y); @@ -152,14 +155,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings var imagesWithText = allImages.Where(i => string.Equals(i.text, "yes", StringComparison.OrdinalIgnoreCase)); var imagesWithoutText = allImages.Where(i => string.Equals(i.text, "no", StringComparison.OrdinalIgnoreCase)); - const double desiredAspect = 0.666666667; + const double DesiredAspect = 2.0 / 3; - programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, desiredAspect) ?? - GetProgramImage(ApiUrl, allImages, true, desiredAspect); + programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ?? + GetProgramImage(ApiUrl, allImages, true, DesiredAspect); - const double wideAspect = 1.77777778; + const double WideAspect = 16.0 / 9; - programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, wideAspect); + programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect); // Don't supply the same image twice if (string.Equals(programEntry.primaryImage, programEntry.thumbImage, StringComparison.Ordinal)) @@ -167,7 +170,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings programEntry.thumbImage = null; } - programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, wideAspect); + programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect); //programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ?? // GetProgramImage(ApiUrl, data, "Banner-L1", false) ?? @@ -178,6 +181,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.programID])); } + return programsInfo; } } @@ -185,12 +189,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings private static int GetSizeOrder(ScheduleDirect.ImageData image) { - if (!string.IsNullOrWhiteSpace(image.height)) + if (!string.IsNullOrWhiteSpace(image.height) + && int.TryParse(image.height, out int value)) { - if (int.TryParse(image.height, out int value)) - { - return value; - } + return value; } return 0; @@ -736,16 +738,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings httpOptions.RequestHeaders["token"] = token; - using (var response = await _httpClient.SendAsync(httpOptions, "PUT")) + using (await _httpClient.SendAsync(httpOptions, "PUT")) { } } - public string Name => "Schedules Direct"; - - public static string TypeName = "SchedulesDirect"; - public string Type => TypeName; - private async Task<bool> HasLineup(ListingsProviderInfo info, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(info.ListingsId)) diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs index 94225a0aa..88693f22a 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -211,7 +211,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings HasImage = program.Icon != null && !string.IsNullOrEmpty(program.Icon.Source), OfficialRating = program.Rating != null && !string.IsNullOrEmpty(program.Rating.Value) ? program.Rating.Value : null, CommunityRating = program.StarRating, - SeriesId = program.Episode == null ? null : program.Title.GetMD5().ToString("N") + SeriesId = program.Episode == null ? null : program.Title.GetMD5().ToString("N", CultureInfo.InvariantCulture) }; if (string.IsNullOrWhiteSpace(program.ProgramId)) @@ -227,7 +227,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings uniqueString = "-" + programInfo.EpisodeNumber.Value.ToString(CultureInfo.InvariantCulture); } - programInfo.ShowId = uniqueString.GetMD5().ToString("N"); + programInfo.ShowId = uniqueString.GetMD5().ToString("N", CultureInfo.InvariantCulture); // If we don't have valid episode info, assume it's a unique program, otherwise recordings might be skipped if (programInfo.IsSeries diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs index 1144c9ab1..e584664c9 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -52,7 +53,7 @@ namespace Emby.Server.Implementations.LiveTv ExternalId = info.Id, ChannelId = GetInternalChannelId(service.Name, info.ChannelId), Status = info.Status, - SeriesTimerId = string.IsNullOrEmpty(info.SeriesTimerId) ? null : GetInternalSeriesTimerId(info.SeriesTimerId).ToString("N"), + SeriesTimerId = string.IsNullOrEmpty(info.SeriesTimerId) ? null : GetInternalSeriesTimerId(info.SeriesTimerId).ToString("N", CultureInfo.InvariantCulture), PrePaddingSeconds = info.PrePaddingSeconds, PostPaddingSeconds = info.PostPaddingSeconds, IsPostPaddingRequired = info.IsPostPaddingRequired, @@ -69,7 +70,7 @@ namespace Emby.Server.Implementations.LiveTv if (!string.IsNullOrEmpty(info.ProgramId)) { - dto.ProgramId = GetInternalProgramId(info.ProgramId).ToString("N"); + dto.ProgramId = GetInternalProgramId(info.ProgramId).ToString("N", CultureInfo.InvariantCulture); } if (program != null) @@ -107,7 +108,7 @@ namespace Emby.Server.Implementations.LiveTv { var dto = new SeriesTimerInfoDto { - Id = GetInternalSeriesTimerId(info.Id).ToString("N"), + Id = GetInternalSeriesTimerId(info.Id).ToString("N", CultureInfo.InvariantCulture), Overview = info.Overview, EndDate = info.EndDate, Name = info.Name, @@ -139,7 +140,7 @@ namespace Emby.Server.Implementations.LiveTv if (!string.IsNullOrEmpty(info.ProgramId)) { - dto.ProgramId = GetInternalProgramId(info.ProgramId).ToString("N"); + dto.ProgramId = GetInternalProgramId(info.ProgramId).ToString("N", CultureInfo.InvariantCulture); } dto.DayPattern = info.Days == null ? null : GetDayPattern(info.Days.ToArray()); @@ -169,7 +170,7 @@ namespace Emby.Server.Implementations.LiveTv try { dto.ParentThumbImageTag = _imageProcessor.GetImageCacheTag(librarySeries, image); - dto.ParentThumbItemId = librarySeries.Id.ToString("N"); + dto.ParentThumbItemId = librarySeries.Id.ToString("N", CultureInfo.InvariantCulture); } catch (Exception ex) { @@ -185,7 +186,7 @@ namespace Emby.Server.Implementations.LiveTv { _imageProcessor.GetImageCacheTag(librarySeries, image) }; - dto.ParentBackdropItemId = librarySeries.Id.ToString("N"); + dto.ParentBackdropItemId = librarySeries.Id.ToString("N", CultureInfo.InvariantCulture); } catch (Exception ex) { @@ -213,7 +214,7 @@ namespace Emby.Server.Implementations.LiveTv try { dto.ParentPrimaryImageTag = _imageProcessor.GetImageCacheTag(program, image); - dto.ParentPrimaryImageItemId = program.Id.ToString("N"); + dto.ParentPrimaryImageItemId = program.Id.ToString("N", CultureInfo.InvariantCulture); } catch (Exception ex) { @@ -232,7 +233,7 @@ namespace Emby.Server.Implementations.LiveTv { _imageProcessor.GetImageCacheTag(program, image) }; - dto.ParentBackdropItemId = program.Id.ToString("N"); + dto.ParentBackdropItemId = program.Id.ToString("N", CultureInfo.InvariantCulture); } catch (Exception ex) { @@ -263,7 +264,7 @@ namespace Emby.Server.Implementations.LiveTv try { dto.ParentThumbImageTag = _imageProcessor.GetImageCacheTag(librarySeries, image); - dto.ParentThumbItemId = librarySeries.Id.ToString("N"); + dto.ParentThumbItemId = librarySeries.Id.ToString("N", CultureInfo.InvariantCulture); } catch (Exception ex) { @@ -279,7 +280,7 @@ namespace Emby.Server.Implementations.LiveTv { _imageProcessor.GetImageCacheTag(librarySeries, image) }; - dto.ParentBackdropItemId = librarySeries.Id.ToString("N"); + dto.ParentBackdropItemId = librarySeries.Id.ToString("N", CultureInfo.InvariantCulture); } catch (Exception ex) { @@ -320,7 +321,7 @@ namespace Emby.Server.Implementations.LiveTv try { dto.ParentPrimaryImageTag = _imageProcessor.GetImageCacheTag(program, image); - dto.ParentPrimaryImageItemId = program.Id.ToString("N"); + dto.ParentPrimaryImageItemId = program.Id.ToString("N", CultureInfo.InvariantCulture); } catch (Exception ex) { @@ -339,7 +340,7 @@ namespace Emby.Server.Implementations.LiveTv { _imageProcessor.GetImageCacheTag(program, image) }; - dto.ParentBackdropItemId = program.Id.ToString("N"); + dto.ParentBackdropItemId = program.Id.ToString("N", CultureInfo.InvariantCulture); } catch (Exception ex) { @@ -407,7 +408,7 @@ namespace Emby.Server.Implementations.LiveTv { var name = ServiceName + externalId + InternalVersionNumber; - return name.ToLowerInvariant().GetMD5().ToString("N"); + return name.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture); } public Guid GetInternalSeriesTimerId(string externalId) diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index 9093d9740..89b92c999 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -59,16 +60,6 @@ namespace Emby.Server.Implementations.LiveTv private IListingsProvider[] _listingProviders = Array.Empty<IListingsProvider>(); private readonly IFileSystem _fileSystem; - public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled; - public event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled; - public event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCreated; - public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCreated; - - public string GetEmbyTvActiveRecordingPath(string id) - { - return EmbyTV.EmbyTV.Current.GetActiveRecordingPath(id); - } - public LiveTvManager( IServerApplicationHost appHost, IServerConfigurationManager config, @@ -101,17 +92,34 @@ namespace Emby.Server.Implementations.LiveTv _tvDtoService = new LiveTvDtoService(dtoService, imageProcessor, loggerFactory, appHost, _libraryManager); } + public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled; + + public event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCancelled; + + public event EventHandler<GenericEventArgs<TimerEventInfo>> TimerCreated; + + public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCreated; + /// <summary> /// Gets the services. /// </summary> /// <value>The services.</value> public IReadOnlyList<ILiveTvService> Services => _services; + public ITunerHost[] TunerHosts => _tunerHosts; + + public IListingsProvider[] ListingProviders => _listingProviders; + private LiveTvOptions GetConfiguration() { return _config.GetConfiguration<LiveTvOptions>("livetv"); } + public string GetEmbyTvActiveRecordingPath(string id) + { + return EmbyTV.EmbyTV.Current.GetActiveRecordingPath(id); + } + /// <summary> /// Adds the parts. /// </summary> @@ -129,13 +137,13 @@ namespace Emby.Server.Implementations.LiveTv { if (service is EmbyTV.EmbyTV embyTv) { - embyTv.TimerCreated += EmbyTv_TimerCreated; - embyTv.TimerCancelled += EmbyTv_TimerCancelled; + embyTv.TimerCreated += OnEmbyTvTimerCreated; + embyTv.TimerCancelled += OnEmbyTvTimerCancelled; } } } - private void EmbyTv_TimerCancelled(object sender, GenericEventArgs<string> e) + private void OnEmbyTvTimerCancelled(object sender, GenericEventArgs<string> e) { var timerId = e.Argument; @@ -148,10 +156,9 @@ namespace Emby.Server.Implementations.LiveTv }); } - private void EmbyTv_TimerCreated(object sender, GenericEventArgs<TimerInfo> e) + private void OnEmbyTvTimerCreated(object sender, GenericEventArgs<TimerInfo> e) { var timer = e.Argument; - var service = sender as ILiveTvService; TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo> { @@ -163,10 +170,6 @@ namespace Emby.Server.Implementations.LiveTv }); } - public ITunerHost[] TunerHosts => _tunerHosts; - - public IListingsProvider[] ListingProviders => _listingProviders; - public List<NameIdPair> GetTunerHostTypes() { return _tunerHosts.OrderBy(i => i.Name).Select(i => new NameIdPair @@ -258,7 +261,7 @@ namespace Emby.Server.Implementations.LiveTv } info.RequiresClosing = true; - var idPrefix = service.GetType().FullName.GetMD5().ToString("N") + "_"; + var idPrefix = service.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + "_"; info.LiveStreamId = idPrefix + info.Id; @@ -820,7 +823,7 @@ namespace Emby.Server.Implementations.LiveTv if (!string.IsNullOrWhiteSpace(query.SeriesTimerId)) { var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery { }, cancellationToken).ConfigureAwait(false); - var seriesTimer = seriesTimers.Items.FirstOrDefault(i => string.Equals(_tvDtoService.GetInternalSeriesTimerId(i.Id).ToString("N"), query.SeriesTimerId, StringComparison.OrdinalIgnoreCase)); + var seriesTimer = seriesTimers.Items.FirstOrDefault(i => string.Equals(_tvDtoService.GetInternalSeriesTimerId(i.Id).ToString("N", CultureInfo.InvariantCulture), query.SeriesTimerId, StringComparison.OrdinalIgnoreCase)); if (seriesTimer != null) { internalQuery.ExternalSeriesId = seriesTimer.SeriesId; @@ -880,7 +883,7 @@ namespace Emby.Server.Implementations.LiveTv } var programList = _libraryManager.QueryItems(internalQuery).Items; - var totalCount = programList.Length; + var totalCount = programList.Count; var orderedPrograms = programList.Cast<LiveTvProgram>().OrderBy(i => i.StartDate.Date); @@ -965,11 +968,8 @@ namespace Emby.Server.Implementations.LiveTv private async Task AddRecordingInfo(IEnumerable<Tuple<BaseItemDto, string, string>> programs, CancellationToken cancellationToken) { - var timers = new Dictionary<string, List<TimerInfo>>(); - var seriesTimers = new Dictionary<string, List<SeriesTimerInfo>>(); - - TimerInfo[] timerList = null; - SeriesTimerInfo[] seriesTimerList = null; + IReadOnlyList<TimerInfo> timerList = null; + IReadOnlyList<SeriesTimerInfo> seriesTimerList = null; foreach (var programTuple in programs) { @@ -997,7 +997,7 @@ namespace Emby.Server.Implementations.LiveTv if (!string.IsNullOrEmpty(timer.SeriesTimerId)) { program.SeriesTimerId = _tvDtoService.GetInternalSeriesTimerId(timer.SeriesTimerId) - .ToString("N"); + .ToString("N", CultureInfo.InvariantCulture); foundSeriesTimer = true; } @@ -1018,7 +1018,7 @@ namespace Emby.Server.Implementations.LiveTv if (seriesTimer != null) { program.SeriesTimerId = _tvDtoService.GetInternalSeriesTimerId(seriesTimer.Id) - .ToString("N"); + .ToString("N", CultureInfo.InvariantCulture); } } } @@ -1295,6 +1295,7 @@ namespace Emby.Server.Implementations.LiveTv } private const int MaxGuideDays = 14; + private double GetGuideDays() { var config = GetConfiguration(); @@ -1339,6 +1340,7 @@ namespace Emby.Server.Implementations.LiveTv excludeItemTypes.Add(typeof(Movie).Name); } } + if (query.IsSeries.HasValue) { if (query.IsSeries.Value) @@ -1350,10 +1352,12 @@ namespace Emby.Server.Implementations.LiveTv excludeItemTypes.Add(typeof(Episode).Name); } } + if (query.IsSports ?? false) { genres.Add("Sports"); } + if (query.IsKids ?? false) { genres.Add("Kids"); @@ -1399,20 +1403,20 @@ namespace Emby.Server.Implementations.LiveTv if (query.IsInProgress ?? false) { - //TODO Fix The co-variant conversion between Video[] and BaseItem[], this can generate runtime issues. + // TODO: Fix The co-variant conversion between Video[] and BaseItem[], this can generate runtime issues. result.Items = result .Items .OfType<Video>() .Where(i => !i.IsCompleteMedia) .ToArray(); - result.TotalRecordCount = result.Items.Length; + result.TotalRecordCount = result.Items.Count; } return result; } - public Task AddInfoToProgramDto(List<Tuple<BaseItem, BaseItemDto>> tuples, ItemFields[] fields, User user = null) + public Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> tuples, ItemFields[] fields, User user = null) { var programTuples = new List<Tuple<BaseItemDto, string, string>>(); var hasChannelImage = fields.Contains(ItemFields.ChannelImage); @@ -1472,7 +1476,7 @@ namespace Emby.Server.Implementations.LiveTv dto.SeriesTimerId = string.IsNullOrEmpty(info.SeriesTimerId) ? null - : _tvDtoService.GetInternalSeriesTimerId(info.SeriesTimerId).ToString("N"); + : _tvDtoService.GetInternalSeriesTimerId(info.SeriesTimerId).ToString("N", CultureInfo.InvariantCulture); dto.TimerId = string.IsNullOrEmpty(info.Id) ? null @@ -1596,8 +1600,6 @@ namespace Emby.Server.Implementations.LiveTv if (!string.IsNullOrEmpty(query.Id)) { - var guid = new Guid(query.Id); - timers = timers .Where(i => string.Equals(_tvDtoService.GetInternalTimerId(i.Item1.Id), query.Id, StringComparison.OrdinalIgnoreCase)); } @@ -1876,7 +1878,7 @@ namespace Emby.Server.Implementations.LiveTv return _libraryManager.GetItemById(internalChannelId); } - public void AddChannelInfo(List<Tuple<BaseItemDto, LiveTvChannel>> tuples, DtoOptions options, User user) + public void AddChannelInfo(IReadOnlyCollection<(BaseItemDto, LiveTvChannel)> tuples, DtoOptions options, User user) { var now = DateTime.UtcNow; @@ -2027,7 +2029,7 @@ namespace Emby.Server.Implementations.LiveTv info.StartDate = program.StartDate; info.Name = program.Name; info.Overview = program.Overview; - info.ProgramId = programDto.Id.ToString("N"); + info.ProgramId = programDto.Id.ToString("N", CultureInfo.InvariantCulture); info.ExternalProgramId = program.ExternalId; if (program.EndDate.HasValue) @@ -2088,7 +2090,7 @@ namespace Emby.Server.Implementations.LiveTv if (service is ISupportsNewTimerIds supportsNewTimerIds) { newTimerId = await supportsNewTimerIds.CreateSeriesTimer(info, cancellationToken).ConfigureAwait(false); - newTimerId = _tvDtoService.GetInternalSeriesTimerId(newTimerId).ToString("N"); + newTimerId = _tvDtoService.GetInternalSeriesTimerId(newTimerId).ToString("N", CultureInfo.InvariantCulture); } else { @@ -2192,7 +2194,7 @@ namespace Emby.Server.Implementations.LiveTv info.EnabledUsers = _userManager.Users .Where(IsLiveTvEnabled) - .Select(i => i.Id.ToString("N")) + .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) .ToArray(); return info; @@ -2219,7 +2221,7 @@ namespace Emby.Server.Implementations.LiveTv { var parts = id.Split(new[] { '_' }, 2); - var service = _services.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N"), parts[0], StringComparison.OrdinalIgnoreCase)); + var service = _services.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), parts[0], StringComparison.OrdinalIgnoreCase)); if (service == null) { @@ -2269,7 +2271,7 @@ namespace Emby.Server.Implementations.LiveTv if (index == -1 || string.IsNullOrWhiteSpace(info.Id)) { - info.Id = Guid.NewGuid().ToString("N"); + info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); list.Add(info); config.TunerHosts = list.ToArray(); } @@ -2312,7 +2314,7 @@ namespace Emby.Server.Implementations.LiveTv if (index == -1 || string.IsNullOrWhiteSpace(info.Id)) { - info.Id = Guid.NewGuid().ToString("N"); + info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); list.Add(info); config.ListingProviders = list.ToArray(); } diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs index cd1731de5..52d60c004 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -101,7 +102,7 @@ namespace Emby.Server.Implementations.LiveTv { var openKeys = new List<string>(); openKeys.Add(item.GetType().Name); - openKeys.Add(item.Id.ToString("N")); + openKeys.Add(item.Id.ToString("N", CultureInfo.InvariantCulture)); openKeys.Add(source.Id ?? string.Empty); source.OpenToken = string.Join(StreamIdDelimeterString, openKeys.ToArray()); } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index ed524cae3..da98f3e58 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Net; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; @@ -30,6 +32,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun private readonly IServerApplicationHost _appHost; private readonly ISocketFactory _socketFactory; private readonly INetworkManager _networkManager; + private readonly IStreamHelper _streamHelper; public HdHomerunHost( IServerConfigurationManager config, @@ -39,29 +42,25 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun IHttpClient httpClient, IServerApplicationHost appHost, ISocketFactory socketFactory, - INetworkManager networkManager) + INetworkManager networkManager, + IStreamHelper streamHelper) : base(config, logger, jsonSerializer, fileSystem) { _httpClient = httpClient; _appHost = appHost; _socketFactory = socketFactory; _networkManager = networkManager; + _streamHelper = streamHelper; } public string Name => "HD Homerun"; - public override string Type => DeviceType; - - public static string DeviceType => "hdhomerun"; + public override string Type => "hdhomerun"; protected override string ChannelIdPrefix => "hdhr_"; private string GetChannelId(TunerHostInfo info, Channels i) - { - var id = ChannelIdPrefix + i.GuideNumber; - - return id; - } + => ChannelIdPrefix + i.GuideNumber; private async Task<List<Channels>> GetLineup(TunerHostInfo info, CancellationToken cancellationToken) { @@ -73,19 +72,18 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun CancellationToken = cancellationToken, BufferContent = false }; - using (var response = await _httpClient.SendAsync(options, "GET").ConfigureAwait(false)) - { - using (var stream = response.Content) - { - var lineup = await JsonSerializer.DeserializeFromStreamAsync<List<Channels>>(stream).ConfigureAwait(false) ?? new List<Channels>(); - if (info.ImportFavoritesOnly) - { - lineup = lineup.Where(i => i.Favorite).ToList(); - } + using (var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false)) + using (var stream = response.Content) + { + var lineup = await JsonSerializer.DeserializeFromStreamAsync<List<Channels>>(stream).ConfigureAwait(false) ?? new List<Channels>(); - return lineup.Where(i => !i.DRM).ToList(); + if (info.ImportFavoritesOnly) + { + lineup = lineup.Where(i => i.Favorite).ToList(); } + + return lineup.Where(i => !i.DRM).ToList(); } } @@ -138,23 +136,20 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun Url = string.Format("{0}/discover.json", GetApiUrl(info)), CancellationToken = cancellationToken, BufferContent = false - - }, "GET").ConfigureAwait(false)) + }, HttpMethod.Get).ConfigureAwait(false)) + using (var stream = response.Content) { - using (var stream = response.Content) - { - var discoverResponse = await JsonSerializer.DeserializeFromStreamAsync<DiscoverResponse>(stream).ConfigureAwait(false); + var discoverResponse = await JsonSerializer.DeserializeFromStreamAsync<DiscoverResponse>(stream).ConfigureAwait(false); - if (!string.IsNullOrEmpty(cacheKey)) + if (!string.IsNullOrEmpty(cacheKey)) + { + lock (_modelCache) { - lock (_modelCache) - { - _modelCache[cacheKey] = discoverResponse; - } + _modelCache[cacheKey] = discoverResponse; } - - return discoverResponse; } + + return discoverResponse; } } catch (HttpException ex) @@ -185,36 +180,36 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); - using (var stream = await _httpClient.Get(new HttpRequestOptions() + using (var response = await _httpClient.SendAsync(new HttpRequestOptions() { Url = string.Format("{0}/tuners.html", GetApiUrl(info)), CancellationToken = cancellationToken, BufferContent = false - })) + }, HttpMethod.Get)) + using (var stream = response.Content) + using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8)) { var tuners = new List<LiveTvTunerInfo>(); - using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8)) + while (!sr.EndOfStream) { - while (!sr.EndOfStream) + string line = StripXML(sr.ReadLine()); + if (line.Contains("Channel")) { - string line = StripXML(sr.ReadLine()); - if (line.Contains("Channel")) + LiveTvTunerStatus status; + var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); + var name = line.Substring(0, index - 1); + var currentChannel = line.Substring(index + 7); + if (currentChannel != "none") { status = LiveTvTunerStatus.LiveTv; } else { status = LiveTvTunerStatus.Available; } + tuners.Add(new LiveTvTunerInfo { - LiveTvTunerStatus status; - var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); - var name = line.Substring(0, index - 1); - var currentChannel = line.Substring(index + 7); - if (currentChannel != "none") { status = LiveTvTunerStatus.LiveTv; } else { status = LiveTvTunerStatus.Available; } - tuners.Add(new LiveTvTunerInfo - { - Name = name, - SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber, - ProgramName = currentChannel, - Status = status - }); - } + Name = name, + SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber, + ProgramName = currentChannel, + Status = status + }); } } + return tuners; } } @@ -244,6 +239,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun bufferIndex++; } } + return new string(buffer, 0, bufferIndex); } @@ -255,7 +251,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var uri = new Uri(GetApiUrl(info)); - using (var manager = new HdHomerunManager(_socketFactory, Logger)) + using (var manager = new HdHomerunManager()) { // Legacy HdHomeruns are IPv4 only var ipInfo = IPAddress.Parse(uri.Host); @@ -275,6 +271,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun }); } } + return tuners; } @@ -433,12 +430,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { videoCodec = channelInfo.VideoCodec; } + string audioCodec = channelInfo.AudioCodec; if (!videoBitrate.HasValue) { videoBitrate = isHd ? 15000000 : 2000000; } + int? audioBitrate = isHd ? 448000 : 192000; // normalize @@ -460,7 +459,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { id = "native"; } - id += "_" + channelId.GetMD5().ToString("N") + "_" + url.GetMD5().ToString("N"); + + id += "_" + channelId.GetMD5().ToString("N", CultureInfo.InvariantCulture) + "_" + url.GetMD5().ToString("N", CultureInfo.InvariantCulture); var mediaSource = new MediaSourceInfo { @@ -526,29 +526,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } else { - try - { - var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); + var modelInfo = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); - if (modelInfo != null && modelInfo.SupportsTranscoding) + if (modelInfo != null && modelInfo.SupportsTranscoding) + { + if (info.AllowHWTranscoding) { - if (info.AllowHWTranscoding) - { - list.Add(GetMediaSource(info, hdhrId, channelInfo, "heavy")); - - list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet540")); - list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet480")); - list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet360")); - list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet240")); - list.Add(GetMediaSource(info, hdhrId, channelInfo, "mobile")); - } + list.Add(GetMediaSource(info, hdhrId, channelInfo, "heavy")); - list.Add(GetMediaSource(info, hdhrId, channelInfo, "native")); + list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet540")); + list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet480")); + list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet360")); + list.Add(GetMediaSource(info, hdhrId, channelInfo, "internet240")); + list.Add(GetMediaSource(info, hdhrId, channelInfo, "mobile")); } - } - catch - { + list.Add(GetMediaSource(info, hdhrId, channelInfo, "native")); } if (list.Count == 0) @@ -581,7 +574,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun if (hdhomerunChannel != null && hdhomerunChannel.IsLegacyTuner) { - return new HdHomerunUdpStream(mediaSource, info, streamId, new LegacyHdHomerunChannelCommands(hdhomerunChannel.Path), modelInfo.TunerCount, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager); + return new HdHomerunUdpStream( + mediaSource, + info, + streamId, + new LegacyHdHomerunChannelCommands(hdhomerunChannel.Path), + modelInfo.TunerCount, + FileSystem, + Logger, + Config.ApplicationPaths, + _appHost, + _networkManager, + _streamHelper); + } var enableHttpStream = true; @@ -596,12 +601,33 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { httpUrl += "?transcode=" + profile; } - mediaSource.Path = httpUrl; - return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost); - } + mediaSource.Path = httpUrl; - return new HdHomerunUdpStream(mediaSource, info, streamId, new HdHomerunChannelCommands(hdhomerunChannel.Number, profile), modelInfo.TunerCount, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _socketFactory, _networkManager); + return new SharedHttpStream( + mediaSource, + info, + streamId, + FileSystem, + _httpClient, + Logger, + Config.ApplicationPaths, + _appHost, + _streamHelper); + } + + return new HdHomerunUdpStream( + mediaSource, + info, + streamId, + new HdHomerunChannelCommands(hdhomerunChannel.Number, profile), + modelInfo.TunerCount, + FileSystem, + Logger, + Config.ApplicationPaths, + _appHost, + _networkManager, + _streamHelper); } public async Task Validate(TunerHostInfo info) @@ -700,9 +726,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun catch (OperationCanceledException) { } - catch + catch (Exception ex) { // Socket timeout indicates all messages have been received. + Logger.LogError(ex, "Error while sending discovery message"); } } @@ -717,21 +744,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun Url = url }; - try - { - var modelInfo = await GetModelInfo(hostInfo, false, cancellationToken).ConfigureAwait(false); - - hostInfo.DeviceId = modelInfo.DeviceID; - hostInfo.FriendlyName = modelInfo.FriendlyName; + var modelInfo = await GetModelInfo(hostInfo, false, cancellationToken).ConfigureAwait(false); - return hostInfo; - } - catch - { - // logged at lower levels - } + hostInfo.DeviceId = modelInfo.DeviceID; + hostInfo.FriendlyName = modelInfo.FriendlyName; - return null; + return hostInfo; } } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index 6e79441da..9702392b2 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -1,19 +1,20 @@ using System; +using System.Buffers; using System.Collections.Generic; +using System.Globalization; using System.Net; +using System.Net.Sockets; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.Net; -using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { public interface IHdHomerunChannelCommands { - IEnumerable<Tuple<string, string>> GetCommands(); + IEnumerable<(string, string)> GetCommands(); } public class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands @@ -32,16 +33,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } } - public IEnumerable<Tuple<string, string>> GetCommands() + public IEnumerable<(string, string)> GetCommands() { - var commands = new List<Tuple<string, string>>(); - if (!string.IsNullOrEmpty(_channel)) - commands.Add(Tuple.Create("channel", _channel)); + { + yield return ("channel", _channel); + } if (!string.IsNullOrEmpty(_program)) - commands.Add(Tuple.Create("program", _program)); - return commands; + { + yield return ("program", _program); + } } } @@ -56,95 +58,87 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun _profile = profile; } - public IEnumerable<Tuple<string, string>> GetCommands() + public IEnumerable<(string, string)> GetCommands() { - var commands = new List<Tuple<string, string>>(); - if (!string.IsNullOrEmpty(_channel)) { - if (!string.IsNullOrEmpty(_profile) && !string.Equals(_profile, "native", StringComparison.OrdinalIgnoreCase)) + if (!string.IsNullOrEmpty(_profile) + && !string.Equals(_profile, "native", StringComparison.OrdinalIgnoreCase)) { - commands.Add(Tuple.Create("vchannel", string.Format("{0} transcode={1}", _channel, _profile))); + yield return ("vchannel", $"{_channel} transcode={_profile}"); } else { - commands.Add(Tuple.Create("vchannel", _channel)); + yield return ("vchannel", _channel); } } - - return commands; } } public class HdHomerunManager : IDisposable { - public static int HdHomeRunPort = 65001; + public const int HdHomeRunPort = 65001; + // Message constants - private static byte GetSetName = 3; - private static byte GetSetValue = 4; - private static byte GetSetLockkey = 21; - private static ushort GetSetRequest = 4; - private static ushort GetSetReply = 5; + private const byte GetSetName = 3; + private const byte GetSetValue = 4; + private const byte GetSetLockkey = 21; + private const ushort GetSetRequest = 4; + private const ushort GetSetReply = 5; private uint? _lockkey = null; private int _activeTuner = -1; - private readonly ISocketFactory _socketFactory; - private IPAddress _remoteIp; + private IPEndPoint _remoteEndPoint; - private ILogger _logger; - private ISocket _currentTcpSocket; - - public HdHomerunManager(ISocketFactory socketFactory, ILogger logger) - { - _socketFactory = socketFactory; - _logger = logger; - } + private TcpClient _tcpClient; public void Dispose() { - using (var socket = _currentTcpSocket) + using (var socket = _tcpClient) { if (socket != null) { - _currentTcpSocket = null; + _tcpClient = null; - var task = StopStreaming(socket); - Task.WaitAll(task); + StopStreaming(socket).GetAwaiter().GetResult(); } } } public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken) { - using (var socket = _socketFactory.CreateTcpSocket(remoteIp, HdHomeRunPort)) + using (var client = new TcpClient(new IPEndPoint(remoteIp, HdHomeRunPort))) + using (var stream = client.GetStream()) { - return await CheckTunerAvailability(socket, remoteIp, tuner, cancellationToken).ConfigureAwait(false); + return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false); } } - private static async Task<bool> CheckTunerAvailability(ISocket socket, IPAddress remoteIp, int tuner, CancellationToken cancellationToken) + private static async Task<bool> CheckTunerAvailability(NetworkStream stream, int tuner, CancellationToken cancellationToken) { - var ipEndPoint = new IPEndPoint(remoteIp, HdHomeRunPort); - var lockkeyMsg = CreateGetMessage(tuner, "lockkey"); - await socket.SendToAsync(lockkeyMsg, 0, lockkeyMsg.Length, ipEndPoint, cancellationToken); + await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false); - var receiveBuffer = new byte[8192]; - var response = await socket.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false); + byte[] buffer = ArrayPool<byte>.Shared.Rent(8192); + try + { + int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); - ParseReturnMessage(response.Buffer, response.ReceivedBytes, out string returnVal); + ParseReturnMessage(buffer, receivedBytes, out string returnVal); - return string.Equals(returnVal, "none", StringComparison.OrdinalIgnoreCase); + return string.Equals(returnVal, "none", StringComparison.OrdinalIgnoreCase); + } + finally + { + ArrayPool<byte>.Shared.Return(buffer); + } } public async Task StartStreaming(IPAddress remoteIp, IPAddress localIp, int localPort, IHdHomerunChannelCommands commands, int numTuners, CancellationToken cancellationToken) { - _remoteIp = remoteIp; - - var tcpClient = _socketFactory.CreateTcpSocket(_remoteIp, HdHomeRunPort); - _currentTcpSocket = tcpClient; + _remoteEndPoint = new IPEndPoint(remoteIp, HdHomeRunPort); - var receiveBuffer = new byte[8192]; + _tcpClient = new TcpClient(_remoteEndPoint); if (!_lockkey.HasValue) { @@ -153,51 +147,64 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } var lockKeyValue = _lockkey.Value; + var stream = _tcpClient.GetStream(); - var ipEndPoint = new IPEndPoint(_remoteIp, HdHomeRunPort); - - for (int i = 0; i < numTuners; ++i) + byte[] buffer = ArrayPool<byte>.Shared.Rent(8192); + try { - if (!await CheckTunerAvailability(tcpClient, _remoteIp, i, cancellationToken).ConfigureAwait(false)) - continue; - - _activeTuner = i; - var lockKeyString = string.Format("{0:d}", lockKeyValue); - var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null); - await tcpClient.SendToAsync(lockkeyMsg, 0, lockkeyMsg.Length, ipEndPoint, cancellationToken).ConfigureAwait(false); - var response = await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false); - // parse response to make sure it worked - if (!ParseReturnMessage(response.Buffer, response.ReceivedBytes, out var returnVal)) - continue; - - var commandList = commands.GetCommands(); - foreach (Tuple<string, string> command in commandList) + for (int i = 0; i < numTuners; ++i) { - var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue); - await tcpClient.SendToAsync(channelMsg, 0, channelMsg.Length, ipEndPoint, cancellationToken).ConfigureAwait(false); - response = await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false); + if (!await CheckTunerAvailability(stream, i, cancellationToken).ConfigureAwait(false)) + { + continue; + } + + _activeTuner = i; + var lockKeyString = string.Format("{0:d}", lockKeyValue); + var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null); + await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false); + int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + // parse response to make sure it worked - if (!ParseReturnMessage(response.Buffer, response.ReceivedBytes, out returnVal)) + if (!ParseReturnMessage(buffer, receivedBytes, out _)) { - await ReleaseLockkey(tcpClient, lockKeyValue).ConfigureAwait(false); continue; } - } + var commandList = commands.GetCommands(); + foreach (var command in commandList) + { + var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue); + await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false); + receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + + // parse response to make sure it worked + if (!ParseReturnMessage(buffer, receivedBytes, out _)) + { + await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false); + continue; + } + } - var targetValue = string.Format("rtp://{0}:{1}", localIp, localPort); - var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue); + var targetValue = string.Format("rtp://{0}:{1}", localIp, localPort); + var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue); - await tcpClient.SendToAsync(targetMsg, 0, targetMsg.Length, ipEndPoint, cancellationToken).ConfigureAwait(false); - response = await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false); - // parse response to make sure it worked - if (!ParseReturnMessage(response.Buffer, response.ReceivedBytes, out returnVal)) - { - await ReleaseLockkey(tcpClient, lockKeyValue).ConfigureAwait(false); - continue; - } + await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false); + receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); - return; + // parse response to make sure it worked + if (!ParseReturnMessage(buffer, receivedBytes, out _)) + { + await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false); + continue; + } + + return; + } + } + finally + { + ArrayPool<byte>.Shared.Return(buffer); } _activeTuner = -1; @@ -207,58 +214,74 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun public async Task ChangeChannel(IHdHomerunChannelCommands commands, CancellationToken cancellationToken) { if (!_lockkey.HasValue) + { return; + } - using (var tcpClient = _socketFactory.CreateTcpSocket(_remoteIp, HdHomeRunPort)) + using (var tcpClient = new TcpClient(_remoteEndPoint)) + using (var stream = tcpClient.GetStream()) { var commandList = commands.GetCommands(); - var receiveBuffer = new byte[8192]; - - foreach (Tuple<string, string> command in commandList) + byte[] buffer = ArrayPool<byte>.Shared.Rent(8192); + try { - var channelMsg = CreateSetMessage(_activeTuner, command.Item1, command.Item2, _lockkey); - await tcpClient.SendToAsync(channelMsg, 0, channelMsg.Length, new IPEndPoint(_remoteIp, HdHomeRunPort), cancellationToken).ConfigureAwait(false); - var response = await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false); - // parse response to make sure it worked - if (!ParseReturnMessage(response.Buffer, response.ReceivedBytes, out string returnVal)) + foreach (var command in commandList) { - return; + var channelMsg = CreateSetMessage(_activeTuner, command.Item1, command.Item2, _lockkey); + await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false); + int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + + // parse response to make sure it worked + if (!ParseReturnMessage(buffer, receivedBytes, out _)) + { + return; + } } } + finally + { + ArrayPool<byte>.Shared.Return(buffer); + } } } - public Task StopStreaming(ISocket socket) + public Task StopStreaming(TcpClient client) { var lockKey = _lockkey; if (!lockKey.HasValue) + { return Task.CompletedTask; + } - return ReleaseLockkey(socket, lockKey.Value); + return ReleaseLockkey(client, lockKey.Value); } - private async Task ReleaseLockkey(ISocket tcpClient, uint lockKeyValue) + private async Task ReleaseLockkey(TcpClient client, uint lockKeyValue) { - _logger.LogInformation("HdHomerunManager.ReleaseLockkey {0}", lockKeyValue); - - var ipEndPoint = new IPEndPoint(_remoteIp, HdHomeRunPort); + var stream = client.GetStream(); var releaseTarget = CreateSetMessage(_activeTuner, "target", "none", lockKeyValue); - await tcpClient.SendToAsync(releaseTarget, 0, releaseTarget.Length, ipEndPoint, CancellationToken.None).ConfigureAwait(false); + await stream.WriteAsync(releaseTarget, 0, releaseTarget.Length).ConfigureAwait(false); - var receiveBuffer = new byte[8192]; - - await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, CancellationToken.None).ConfigureAwait(false); - var releaseKeyMsg = CreateSetMessage(_activeTuner, "lockkey", "none", lockKeyValue); - _lockkey = null; - await tcpClient.SendToAsync(releaseKeyMsg, 0, releaseKeyMsg.Length, ipEndPoint, CancellationToken.None).ConfigureAwait(false); - await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, CancellationToken.None).ConfigureAwait(false); + var buffer = ArrayPool<byte>.Shared.Rent(8192); + try + { + await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + var releaseKeyMsg = CreateSetMessage(_activeTuner, "lockkey", "none", lockKeyValue); + _lockkey = null; + await stream.WriteAsync(releaseKeyMsg, 0, releaseKeyMsg.Length).ConfigureAwait(false); + await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + } + finally + { + ArrayPool<byte>.Shared.Return(buffer); + } } private static byte[] CreateGetMessage(int tuner, string name) { - var byteName = Encoding.UTF8.GetBytes(string.Format("/tuner{0}/{1}\0", tuner, name)); + var byteName = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}\0", tuner, name)); int messageLength = byteName.Length + 10; // 4 bytes for header + 4 bytes for crc + 2 bytes for tag name and length var message = new byte[messageLength]; @@ -270,7 +293,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun // calculate crc and insert at the end of the message var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4)); if (flipEndian) + { Array.Reverse(crcBytes); + } + Buffer.BlockCopy(crcBytes, 0, message, offset, 4); return message; @@ -278,12 +304,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun private static byte[] CreateSetMessage(int tuner, string name, string value, uint? lockkey) { - var byteName = Encoding.UTF8.GetBytes(string.Format("/tuner{0}/{1}\0", tuner, name)); - var byteValue = Encoding.UTF8.GetBytes(string.Format("{0}\0", value)); + var byteName = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}\0", tuner, name)); + var byteValue = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "{0}\0", value)); int messageLength = byteName.Length + byteValue.Length + 12; if (lockkey.HasValue) + { messageLength += 6; + } var message = new byte[messageLength]; @@ -291,21 +319,20 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun bool flipEndian = BitConverter.IsLittleEndian; - message[offset] = GetSetValue; - offset++; - message[offset] = Convert.ToByte(byteValue.Length); - offset++; + message[offset++] = GetSetValue; + message[offset++] = Convert.ToByte(byteValue.Length); Buffer.BlockCopy(byteValue, 0, message, offset, byteValue.Length); offset += byteValue.Length; if (lockkey.HasValue) { - message[offset] = GetSetLockkey; - offset++; - message[offset] = (byte)4; - offset++; + message[offset++] = GetSetLockkey; + message[offset++] = 4; var lockKeyBytes = BitConverter.GetBytes(lockkey.Value); if (flipEndian) + { Array.Reverse(lockKeyBytes); + } + Buffer.BlockCopy(lockKeyBytes, 0, message, offset, 4); offset += 4; } @@ -313,7 +340,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun // calculate crc and insert at the end of the message var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4)); if (flipEndian) + { Array.Reverse(crcBytes); + } + Buffer.BlockCopy(crcBytes, 0, message, offset, 4); return message; @@ -342,10 +372,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun offset += 2; // insert tag name and length - message[offset] = GetSetName; - offset++; - message[offset] = Convert.ToByte(byteName.Length); - offset++; + message[offset++] = GetSetName; + message[offset++] = Convert.ToByte(byteName.Length); // insert name string Buffer.BlockCopy(byteName, 0, message, offset, byteName.Length); @@ -359,7 +387,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun returnVal = string.Empty; if (numBytes < 4) + { return false; + } var flipEndian = BitConverter.IsLittleEndian; int offset = 0; @@ -367,45 +397,49 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun Buffer.BlockCopy(buf, offset, msgTypeBytes, 0, msgTypeBytes.Length); if (flipEndian) + { Array.Reverse(msgTypeBytes); + } var msgType = BitConverter.ToUInt16(msgTypeBytes, 0); offset += 2; if (msgType != GetSetReply) + { return false; + } byte[] msgLengthBytes = new byte[2]; Buffer.BlockCopy(buf, offset, msgLengthBytes, 0, msgLengthBytes.Length); if (flipEndian) + { Array.Reverse(msgLengthBytes); + } var msgLength = BitConverter.ToUInt16(msgLengthBytes, 0); offset += 2; if (numBytes < msgLength + 8) + { return false; + } - var nameTag = buf[offset]; - offset++; + offset++; // Name Tag - var nameLength = buf[offset]; - offset++; + var nameLength = buf[offset++]; // skip the name field to get to value for return offset += nameLength; - var valueTag = buf[offset]; - offset++; + offset++; // Value Tag - var valueLength = buf[offset]; - offset++; + var valueLength = buf[offset++]; returnVal = Encoding.UTF8.GetString(buf, offset, valueLength - 1); // remove null terminator return true; } - private class HdHomerunCrc + private static class HdHomerunCrc { private static uint[] crc_table = { 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, @@ -477,15 +511,16 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { var hash = 0xffffffff; for (var i = 0; i < numBytes; i++) + { hash = (hash >> 8) ^ crc_table[(hash ^ bytes[i]) & 0xff]; + } var tmp = ~hash & 0xffffffff; var b0 = tmp & 0xff; var b1 = (tmp >> 8) & 0xff; var b2 = (tmp >> 16) & 0xff; var b3 = (tmp >> 24) & 0xff; - hash = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3; - return hash; + return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3; } } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index ec708cf20..eafa86d54 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -18,9 +18,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { public class HdHomerunUdpStream : LiveStream, IDirectStreamProvider { - private readonly IServerApplicationHost _appHost; - private readonly MediaBrowser.Model.Net.ISocketFactory _socketFactory; + private const int RtpHeaderBytes = 12; + private readonly IServerApplicationHost _appHost; private readonly IHdHomerunChannelCommands _channelCommands; private readonly int _numTuners; private readonly INetworkManager _networkManager; @@ -32,16 +32,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun IHdHomerunChannelCommands channelCommands, int numTuners, IFileSystem fileSystem, - IHttpClient httpClient, ILogger logger, IServerApplicationPaths appPaths, IServerApplicationHost appHost, - MediaBrowser.Model.Net.ISocketFactory socketFactory, - INetworkManager networkManager) - : base(mediaSource, tunerHostInfo, fileSystem, logger, appPaths) + INetworkManager networkManager, + IStreamHelper streamHelper) + : base(mediaSource, tunerHostInfo, fileSystem, logger, appPaths, streamHelper) { _appHost = appHost; - _socketFactory = socketFactory; _networkManager = networkManager; OriginalStreamId = originalStreamId; _channelCommands = channelCommands; @@ -49,13 +47,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun EnableStreamSharing = true; } - private static Socket CreateSocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType) - { - var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp); - - return socket; - } - public override async Task Open(CancellationToken openCancellationToken) { LiveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested(); @@ -71,13 +62,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var remoteAddress = IPAddress.Parse(uri.Host); IPAddress localAddress = null; - using (var tcpSocket = CreateSocket(remoteAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp)) + using (var tcpClient = new TcpClient()) { try { - tcpSocket.Connect(new IPEndPoint(remoteAddress, HdHomerunManager.HdHomeRunPort)); - localAddress = ((IPEndPoint)tcpSocket.LocalEndPoint).Address; - tcpSocket.Close(); + await tcpClient.ConnectAsync(remoteAddress, HdHomerunManager.HdHomeRunPort).ConfigureAwait(false); + localAddress = ((IPEndPoint)tcpClient.Client.RemoteEndPoint).Address; + tcpClient.Close(); } catch (Exception ex) { @@ -86,13 +77,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } } - var udpClient = _socketFactory.CreateUdpSocket(localPort); - var hdHomerunManager = new HdHomerunManager(_socketFactory, Logger); + var udpClient = new UdpClient(localPort, AddressFamily.InterNetwork); + var hdHomerunManager = new HdHomerunManager(); try { // send url to start streaming - await hdHomerunManager.StartStreaming(remoteAddress, localAddress, localPort, _channelCommands, _numTuners, openCancellationToken).ConfigureAwait(false); + await hdHomerunManager.StartStreaming( + remoteAddress, + localAddress, + localPort, + _channelCommands, + _numTuners, + openCancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -103,13 +100,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { Logger.LogError(ex, "Error opening live stream:"); } + throw; } } var taskCompletionSource = new TaskCompletionSource<bool>(); - await StartStreaming(udpClient, hdHomerunManager, remoteAddress, taskCompletionSource, LiveStreamCancellationTokenSource.Token); + await StartStreaming( + udpClient, + hdHomerunManager, + remoteAddress, + taskCompletionSource, + LiveStreamCancellationTokenSource.Token).ConfigureAwait(false); //OpenedMediaSource.Protocol = MediaProtocol.File; //OpenedMediaSource.Path = tempFile; @@ -125,7 +128,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun await taskCompletionSource.Task.ConfigureAwait(false); } - private Task StartStreaming(MediaBrowser.Model.Net.ISocket udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) + private Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) { return Task.Run(async () => { @@ -154,170 +157,47 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun }); } - private static void Resolve(TaskCompletionSource<bool> openTaskCompletionSource) + private async Task CopyTo(UdpClient udpClient, string file, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) { - Task.Run(() => - { - openTaskCompletionSource.TrySetResult(true); - }); - } - - private const int RtpHeaderBytes = 12; - - private async Task CopyTo(MediaBrowser.Model.Net.ISocket udpClient, string file, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) - { - var bufferSize = 81920; - - byte[] buffer = new byte[bufferSize]; - int read; var resolved = false; - using (var source = _socketFactory.CreateNetworkStream(udpClient, false)) - using (var fileStream = FileSystem.GetFileStream(file, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, FileOpenOptions.None)) + using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read)) { - var currentCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, new CancellationTokenSource(TimeSpan.FromSeconds(30)).Token).Token; - - while ((read = await source.ReadAsync(buffer, 0, buffer.Length, currentCancellationToken).ConfigureAwait(false)) != 0) + while (true) { cancellationToken.ThrowIfCancellationRequested(); - - currentCancellationToken = cancellationToken; - - read -= RtpHeaderBytes; - - if (read > 0) + using (var timeOutSource = new CancellationTokenSource()) + using (var linkedSource = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, + timeOutSource.Token)) { - fileStream.Write(buffer, RtpHeaderBytes, read); + var resTask = udpClient.ReceiveAsync(); + if (await Task.WhenAny(resTask, Task.Delay(30000, linkedSource.Token)).ConfigureAwait(false) != resTask) + { + resTask.Dispose(); + break; + } + + // We don't want all these delay tasks to keep running + timeOutSource.Cancel(); + var res = await resTask.ConfigureAwait(false); + var buffer = res.Buffer; + + var read = buffer.Length - RtpHeaderBytes; + + if (read > 0) + { + fileStream.Write(buffer, RtpHeaderBytes, read); + } + + if (!resolved) + { + resolved = true; + DateOpened = DateTime.UtcNow; + openTaskCompletionSource.TrySetResult(true); + } } - - if (!resolved) - { - resolved = true; - DateOpened = DateTime.UtcNow; - Resolve(openTaskCompletionSource); - } - } - } - } - - public class UdpClientStream : Stream - { - private static int RtpHeaderBytes = 12; - private static int PacketSize = 1316; - private readonly MediaBrowser.Model.Net.ISocket _udpClient; - bool disposed; - - public UdpClientStream(MediaBrowser.Model.Net.ISocket udpClient) : base() - { - _udpClient = udpClient; - } - - public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - if (buffer == null) - throw new ArgumentNullException(nameof(buffer)); - - if (offset + count < 0) - throw new ArgumentOutOfRangeException(nameof(offset), "offset + count must not be negative"); - - if (offset + count > buffer.Length) - throw new ArgumentException("offset + count must not be greater than the length of buffer"); - - if (disposed) - throw new ObjectDisposedException(nameof(UdpClientStream)); - - // This will always receive a 1328 packet size (PacketSize + RtpHeaderSize) - // The RTP header will be stripped so see how many reads we need to make to fill the buffer. - int numReads = count / PacketSize; - int totalBytesRead = 0; - byte[] receiveBuffer = new byte[81920]; - - for (int i = 0; i < numReads; ++i) - { - var data = await _udpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false); - - var bytesRead = data.ReceivedBytes - RtpHeaderBytes; - - // remove rtp header - Buffer.BlockCopy(data.Buffer, RtpHeaderBytes, buffer, offset, bytesRead); - offset += bytesRead; - totalBytesRead += bytesRead; } - return totalBytesRead; - } - - public override int Read(byte[] buffer, int offset, int count) - { - if (buffer == null) - throw new ArgumentNullException(nameof(buffer)); - - if (offset + count < 0) - throw new ArgumentOutOfRangeException("offset + count must not be negative", "offset+count"); - - if (offset + count > buffer.Length) - throw new ArgumentException("offset + count must not be greater than the length of buffer"); - - if (disposed) - throw new ObjectDisposedException(nameof(UdpClientStream)); - - // This will always receive a 1328 packet size (PacketSize + RtpHeaderSize) - // The RTP header will be stripped so see how many reads we need to make to fill the buffer. - int numReads = count / PacketSize; - int totalBytesRead = 0; - byte[] receiveBuffer = new byte[81920]; - - for (int i = 0; i < numReads; ++i) - { - var receivedBytes = _udpClient.Receive(receiveBuffer, 0, receiveBuffer.Length); - - var bytesRead = receivedBytes - RtpHeaderBytes; - - // remove rtp header - Buffer.BlockCopy(receiveBuffer, RtpHeaderBytes, buffer, offset, bytesRead); - offset += bytesRead; - totalBytesRead += bytesRead; - } - return totalBytesRead; - } - - protected override void Dispose(bool disposing) - { - disposed = true; - } - - public override bool CanRead => throw new NotImplementedException(); - - public override bool CanSeek => throw new NotImplementedException(); - - public override bool CanWrite => throw new NotImplementedException(); - - public override long Length => throw new NotImplementedException(); - - public override long Position - { - get => throw new NotImplementedException(); - - set => throw new NotImplementedException(); - } - - public override void Flush() - { - throw new NotImplementedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } - - public override void SetLength(long value) - { - throw new NotImplementedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); } } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs index ece2cbd54..d12c96392 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading; @@ -15,34 +16,28 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { public class LiveStream : ILiveStream { - public MediaSourceInfo OriginalMediaSource { get; set; } - public MediaSourceInfo MediaSource { get; set; } - - public int ConsumerCount { get; set; } - - public string OriginalStreamId { get; set; } - public bool EnableStreamSharing { get; set; } - public string UniqueId { get; } - protected readonly IFileSystem FileSystem; protected readonly IServerApplicationPaths AppPaths; + protected readonly IStreamHelper StreamHelper; protected string TempFilePath; protected readonly ILogger Logger; protected readonly CancellationTokenSource LiveStreamCancellationTokenSource = new CancellationTokenSource(); - public string TunerHostId { get; } - - public DateTime DateOpened { get; protected set; } - - public LiveStream(MediaSourceInfo mediaSource, TunerHostInfo tuner, IFileSystem fileSystem, ILogger logger, IServerApplicationPaths appPaths) + public LiveStream( + MediaSourceInfo mediaSource, + TunerHostInfo tuner, + IFileSystem fileSystem, + ILogger logger, + IServerApplicationPaths appPaths, + IStreamHelper streamHelper) { OriginalMediaSource = mediaSource; FileSystem = fileSystem; MediaSource = mediaSource; Logger = logger; EnableStreamSharing = true; - UniqueId = Guid.NewGuid().ToString("N"); + UniqueId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); if (tuner != null) { @@ -50,11 +45,27 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts } AppPaths = appPaths; + StreamHelper = streamHelper; ConsumerCount = 1; SetTempFilePath("ts"); } + protected virtual int EmptyReadLimit => 1000; + + public MediaSourceInfo OriginalMediaSource { get; set; } + public MediaSourceInfo MediaSource { get; set; } + + public int ConsumerCount { get; set; } + + public string OriginalStreamId { get; set; } + public bool EnableStreamSharing { get; set; } + public string UniqueId { get; } + + public string TunerHostId { get; } + + public DateTime DateOpened { get; protected set; } + protected void SetTempFilePath(string extension) { TempFilePath = Path.Combine(AppPaths.GetTranscodingTempPath(), UniqueId + "." + extension); @@ -70,24 +81,21 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { EnableStreamSharing = false; - Logger.LogInformation("Closing " + GetType().Name); + Logger.LogInformation("Closing {Type}", GetType().Name); LiveStreamCancellationTokenSource.Cancel(); return Task.CompletedTask; } - protected Stream GetInputStream(string path, bool allowAsyncFileRead) - { - var fileOpenOptions = FileOpenOptions.SequentialScan; - - if (allowAsyncFileRead) - { - fileOpenOptions |= FileOpenOptions.Asynchronous; - } - - return FileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.ReadWrite, fileOpenOptions); - } + protected FileStream GetInputStream(string path, bool allowAsyncFileRead) + => new FileStream( + path, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite, + StreamDefaults.DefaultFileStreamBufferSize, + allowAsyncFileRead ? FileOptions.SequentialScan | FileOptions.Asynchronous : FileOptions.SequentialScan); public Task DeleteTempFiles() { @@ -143,8 +151,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts bool seekFile = (DateTime.UtcNow - DateOpened).TotalSeconds > 10; var nextFileInfo = GetNextFile(null); - var nextFile = nextFileInfo.Item1; - var isLastFile = nextFileInfo.Item2; + var nextFile = nextFileInfo.file; + var isLastFile = nextFileInfo.isLastFile; while (!string.IsNullOrEmpty(nextFile)) { @@ -154,8 +162,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts seekFile = false; nextFileInfo = GetNextFile(nextFile); - nextFile = nextFileInfo.Item1; - isLastFile = nextFileInfo.Item2; + nextFile = nextFileInfo.file; + isLastFile = nextFileInfo.isLastFile; } Logger.LogInformation("Live Stream ended."); @@ -179,19 +187,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private async Task CopyFile(string path, bool seekFile, int emptyReadLimit, bool allowAsync, Stream stream, CancellationToken cancellationToken) { - using (var inputStream = (FileStream)GetInputStream(path, allowAsync)) + using (var inputStream = GetInputStream(path, allowAsync)) { if (seekFile) { TrySeek(inputStream, -20000); } - await ApplicationHost.StreamHelper.CopyToAsync(inputStream, stream, 81920, emptyReadLimit, cancellationToken).ConfigureAwait(false); + await StreamHelper.CopyToAsync( + inputStream, + stream, + StreamDefaults.DefaultCopyToBufferSize, + emptyReadLimit, + cancellationToken).ConfigureAwait(false); } } - protected virtual int EmptyReadLimit => 1000; - private void TrySeek(FileStream stream, long offset) { if (!stream.CanSeek) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 2d9bec53f..a02a9ade4 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading; @@ -27,14 +28,25 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private readonly IServerApplicationHost _appHost; private readonly INetworkManager _networkManager; private readonly IMediaSourceManager _mediaSourceManager; - - public M3UTunerHost(IServerConfigurationManager config, IMediaSourceManager mediaSourceManager, ILogger logger, IJsonSerializer jsonSerializer, IFileSystem fileSystem, IHttpClient httpClient, IServerApplicationHost appHost, INetworkManager networkManager) + private readonly IStreamHelper _streamHelper; + + public M3UTunerHost( + IServerConfigurationManager config, + IMediaSourceManager mediaSourceManager, + ILogger logger, + IJsonSerializer jsonSerializer, + IFileSystem fileSystem, + IHttpClient httpClient, + IServerApplicationHost appHost, + INetworkManager networkManager, + IStreamHelper streamHelper) : base(config, logger, jsonSerializer, fileSystem) { _httpClient = httpClient; _appHost = appHost; _networkManager = networkManager; _mediaSourceManager = mediaSourceManager; + _streamHelper = streamHelper; } public override string Type => "m3u"; @@ -43,7 +55,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private string GetFullChannelIdPrefix(TunerHostInfo info) { - return ChannelIdPrefix + info.Url.GetMD5().ToString("N"); + return ChannelIdPrefix + info.Url.GetMD5().ToString("N", CultureInfo.InvariantCulture); } protected override async Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo info, CancellationToken cancellationToken) @@ -61,7 +73,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts Name = Name, SourceType = Type, Status = LiveTvTunerStatus.Available, - Id = i.Url.GetMD5().ToString("N"), + Id = i.Url.GetMD5().ToString("N", CultureInfo.InvariantCulture), Url = i.Url }) .ToList(); @@ -102,11 +114,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) { - return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost); + return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config.ApplicationPaths, _appHost, _streamHelper); } } - return new LiveStream(mediaSource, info, FileSystem, Logger, Config.ApplicationPaths); + return new LiveStream(mediaSource, info, FileSystem, Logger, Config.ApplicationPaths, _streamHelper); } public async Task Validate(TunerHostInfo info) @@ -173,7 +185,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts ReadAtNativeFramerate = false, - Id = channel.Path.GetMD5().ToString("N"), + Id = channel.Path.GetMD5().ToString("N", CultureInfo.InvariantCulture), IsInfiniteStream = true, IsRemote = isRemote, diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index 814031b12..3d2267e75 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -58,6 +58,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts UserAgent = _appHost.ApplicationUserAgent }); } + return Task.FromResult((Stream)File.OpenRead(url)); } @@ -92,11 +93,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts var channel = GetChannelnfo(extInf, tunerHostId, line); if (string.IsNullOrWhiteSpace(channel.Id)) { - channel.Id = channelIdPrefix + line.GetMD5().ToString("N"); + channel.Id = channelIdPrefix + line.GetMD5().ToString("N", CultureInfo.InvariantCulture); } else { - channel.Id = channelIdPrefix + channel.Id.GetMD5().ToString("N"); + channel.Id = channelIdPrefix + channel.Id.GetMD5().ToString("N", CultureInfo.InvariantCulture); } channel.Path = line; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index 7de9931c7..c6e894560 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -19,8 +19,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private readonly IHttpClient _httpClient; private readonly IServerApplicationHost _appHost; - public SharedHttpStream(MediaSourceInfo mediaSource, TunerHostInfo tunerHostInfo, string originalStreamId, IFileSystem fileSystem, IHttpClient httpClient, ILogger logger, IServerApplicationPaths appPaths, IServerApplicationHost appHost) - : base(mediaSource, tunerHostInfo, fileSystem, logger, appPaths) + public SharedHttpStream( + MediaSourceInfo mediaSource, + TunerHostInfo tunerHostInfo, + string originalStreamId, + IFileSystem fileSystem, + IHttpClient httpClient, + ILogger logger, + IServerApplicationPaths appPaths, + IServerApplicationHost appHost, + IStreamHelper streamHelper) + : base(mediaSource, tunerHostInfo, fileSystem, logger, appPaths, streamHelper) { _httpClient = httpClient; _appHost = appHost; @@ -118,7 +127,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts using (var stream = response.Content) using (var fileStream = FileSystem.GetFileStream(TempFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, FileOpenOptions.None)) { - await ApplicationHost.StreamHelper.CopyToAsync(stream, fileStream, 81920, () => Resolve(openTaskCompletionSource), cancellationToken).ConfigureAwait(false); + await StreamHelper.CopyToAsync( + stream, + fileStream, + StreamDefaults.DefaultCopyToBufferSize, + () => Resolve(openTaskCompletionSource), + cancellationToken).ConfigureAwait(false); } } catch (OperationCanceledException) @@ -128,6 +142,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { Logger.LogError(ex, "Error copying live stream."); } + EnableStreamSharing = false; await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false); }); diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index eb145b4fe..4da3cdd3b 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -1,22 +1,22 @@ { - "Albums": "الألبومات", - "AppDeviceValues": "التطبيق: {0}. الجهاز: {1}.", + "Albums": "ألبومات", + "AppDeviceValues": "تطبيق: {0}, جهاز: {1}", "Application": "التطبيق", - "Artists": "الفنانون", - "AuthenticationSucceededWithUserName": "تم التأكد من {0} بنجاح", - "Books": "الكتب", - "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "Artists": "الفنان", + "AuthenticationSucceededWithUserName": "{0} سجل الدخول بنجاح", + "Books": "كتب", + "CameraImageUploadedFrom": "صورة كاميرا جديدة تم رفعها من {0}", "Channels": "القنوات", "ChapterNameValue": "الباب {0}", - "Collections": "المجاميع", + "Collections": "مجموعات", "DeviceOfflineWithName": "تم قطع الاتصال بـ{0}", "DeviceOnlineWithName": "{0} متصل", "FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}", - "Favorites": "المفضلات", + "Favorites": "التفضيلات", "Folders": "المجلدات", "Genres": "أنواع الأفلام", - "HeaderAlbumArtists": "فنانو الألبومات", - "HeaderCameraUploads": "Camera Uploads", + "HeaderAlbumArtists": "فناني الألبومات", + "HeaderCameraUploads": "تحميلات الكاميرا", "HeaderContinueWatching": "استئناف المشاهدة", "HeaderFavoriteAlbums": "الألبومات المفضلة", "HeaderFavoriteArtists": "الفنانون المفضلون", @@ -24,7 +24,7 @@ "HeaderFavoriteShows": "المسلسلات المفضلة", "HeaderFavoriteSongs": "الأغاني المفضلة", "HeaderLiveTV": "التلفاز المباشر", - "HeaderNextUp": "التشغيل التالي", + "HeaderNextUp": "التالي", "HeaderRecordingGroups": "مجموعات التسجيل", "HomeVideos": "الفيديوهات المنزلية", "Inherit": "توريث", @@ -34,29 +34,29 @@ "LabelRunningTimeValue": "وقت التشغيل: {0}", "Latest": "الأحدث", "MessageApplicationUpdated": "لقد تم تحديث خادم أمبي", - "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", + "MessageApplicationUpdatedTo": "تم تحديث سيرفر Jellyfin الى {0}", "MessageNamedServerConfigurationUpdatedWithValue": "تم تحديث إعدادات الخادم في قسم {0}", "MessageServerConfigurationUpdated": "تم تحديث إعدادات الخادم", "MixedContent": "محتوى مخلوط", "Movies": "الأفلام", "Music": "الموسيقى", "MusicVideos": "الفيديوهات الموسيقية", - "NameInstallFailed": "{0} installation failed", + "NameInstallFailed": "فشل التثبيت {0}", "NameSeasonNumber": "الموسم {0}", - "NameSeasonUnknown": "Season Unknown", - "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", + "NameSeasonUnknown": "الموسم غير معروف", + "NewVersionIsAvailable": "نسخة حديثة من سيرفر Jellyfin متوفرة للتحميل .", "NotificationOptionApplicationUpdateAvailable": "يوجد تحديث للتطبيق", "NotificationOptionApplicationUpdateInstalled": "تم تحديث التطبيق", "NotificationOptionAudioPlayback": "بدأ تشغيل المقطع الصوتي", "NotificationOptionAudioPlaybackStopped": "تم إيقاف تشغيل المقطع الصوتي", "NotificationOptionCameraImageUploaded": "تم رقع صورة الكاميرا", - "NotificationOptionInstallationFailed": "عملية التنصيب فشلت", + "NotificationOptionInstallationFailed": "فشل في التثبيت", "NotificationOptionNewLibraryContent": "تم إضافة محتوى جديد", "NotificationOptionPluginError": "فشل في الملحق", "NotificationOptionPluginInstalled": "تم تثبيت الملحق", "NotificationOptionPluginUninstalled": "تمت إزالة الملحق", - "NotificationOptionPluginUpdateInstalled": "تم تحديث الملحق", - "NotificationOptionServerRestartRequired": "يجب إعادة تشغيل الخادم", + "NotificationOptionPluginUpdateInstalled": "تم تثبيت تحديثات الملحق", + "NotificationOptionServerRestartRequired": "يجب إعادة تشغيل السيرفر", "NotificationOptionTaskFailed": "فشل في المهمة المجدولة", "NotificationOptionUserLockedOut": "تم إقفال حساب المستخدم", "NotificationOptionVideoPlayback": "بدأ تشغيل الفيديو", @@ -70,28 +70,28 @@ "ProviderValue": "المزود: {0}", "ScheduledTaskFailedWithName": "العملية {0} فشلت", "ScheduledTaskStartedWithName": "تم بدء {0}", - "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", - "Shows": "Shows", + "ServerNameNeedsToBeRestarted": "يحتاج لإعادة تشغيله {0}", + "Shows": "الحلقات", "Songs": "الأغاني", - "StartupEmbyServerIsLoading": "خادم أمبي قيد التحميل. الرجاء المحاوية بعد حين", + "StartupEmbyServerIsLoading": "سيرفر Jellyfin قيد التشغيل . الرجاء المحاولة بعد قليل.", "SubtitleDownloadFailureForItem": "عملية إنزال الترجمة فشلت لـ{0}", - "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", - "SubtitlesDownloadedForItem": "تم إنزال الترجمات لـ {0}", + "SubtitleDownloadFailureFromForItem": "الترجمات فشلت في التحميل من {0} لـ {1}", + "SubtitlesDownloadedForItem": "تم تحميل الترجمات لـ {0}", "Sync": "مزامنة", "System": "النظام", - "TvShows": "TV Shows", + "TvShows": "البرامج التلفزيونية", "User": "المستخدم", "UserCreatedWithName": "تم إنشاء المستخدم {0}", "UserDeletedWithName": "تم حذف المستخدم {0}", "UserDownloadingItemWithValues": "{0} يقوم بإنزال {1}", "UserLockedOutWithName": "المستخدم {0} تم منعه من الدخول", "UserOfflineFromDevice": "تم قطع اتصال {0} من {1}", - "UserOnlineFromDevice": "{0} متصلة عبر {1}", + "UserOnlineFromDevice": "{0} متصل عبر {1}", "UserPasswordChangedWithName": "تم تغيير كلمة السر للمستخدم {0}", - "UserPolicyUpdatedWithName": "User policy has been updated for {0}", - "UserStartedPlayingItemWithValues": "قام {0} ببدء تشغيل {1}", - "UserStoppedPlayingItemWithValues": "قام {0} بإيقاف تشغيل {1}", - "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", - "ValueSpecialEpisodeName": "خاص - {0}", + "UserPolicyUpdatedWithName": "سياسة المستخدمين تم تحديثها لـ {0}", + "UserStartedPlayingItemWithValues": "قام {0} ببدء تشغيل {1} على {2}", + "UserStoppedPlayingItemWithValues": "قام {0} بإيقاف تشغيل {1} على {2}", + "ValueHasBeenAddedToLibrary": "{0} تم اضافتها الى مكتبة الوسائط", + "ValueSpecialEpisodeName": "مميز - {0}", "VersionNumber": "الإصدار رقم {0}" } diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json index b01abafa1..cc8b7dbd5 100644 --- a/Emby.Server.Implementations/Localization/Core/da.json +++ b/Emby.Server.Implementations/Localization/Core/da.json @@ -23,7 +23,7 @@ "HeaderFavoriteEpisodes": "Favoritepisoder", "HeaderFavoriteShows": "Favoritserier", "HeaderFavoriteSongs": "Favoritsange", - "HeaderLiveTV": "Live TV", + "HeaderLiveTV": "Live-TV", "HeaderNextUp": "Næste", "HeaderRecordingGroups": "Optagelsesgrupper", "HomeVideos": "Hjemmevideoer", @@ -50,7 +50,7 @@ "NotificationOptionAudioPlayback": "Lydafspilning påbegyndt", "NotificationOptionAudioPlaybackStopped": "Lydafspilning stoppet", "NotificationOptionCameraImageUploaded": "Kamerabillede uploadet", - "NotificationOptionInstallationFailed": "Installationsfejl", + "NotificationOptionInstallationFailed": "Installationen fejlede", "NotificationOptionNewLibraryContent": "Nyt indhold tilføjet", "NotificationOptionPluginError": "Pluginfejl", "NotificationOptionPluginInstalled": "Plugin installeret", diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index db7ebb0c0..3589a4893 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -3,7 +3,7 @@ "AppDeviceValues": "Εφαρμογή: {0}, Συσκευή: {1}", "Application": "Εφαρμογή", "Artists": "Καλλιτέχνες", - "AuthenticationSucceededWithUserName": "{0} επιτυχείς σύνδεση", + "AuthenticationSucceededWithUserName": "Ο χρήστης {0} επαληθεύτηκε με επιτυχία", "Books": "Βιβλία", "CameraImageUploadedFrom": "Μια νέα εικόνα κάμερας έχει αποσταλεί από {0}", "Channels": "Κανάλια", @@ -15,9 +15,9 @@ "Favorites": "Αγαπημένα", "Folders": "Φάκελοι", "Genres": "Είδη", - "HeaderAlbumArtists": "Άλμπουμ Καλλιτεχνών", + "HeaderAlbumArtists": "Καλλιτέχνες του Άλμπουμ", "HeaderCameraUploads": "Μεταφορτώσεις Κάμερας", - "HeaderContinueWatching": "Συνεχίστε να παρακολουθείτε", + "HeaderContinueWatching": "Συνεχίστε την παρακολούθηση", "HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ", "HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες", "HeaderFavoriteEpisodes": "Αγαπημένα Επεισόδια", @@ -27,7 +27,7 @@ "HeaderNextUp": "Επόμενο", "HeaderRecordingGroups": "Γκρουπ Εγγραφών", "HomeVideos": "Προσωπικά βίντεο", - "Inherit": "Inherit", + "Inherit": "Κληρονόμηση", "ItemAddedWithName": "{0} προστέθηκε στη βιβλιοθήκη", "ItemRemovedWithName": "{0} διαγράφηκε από τη βιβλιοθήκη", "LabelIpAddressValue": "Διεύθυνση IP: {0}", @@ -92,6 +92,6 @@ "UserStartedPlayingItemWithValues": "{0} παίζει {1} σε {2}", "UserStoppedPlayingItemWithValues": "{0} τελείωσε να παίζει {1} σε {2}", "ValueHasBeenAddedToLibrary": "{0} προστέθηκαν στη βιβλιοθήκη πολυμέσων σας", - "ValueSpecialEpisodeName": "Special - {0}", + "ValueSpecialEpisodeName": "Σπέσιαλ - {0}", "VersionNumber": "Έκδοση {0}" } diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json index 003632968..99fda7aa6 100644 --- a/Emby.Server.Implementations/Localization/Core/es-MX.json +++ b/Emby.Server.Implementations/Localization/Core/es-MX.json @@ -15,7 +15,7 @@ "Favorites": "Favoritos", "Folders": "Carpetas", "Genres": "Géneros", - "HeaderAlbumArtists": "Artistas del Álbum", + "HeaderAlbumArtists": "Artistas del álbum", "HeaderCameraUploads": "Subidos desde Camara", "HeaderContinueWatching": "Continuar Viendo", "HeaderFavoriteAlbums": "Álbumes favoritos", diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index f03184d5b..2dcc2c1c8 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -3,7 +3,7 @@ "AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}", "Application": "Aplicación", "Artists": "Artistas", - "AuthenticationSucceededWithUserName": "{0} autenticado correctamente", + "AuthenticationSucceededWithUserName": "{0} identificado correctamente", "Books": "Libros", "CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}", "Channels": "Canales", @@ -16,15 +16,15 @@ "Folders": "Carpetas", "Genres": "Géneros", "HeaderAlbumArtists": "Artistas del álbum", - "HeaderCameraUploads": "Subidas desde cámara", + "HeaderCameraUploads": "Subidas desde la cámara", "HeaderContinueWatching": "Continuar viendo", "HeaderFavoriteAlbums": "Álbumes favoritos", "HeaderFavoriteArtists": "Artistas favoritos", "HeaderFavoriteEpisodes": "Episodios favoritos", "HeaderFavoriteShows": "Series favoritas", "HeaderFavoriteSongs": "Canciones favoritas", - "HeaderLiveTV": "TV en directo", - "HeaderNextUp": "Siguiendo", + "HeaderLiveTV": "Televisión en directo", + "HeaderNextUp": "Siguiente", "HeaderRecordingGroups": "Grupos de grabación", "HomeVideos": "Vídeos caseros", "Inherit": "Heredar", @@ -50,7 +50,7 @@ "NotificationOptionAudioPlayback": "Se inició la reproducción de audio", "NotificationOptionAudioPlaybackStopped": "Se detuvo la reproducción de audio", "NotificationOptionCameraImageUploaded": "Imagen de la cámara cargada", - "NotificationOptionInstallationFailed": "Error de instalación", + "NotificationOptionInstallationFailed": "Error en la instalación", "NotificationOptionNewLibraryContent": "Nuevo contenido añadido", "NotificationOptionPluginError": "Error en plugin", "NotificationOptionPluginInstalled": "Plugin instalado", @@ -79,13 +79,13 @@ "SubtitlesDownloadedForItem": "Descargar subtítulos para {0}", "Sync": "Sincronizar", "System": "Sistema", - "TvShows": "Series de TV", + "TvShows": "Programas de televisión", "User": "Usuario", "UserCreatedWithName": "El usuario {0} ha sido creado", "UserDeletedWithName": "El usuario {0} ha sido borrado", "UserDownloadingItemWithValues": "{0} está descargando {1}", "UserLockedOutWithName": "El usuario {0} ha sido bloqueado", - "UserOfflineFromDevice": "{0} se ha desconectado de {1}", + "UserOfflineFromDevice": "{0} se ha desconectado desde {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}", diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json new file mode 100644 index 000000000..15aa0a8ee --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/fi.json @@ -0,0 +1,48 @@ +{ + "HeaderLiveTV": "Netti-TV", + "NewVersionIsAvailable": "Uusi versio Jellyfin palvelimesta on ladattavissa", + "NameSeasonUnknown": "Tuntematon Kausi", + "NameSeasonNumber": "Kausi {0}", + "NameInstallFailed": "{0} asennus epäonnistui", + "MusicVideos": "Musiikkivideot", + "Music": "Musiikki", + "Movies": "Elokuvat", + "MixedContent": "Sekoitettu sisältö", + "MessageServerConfigurationUpdated": "Palvelimen konfiguraatio on päivitetty", + "MessageNamedServerConfigurationUpdatedWithValue": "Palvelimen konfiguraatio-osa {0} on päivitetty", + "MessageApplicationUpdatedTo": "Jellyfin palvelin on päivitetty {0}", + "MessageApplicationUpdated": "Jellyfin palvelin on päivitetty", + "Latest": "Viimeisin", + "LabelRunningTimeValue": "Kesto: {0}", + "LabelIpAddressValue": "IP-osoite: {0}", + "ItemRemovedWithName": "{0} poistettiin kirjastosta", + "ItemAddedWithName": "{0} lisättiin kirjastoon", + "Inherit": "Periä", + "HomeVideos": "Kotivideot", + "HeaderRecordingGroups": "Äänitysryhmä", + "HeaderNextUp": "Seuraavaksi", + "HeaderFavoriteSongs": "Lempikappaleet", + "HeaderFavoriteShows": "Lempisarjat", + "HeaderFavoriteEpisodes": "Lempijaksot", + "HeaderCameraUploads": "Kamerasta Ladatut", + "HeaderFavoriteArtists": "Lempiartistit", + "HeaderFavoriteAlbums": "Lempialbumit", + "HeaderContinueWatching": "Jatka Katsomista", + "HeaderAlbumArtists": "Albumiartistit", + "Genres": "Genret", + "Folders": "Kansiot", + "Favorites": "Suosikit", + "FailedLoginAttemptWithUserName": "Epäonnistunut kirjautumisyritys kohteesta {0}", + "DeviceOnlineWithName": "{0} on yhdistynyt", + "DeviceOfflineWithName": "{0} on katkaissut yhteytensä", + "Collections": "Kokoelmat", + "ChapterNameValue": "Luku: {0}", + "Channels": "Kanavat", + "CameraImageUploadedFrom": "Uusi kamerakuva on lähetetty kohteesta {0}", + "Books": "Kirjat", + "AuthenticationSucceededWithUserName": "{0} todennettu onnistuneesti", + "Artists": "Artistit", + "Application": "Sovellus", + "AppDeviceValues": "Sovellus: {0}, Laite: {1}", + "Albums": "Albumit" +} diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index e434b7605..9805992be 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -5,7 +5,7 @@ "Artists": "Artistes", "AuthenticationSucceededWithUserName": "{0} s'est authentifié avec succès", "Books": "Livres", - "CameraImageUploadedFrom": "Une image de caméra a été chargée depuis {0}", + "CameraImageUploadedFrom": "Une nouvelle image de caméra a été chargée depuis {0}", "Channels": "Chaînes", "ChapterNameValue": "Chapitre {0}", "Collections": "Collections", @@ -24,7 +24,7 @@ "HeaderFavoriteShows": "Séries favorites", "HeaderFavoriteSongs": "Chansons favorites", "HeaderLiveTV": "TV en direct", - "HeaderNextUp": "En Cours", + "HeaderNextUp": "À suivre", "HeaderRecordingGroups": "Groupes d'enregistrements", "HomeVideos": "Vidéos personnelles", "Inherit": "Hériter", @@ -34,14 +34,14 @@ "LabelRunningTimeValue": "Durée : {0}", "Latest": "Derniers", "MessageApplicationUpdated": "Le serveur Jellyfin a été mis à jour", - "MessageApplicationUpdatedTo": "Jellyfin Serveur a été mis à jour en version {0}", + "MessageApplicationUpdatedTo": "Le serveur Jellyfin a été mis à jour vers {0}", "MessageNamedServerConfigurationUpdatedWithValue": "La configuration de la section {0} du serveur a été mise à jour", "MessageServerConfigurationUpdated": "La configuration du serveur a été mise à jour", "MixedContent": "Contenu mixte", "Movies": "Films", "Music": "Musique", "MusicVideos": "Vidéos musicales", - "NameInstallFailed": "{0} échec d'installation", + "NameInstallFailed": "{0} échec de l'installation", "NameSeasonNumber": "Saison {0}", "NameSeasonUnknown": "Saison Inconnue", "NewVersionIsAvailable": "Une nouvelle version de Jellyfin Serveur est disponible au téléchargement.", @@ -50,7 +50,7 @@ "NotificationOptionAudioPlayback": "Lecture audio démarrée", "NotificationOptionAudioPlaybackStopped": "Lecture audio arrêtée", "NotificationOptionCameraImageUploaded": "L'image de l'appareil photo a été transférée", - "NotificationOptionInstallationFailed": "Échec d'installation", + "NotificationOptionInstallationFailed": "Échec de l'installation", "NotificationOptionNewLibraryContent": "Nouveau contenu ajouté", "NotificationOptionPluginError": "Erreur d'extension", "NotificationOptionPluginInstalled": "Extension installée", @@ -91,7 +91,7 @@ "UserPolicyUpdatedWithName": "La politique de l'utilisateur a été mise à jour pour {0}", "UserStartedPlayingItemWithValues": "{0} est en train de lire {1} sur {2}", "UserStoppedPlayingItemWithValues": "{0} vient d'arrêter la lecture de {1} sur {2}", - "ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre librairie", + "ValueHasBeenAddedToLibrary": "{0} a été ajouté à votre médiathèque", "ValueSpecialEpisodeName": "Spécial - {0}", "VersionNumber": "Version {0}" } diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json index f2b7c408c..3d2350c07 100644 --- a/Emby.Server.Implementations/Localization/Core/ko.json +++ b/Emby.Server.Implementations/Localization/Core/ko.json @@ -1,97 +1,97 @@ { "Albums": "앨범", - "AppDeviceValues": "앱: {0}, 디바이스: {1}", + "AppDeviceValues": "앱: {0}, 장치: {1}", "Application": "애플리케이션", "Artists": "아티스트", - "AuthenticationSucceededWithUserName": "{0} 인증에 성공했습니다.", - "Books": "책", - "CameraImageUploadedFrom": "새로운 카메라 이미지가 {0}에서 업로드되었습니다.", + "AuthenticationSucceededWithUserName": "{0}이 성공적으로 인증됨", + "Books": "도서", + "CameraImageUploadedFrom": "{0}에서 새로운 카메라 이미지가 업로드되었습니다", "Channels": "채널", "ChapterNameValue": "챕터 {0}", "Collections": "컬렉션", - "DeviceOfflineWithName": "{0}가 접속이 끊어졌습니다.", - "DeviceOnlineWithName": "{0}가 접속되었습니다.", - "FailedLoginAttemptWithUserName": "{0}에서 로그인이 실패했습니다.", + "DeviceOfflineWithName": "{0} 연결 끊김", + "DeviceOnlineWithName": "{0} 연결됨", + "FailedLoginAttemptWithUserName": "{0} 로그인 실패", "Favorites": "즐겨찾기", "Folders": "폴더", "Genres": "장르", "HeaderAlbumArtists": "앨범 아티스트", "HeaderCameraUploads": "카메라 업로드", "HeaderContinueWatching": "계속 시청하기", - "HeaderFavoriteAlbums": "좋아하는 앨범", - "HeaderFavoriteArtists": "좋아하는 아티스트", - "HeaderFavoriteEpisodes": "좋아하는 에피소드", + "HeaderFavoriteAlbums": "즐겨찾는 앨범", + "HeaderFavoriteArtists": "즐겨찾는 아티스트", + "HeaderFavoriteEpisodes": "즐겨찾는 에피소드", "HeaderFavoriteShows": "즐겨찾는 쇼", - "HeaderFavoriteSongs": "좋아하는 노래", - "HeaderLiveTV": "TV 방송", + "HeaderFavoriteSongs": "즐겨찾는 노래", + "HeaderLiveTV": "실시간 TV", "HeaderNextUp": "다음으로", "HeaderRecordingGroups": "녹화 그룹", "HomeVideos": "홈 비디오", "Inherit": "상속", - "ItemAddedWithName": "{0} 라이브러리에 추가됨", - "ItemRemovedWithName": "{0} 라이브러리에서 제거됨", + "ItemAddedWithName": "{0}가 라이브러리에 추가됨", + "ItemRemovedWithName": "{0}가 라이브러리에서 제거됨", "LabelIpAddressValue": "IP 주소: {0}", "LabelRunningTimeValue": "상영 시간: {0}", "Latest": "최근", - "MessageApplicationUpdated": "Jellyfin 서버 업데이트됨", - "MessageApplicationUpdatedTo": "Jellyfin 서버가 {0}로 업데이트됨", - "MessageNamedServerConfigurationUpdatedWithValue": "서버 환경 설정 {0} 섹션 업데이트 됨", - "MessageServerConfigurationUpdated": "서버 환경 설정 업데이드됨", + "MessageApplicationUpdated": "Jellyfin 서버가 업데이트되었습니다", + "MessageApplicationUpdatedTo": "Jellyfin 서버가 {0}로 업데이트되었습니다", + "MessageNamedServerConfigurationUpdatedWithValue": "서버 환경 설정 {0} 섹션이 업데이트되었습니다", + "MessageServerConfigurationUpdated": "서버 환경 설정이 업데이트되었습니다", "MixedContent": "혼합 콘텐츠", "Movies": "영화", "Music": "음악", - "MusicVideos": "뮤직 비디오", - "NameInstallFailed": "{0} 설치 실패.", + "MusicVideos": "뮤직비디오", + "NameInstallFailed": "{0} 설치 실패", "NameSeasonNumber": "시즌 {0}", "NameSeasonUnknown": "알 수 없는 시즌", - "NewVersionIsAvailable": "새 버전의 Jellyfin 서버를 사용할 수 있습니다.", - "NotificationOptionApplicationUpdateAvailable": "애플리케이션 업데이트 사용 가능", - "NotificationOptionApplicationUpdateInstalled": "애플리케이션 업데이트가 설치됨", - "NotificationOptionAudioPlayback": "오디오 재생을 시작함", - "NotificationOptionAudioPlaybackStopped": "오디오 재생이 중지됨", + "NewVersionIsAvailable": "새로운 버전의 Jellyfin 서버를 사용할 수 있습니다.", + "NotificationOptionApplicationUpdateAvailable": "애플리케이션 업데이트 가능", + "NotificationOptionApplicationUpdateInstalled": "애플리케이션 업데이트 완료", + "NotificationOptionAudioPlayback": "오디오 재생 시작됨", + "NotificationOptionAudioPlaybackStopped": "오디오 재생 중지됨", "NotificationOptionCameraImageUploaded": "카메라 이미지가 업로드됨", "NotificationOptionInstallationFailed": "설치 실패", - "NotificationOptionNewLibraryContent": "새 콘텐트가 추가됨", + "NotificationOptionNewLibraryContent": "새 콘텐츠가 추가됨", "NotificationOptionPluginError": "플러그인 실패", - "NotificationOptionPluginInstalled": "플러그인이 설치됨", - "NotificationOptionPluginUninstalled": "플러그인이 설치 제거됨", - "NotificationOptionPluginUpdateInstalled": "플러그인 업데이트가 설치됨", - "NotificationOptionServerRestartRequired": "서버를 다시 시작하십시오", + "NotificationOptionPluginInstalled": "플러그인 설치됨", + "NotificationOptionPluginUninstalled": "플러그인 제거됨", + "NotificationOptionPluginUpdateInstalled": "플러그인 업데이트 완료", + "NotificationOptionServerRestartRequired": "서버 재시작 필요", "NotificationOptionTaskFailed": "예약 작업 실패", - "NotificationOptionUserLockedOut": "사용자가 잠겼습니다", - "NotificationOptionVideoPlayback": "비디오 재생을 시작함", - "NotificationOptionVideoPlaybackStopped": "비디오 재생이 중지됨", + "NotificationOptionUserLockedOut": "잠긴 사용자", + "NotificationOptionVideoPlayback": "비디오 재생 시작됨", + "NotificationOptionVideoPlaybackStopped": "비디오 재생 중지됨", "Photos": "사진", "Playlists": "재생목록", "Plugin": "플러그인", "PluginInstalledWithName": "{0} 설치됨", - "PluginUninstalledWithName": "{0} 설치 제거됨", + "PluginUninstalledWithName": "{0} 제거됨", "PluginUpdatedWithName": "{0} 업데이트됨", "ProviderValue": "제공자: {0}", "ScheduledTaskFailedWithName": "{0} 실패", "ScheduledTaskStartedWithName": "{0} 시작", - "ServerNameNeedsToBeRestarted": "{0} 를 재시작하십시오", - "Shows": "프로그램", + "ServerNameNeedsToBeRestarted": "{0}를 재시작해야합니다", + "Shows": "쇼", "Songs": "노래", - "StartupEmbyServerIsLoading": "Jellyfin 서버를 불러오고 있습니다. 잠시후 다시시도 해주세요.", + "StartupEmbyServerIsLoading": "Jellyfin 서버를 불러오고 있습니다. 잠시 후에 다시 시도하십시오.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "{0}에서 {1} 자막 다운로드에 실패했습니다", - "SubtitlesDownloadedForItem": "{0} 자막을 다운로드했습니다", + "SubtitlesDownloadedForItem": "{0} 자막 다운로드 완료", "Sync": "동기화", "System": "시스템", "TvShows": "TV 쇼", "User": "사용자", "UserCreatedWithName": "사용자 {0} 생성됨", "UserDeletedWithName": "사용자 {0} 삭제됨", - "UserDownloadingItemWithValues": "{0} is downloading {1}", - "UserLockedOutWithName": "User {0} has been locked out", - "UserOfflineFromDevice": "{0} has disconnected from {1}", - "UserOnlineFromDevice": "{0} is online from {1}", - "UserPasswordChangedWithName": "Password has been changed for user {0}", - "UserPolicyUpdatedWithName": "User policy has been updated for {0}", - "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", - "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", - "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", - "ValueSpecialEpisodeName": "Special - {0}", - "VersionNumber": "Version {0}" + "UserDownloadingItemWithValues": "{0}이(가) {1}을 다운로드 중입니다", + "UserLockedOutWithName": "유저 {0} 은(는) 잠금처리 되었습니다", + "UserOfflineFromDevice": "{1}로부터 {0}의 연결이 끊겼습니다", + "UserOnlineFromDevice": "{0}은 {1}에서 온라인 상태입니다", + "UserPasswordChangedWithName": "사용자 {0}의 비밀번호가 변경되었습니다", + "UserPolicyUpdatedWithName": "{0}의 사용자 정책이 업데이트되었습니다", + "UserStartedPlayingItemWithValues": "{2}에서 {0}이 {1} 재생 중", + "UserStoppedPlayingItemWithValues": "{2}에서 {0}이 {1} 재생을 마침", + "ValueHasBeenAddedToLibrary": "{0}가 미디어 라이브러리에 추가되었습니다", + "ValueSpecialEpisodeName": "스페셜 - {0}", + "VersionNumber": "버전 {0}" } diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index dbda794ad..1237fb70a 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -1,11 +1,11 @@ { "Albums": "Album", - "AppDeviceValues": "App:{0}, Enhet {1}", - "Application": "Applikasjon", + "AppDeviceValues": "App:{0}, Enhet: {1}", + "Application": "Program", "Artists": "Artister", - "AuthenticationSucceededWithUserName": "{0} vellykkede autentisert", + "AuthenticationSucceededWithUserName": "{0} har logget inn", "Books": "Bøker", - "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "CameraImageUploadedFrom": "Et nytt kamerabilde er lastet opp fra {0}", "Channels": "Kanaler", "ChapterNameValue": "Kapittel {0}", "Collections": "Samlinger", @@ -14,16 +14,16 @@ "FailedLoginAttemptWithUserName": "Mislykket påloggingsforsøk fra {0}", "Favorites": "Favoritter", "Folders": "Mapper", - "Genres": "Sjanger", - "HeaderAlbumArtists": "Albumartist", - "HeaderCameraUploads": "Camera Uploads", - "HeaderContinueWatching": "Forsett og see på", - "HeaderFavoriteAlbums": "Favoritt albumer", + "Genres": "Sjangre", + "HeaderAlbumArtists": "Albumartister", + "HeaderCameraUploads": "Kameraopplastinger", + "HeaderContinueWatching": "Forsett å se på", + "HeaderFavoriteAlbums": "Favorittalbum", "HeaderFavoriteArtists": "Favorittartister", - "HeaderFavoriteEpisodes": "Favoritt episode", + "HeaderFavoriteEpisodes": "Favorittepisoder", "HeaderFavoriteShows": "Favorittserier", "HeaderFavoriteSongs": "Favorittsanger", - "HeaderLiveTV": "Direkte TV", + "HeaderLiveTV": "Direkte-TV", "HeaderNextUp": "Neste", "HeaderRecordingGroups": "Opptak Grupper", "HomeVideos": "Hjemmelaget filmer", @@ -34,23 +34,23 @@ "LabelRunningTimeValue": "Løpetid {0}", "Latest": "Siste", "MessageApplicationUpdated": "Jellyfin server har blitt oppdatert", - "MessageApplicationUpdatedTo": "Jellyfin Server has been updated to {0}", + "MessageApplicationUpdatedTo": "Jellyfin-serveren ble oppdatert til {0}", "MessageNamedServerConfigurationUpdatedWithValue": "Server konfigurasjon seksjon {0} har blitt oppdatert", "MessageServerConfigurationUpdated": "Server konfigurasjon er oppdatert", "MixedContent": "Blandet innhold", "Movies": "Filmer", "Music": "Musikk", "MusicVideos": "Musikkvideoer", - "NameInstallFailed": "{0} installation failed", + "NameInstallFailed": "{0}-installasjonen mislyktes", "NameSeasonNumber": "Sesong {0}", - "NameSeasonUnknown": "Season Unknown", - "NewVersionIsAvailable": "A new version of Jellyfin Server is available for download.", + "NameSeasonUnknown": "Sesong ukjent", + "NewVersionIsAvailable": "En ny versjon av Jellyfin-serveren er tilgjengelig for nedlastning.", "NotificationOptionApplicationUpdateAvailable": "Applikasjon oppdatering tilgjengelig", - "NotificationOptionApplicationUpdateInstalled": "Applikasjon oppdatering installert.", + "NotificationOptionApplicationUpdateInstalled": "Applikasjonsoppdatering installert", "NotificationOptionAudioPlayback": "Lyd tilbakespilling startet", "NotificationOptionAudioPlaybackStopped": "Lyd avspilling stoppet", "NotificationOptionCameraImageUploaded": "Kamera bilde lastet opp", - "NotificationOptionInstallationFailed": "Installasjon feil", + "NotificationOptionInstallationFailed": "Installasjonsfeil", "NotificationOptionNewLibraryContent": "Ny innhold er lagt til", "NotificationOptionPluginError": "Plugin feil", "NotificationOptionPluginInstalled": "Plugin installert", @@ -61,8 +61,8 @@ "NotificationOptionUserLockedOut": "Bruker er utestengt", "NotificationOptionVideoPlayback": "Video tilbakespilling startet", "NotificationOptionVideoPlaybackStopped": "Video avspilling stoppet", - "Photos": "BIlder", - "Playlists": "Spilleliste", + "Photos": "Bilder", + "Playlists": "Spillelister", "Plugin": "Plugin", "PluginInstalledWithName": "{0} ble installert", "PluginUninstalledWithName": "{0} ble avinstallert", @@ -70,16 +70,16 @@ "ProviderValue": "Leverandører: {0}", "ScheduledTaskFailedWithName": "{0} Mislykkes", "ScheduledTaskStartedWithName": "{0} Startet", - "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", + "ServerNameNeedsToBeRestarted": "{0} må startes på nytt", "Shows": "Programmer", "Songs": "Sanger", "StartupEmbyServerIsLoading": "Jellyfin server laster. Prøv igjen snart.", "SubtitleDownloadFailureForItem": "En feil oppstå under nedlasting av undertekster for {0}", - "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", + "SubtitleDownloadFailureFromForItem": "Kunne ikke laste ned teksting fra {0} for {1}", "SubtitlesDownloadedForItem": "Undertekster lastet ned for {0}", - "Sync": "Synk", + "Sync": "Synkroniser", "System": "System", - "TvShows": "TV Shows", + "TvShows": "TV-serier", "User": "Bruker", "UserCreatedWithName": "Bruker {0} er opprettet", "UserDeletedWithName": "Bruker {0} har blitt slettet", @@ -88,10 +88,10 @@ "UserOfflineFromDevice": "{0} har koblet fra {1}", "UserOnlineFromDevice": "{0} er tilkoblet fra {1}", "UserPasswordChangedWithName": "Passordet for {0} er oppdatert", - "UserPolicyUpdatedWithName": "User policy has been updated for {0}", + "UserPolicyUpdatedWithName": "Brukerpolicyen har blitt oppdatert for {0}", "UserStartedPlayingItemWithValues": "{0} har startet avspilling {1}", "UserStoppedPlayingItemWithValues": "{0} har stoppet avspilling {1}", - "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", - "ValueSpecialEpisodeName": "Spesial - {0}", + "ValueHasBeenAddedToLibrary": "{0} har blitt lagt til i mediebiblioteket ditt", + "ValueSpecialEpisodeName": "Spesialepisode - {0}", "VersionNumber": "Versjon {0}" } diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index c4ce16dc8..faa8499b8 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-BR.json +++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json @@ -1,7 +1,7 @@ { "Albums": "Álbuns", "AppDeviceValues": "App: {0}, Dispositivo: {1}", - "Application": "Aplicativo", + "Application": "Inscrição", "Artists": "Artistas", "AuthenticationSucceededWithUserName": "{0} autenticado com sucesso", "Books": "Livros", @@ -16,7 +16,7 @@ "Folders": "Pastas", "Genres": "Gêneros", "HeaderAlbumArtists": "Artistas do Álbum", - "HeaderCameraUploads": "Uploads da Câmera", + "HeaderCameraUploads": "Envios da Câmera", "HeaderContinueWatching": "Continuar Assistindo", "HeaderFavoriteAlbums": "Álbuns Favoritos", "HeaderFavoriteArtists": "Artistas Favoritos", diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json index 387604f03..b12d391c1 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-PT.json +++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json @@ -1,11 +1,11 @@ { "Albums": "Álbuns", - "AppDeviceValues": "Aplicação {0}, Dispositivo:{1}", + "AppDeviceValues": "Aplicação {0}, Dispositivo: {1}", "Application": "Aplicação", "Artists": "Artistas", "AuthenticationSucceededWithUserName": "{0} autenticado com sucesso", "Books": "Livros", - "CameraImageUploadedFrom": "Uma nova imagem proveniente de uma câmara foi enviada a partir de {0}", + "CameraImageUploadedFrom": "Uma nova imagem de câmara foi enviada a partir de {0}", "Channels": "Canais", "ChapterNameValue": "Capítulo {0}", "Collections": "Coleções", @@ -16,7 +16,7 @@ "Folders": "Pastas", "Genres": "Géneros", "HeaderAlbumArtists": "Artistas do Álbum", - "HeaderCameraUploads": "Camera Uploads", + "HeaderCameraUploads": "Envios a partir da câmara", "HeaderContinueWatching": "Continuar a Ver", "HeaderFavoriteAlbums": "Álbuns Favoritos", "HeaderFavoriteArtists": "Artistas Favoritos", @@ -27,7 +27,7 @@ "HeaderNextUp": "A Seguir", "HeaderRecordingGroups": "Grupos de Gravação", "HomeVideos": "Home videos", - "Inherit": "Inherit", + "Inherit": "Herdar", "ItemAddedWithName": "{0} foi adicionado à biblioteca", "ItemRemovedWithName": "{0} foi removido da biblioteca", "LabelIpAddressValue": "Endereço IP: {0}", @@ -49,7 +49,7 @@ "NotificationOptionApplicationUpdateInstalled": "Atualização de aplicação instalada", "NotificationOptionAudioPlayback": "Reprodução Iniciada", "NotificationOptionAudioPlaybackStopped": "Reprodução Parada", - "NotificationOptionCameraImageUploaded": "Camera image uploaded", + "NotificationOptionCameraImageUploaded": "Imagem da câmara enviada", "NotificationOptionInstallationFailed": "Falha na instalação", "NotificationOptionNewLibraryContent": "Novo conteúdo adicionado", "NotificationOptionPluginError": "Falha na extensão", diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index c0465def8..0ad4b37aa 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -15,7 +15,7 @@ "Favorites": "Избранное", "Folders": "Папки", "Genres": "Жанры", - "HeaderAlbumArtists": "Исп-ли альбома", + "HeaderAlbumArtists": "Исполнители альбома", "HeaderCameraUploads": "Камеры", "HeaderContinueWatching": "Продолжение просмотра", "HeaderFavoriteAlbums": "Избранные альбомы", diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json index 531dfe51f..c141a40f6 100644 --- a/Emby.Server.Implementations/Localization/Core/sl-SI.json +++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json @@ -3,7 +3,7 @@ "AppDeviceValues": "Aplikacija: {0}, Naprava: {1}", "Application": "Aplikacija", "Artists": "Izvajalci", - "AuthenticationSucceededWithUserName": "{0} preverjanje uspešno", + "AuthenticationSucceededWithUserName": "{0} preverjanje pristnosti uspešno", "Books": "Knjige", "CameraImageUploadedFrom": "Nova fotografija je bila naložena z {0}", "Channels": "Kanali", @@ -44,13 +44,13 @@ "NameInstallFailed": "{0} namestitev neuspešna", "NameSeasonNumber": "Sezona {0}", "NameSeasonUnknown": "Season neznana", - "NewVersionIsAvailable": "Nova razničica Jellyfin strežnika je na voljo za prenos.", + "NewVersionIsAvailable": "Nova različica Jellyfin strežnika je na voljo za prenos.", "NotificationOptionApplicationUpdateAvailable": "Posodobitev aplikacije je na voljo", "NotificationOptionApplicationUpdateInstalled": "Posodobitev aplikacije je bila nameščena", "NotificationOptionAudioPlayback": "Predvajanje zvoka začeto", "NotificationOptionAudioPlaybackStopped": "Predvajanje zvoka zaustavljeno", "NotificationOptionCameraImageUploaded": "Posnetek kamere naložen", - "NotificationOptionInstallationFailed": "Napaka pri nameščanju", + "NotificationOptionInstallationFailed": "Namestitev neuspešna", "NotificationOptionNewLibraryContent": "Nove vsebine dodane", "NotificationOptionPluginError": "Napaka dodatka", "NotificationOptionPluginInstalled": "Dodatek nameščen", @@ -92,6 +92,6 @@ "UserStartedPlayingItemWithValues": "{0} predvaja {1} na {2}", "UserStoppedPlayingItemWithValues": "{0} je nehal predvajati {1} na {2}", "ValueHasBeenAddedToLibrary": "{0} je bil dodan vaši knjižnici", - "ValueSpecialEpisodeName": "Special - {0}", - "VersionNumber": "Version {0}" + "ValueSpecialEpisodeName": "Poseben - {0}", + "VersionNumber": "Različica {0}" } diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index 9e00eba62..3b5ce29c7 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -3,28 +3,28 @@ "AppDeviceValues": "Uygulama: {0}, Aygıt: {1}", "Application": "Uygulama", "Artists": "Sanatçılar", - "AuthenticationSucceededWithUserName": "{0} başarı ile giriş yaptı", + "AuthenticationSucceededWithUserName": "{0} kimlik başarıyla doğrulandı", "Books": "Kitaplar", - "CameraImageUploadedFrom": "A new camera image has been uploaded from {0}", + "CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi", "Channels": "Kanallar", - "ChapterNameValue": "Chapter {0}", - "Collections": "Collections", - "DeviceOfflineWithName": "{0} has disconnected", - "DeviceOnlineWithName": "{0} is connected", - "FailedLoginAttemptWithUserName": "Failed login attempt from {0}", - "Favorites": "Favorites", - "Folders": "Folders", - "Genres": "Genres", - "HeaderAlbumArtists": "Album Artists", - "HeaderCameraUploads": "Camera Uploads", + "ChapterNameValue": "Bölüm {0}", + "Collections": "Koleksiyonlar", + "DeviceOfflineWithName": "{0} bağlantısı kesildi", + "DeviceOnlineWithName": "{0} bağlı", + "FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu", + "Favorites": "Favoriler", + "Folders": "Klasörler", + "Genres": "Türler", + "HeaderAlbumArtists": "Albüm Sanatçıları", + "HeaderCameraUploads": "Kamera Yüklemeleri", "HeaderContinueWatching": "İzlemeye Devam Et", "HeaderFavoriteAlbums": "Favori Albümler", - "HeaderFavoriteArtists": "Favorite Artists", - "HeaderFavoriteEpisodes": "Favorite Episodes", - "HeaderFavoriteShows": "Favori Showlar", - "HeaderFavoriteSongs": "Favorite Songs", - "HeaderLiveTV": "Live TV", - "HeaderNextUp": "Next Up", + "HeaderFavoriteArtists": "Favori Sanatçılar", + "HeaderFavoriteEpisodes": "Favori Bölümler", + "HeaderFavoriteShows": "Favori Diziler", + "HeaderFavoriteSongs": "Favori Şarkılar", + "HeaderLiveTV": "Canlı TV", + "HeaderNextUp": "Sonraki hafta", "HeaderRecordingGroups": "Recording Groups", "HomeVideos": "Home videos", "Inherit": "Inherit", @@ -38,7 +38,7 @@ "MessageNamedServerConfigurationUpdatedWithValue": "Server configuration section {0} has been updated", "MessageServerConfigurationUpdated": "Server configuration has been updated", "MixedContent": "Mixed content", - "Movies": "Movies", + "Movies": "Filmler", "Music": "Müzik", "MusicVideos": "Müzik videoları", "NameInstallFailed": "{0} kurulum başarısız", @@ -50,7 +50,7 @@ "NotificationOptionAudioPlayback": "Audio playback started", "NotificationOptionAudioPlaybackStopped": "Audio playback stopped", "NotificationOptionCameraImageUploaded": "Camera image uploaded", - "NotificationOptionInstallationFailed": "Installation failure", + "NotificationOptionInstallationFailed": "Yükleme hatası", "NotificationOptionNewLibraryContent": "New content added", "NotificationOptionPluginError": "Plugin failure", "NotificationOptionPluginInstalled": "Plugin installed", @@ -61,8 +61,8 @@ "NotificationOptionUserLockedOut": "User locked out", "NotificationOptionVideoPlayback": "Video playback started", "NotificationOptionVideoPlaybackStopped": "Video playback stopped", - "Photos": "Photos", - "Playlists": "Playlists", + "Photos": "Fotoğraflar", + "Playlists": "Çalma listeleri", "Plugin": "Plugin", "PluginInstalledWithName": "{0} was installed", "PluginUninstalledWithName": "{0} was uninstalled", @@ -71,13 +71,13 @@ "ScheduledTaskFailedWithName": "{0} failed", "ScheduledTaskStartedWithName": "{0} started", "ServerNameNeedsToBeRestarted": "{0} needs to be restarted", - "Shows": "Shows", - "Songs": "Songs", + "Shows": "Diziler", + "Songs": "Şarkılar", "StartupEmbyServerIsLoading": "Jellyfin Server is loading. Please try again shortly.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Subtitles failed to download from {0} for {1}", "SubtitlesDownloadedForItem": "Subtitles downloaded for {0}", - "Sync": "Sync", + "Sync": "Eşitle", "System": "System", "TvShows": "TV Shows", "User": "User", @@ -92,6 +92,6 @@ "UserStartedPlayingItemWithValues": "{0} is playing {1} on {2}", "UserStoppedPlayingItemWithValues": "{0} has finished playing {1} on {2}", "ValueHasBeenAddedToLibrary": "{0} has been added to your media library", - "ValueSpecialEpisodeName": "Special - {0}", + "ValueSpecialEpisodeName": "Özel -{0}", "VersionNumber": "Version {0}" } diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index 63aa6a557..ba5e93982 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -3,7 +3,7 @@ "AppDeviceValues": "应用: {0}, 设备: {1}", "Application": "应用程序", "Artists": "艺术家", - "AuthenticationSucceededWithUserName": "{0} 成功验证", + "AuthenticationSucceededWithUserName": "{0} 认证成功", "Books": "书籍", "CameraImageUploadedFrom": "已从 {0} 上传了一张新的相机图像", "Channels": "频道", @@ -12,15 +12,15 @@ "DeviceOfflineWithName": "{0} 已断开", "DeviceOnlineWithName": "{0} 已连接", "FailedLoginAttemptWithUserName": "来自 {0} 的失败登入", - "Favorites": "最爱", + "Favorites": "我的最爱", "Folders": "文件夹", "Genres": "风格", "HeaderAlbumArtists": "专辑作家", "HeaderCameraUploads": "相机上传", "HeaderContinueWatching": "继续观看", "HeaderFavoriteAlbums": "最爱的专辑", - "HeaderFavoriteArtists": "最爱作家", - "HeaderFavoriteEpisodes": "最爱的集", + "HeaderFavoriteArtists": "最爱的艺术家", + "HeaderFavoriteEpisodes": "最爱的剧集", "HeaderFavoriteShows": "最爱的节目", "HeaderFavoriteSongs": "最爱的歌曲", "HeaderLiveTV": "电视直播", @@ -30,7 +30,7 @@ "Inherit": "继承", "ItemAddedWithName": "{0} 已添加到媒体库", "ItemRemovedWithName": "{0} 已从媒体库中移除", - "LabelIpAddressValue": "Ip 地址:{0}", + "LabelIpAddressValue": "IP 地址:{0}", "LabelRunningTimeValue": "运行时间:{0}", "Latest": "最新", "MessageApplicationUpdated": "Jellyfin 服务器已更新", diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 8c49b6405..13cdc50ca 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -17,43 +17,49 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Localization { /// <summary> - /// Class LocalizationManager + /// Class LocalizationManager. /// </summary> public class LocalizationManager : ILocalizationManager { - /// <summary> - /// The _configuration manager - /// </summary> - private readonly IServerConfigurationManager _configurationManager; + private const string DefaultCulture = "en-US"; + private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly; + private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" }; /// <summary> - /// The us culture + /// The _configuration manager. /// </summary> - private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); + private readonly IServerConfigurationManager _configurationManager; + private readonly IJsonSerializer _jsonSerializer; + private readonly ILogger _logger; private readonly Dictionary<string, Dictionary<string, ParentalRating>> _allParentalRatings = new Dictionary<string, Dictionary<string, ParentalRating>>(StringComparer.OrdinalIgnoreCase); - private readonly IJsonSerializer _jsonSerializer; - private readonly ILogger _logger; - private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly; + private readonly ConcurrentDictionary<string, Dictionary<string, string>> _dictionaries = + new ConcurrentDictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase); + + private List<CultureDto> _cultures; /// <summary> /// Initializes a new instance of the <see cref="LocalizationManager" /> class. /// </summary> /// <param name="configurationManager">The configuration manager.</param> /// <param name="jsonSerializer">The json serializer.</param> - /// <param name="loggerFactory">The logger factory</param> + /// <param name="logger">The logger.</param> public LocalizationManager( IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, - ILoggerFactory loggerFactory) + ILogger<LocalizationManager> logger) { _configurationManager = configurationManager; _jsonSerializer = jsonSerializer; - _logger = loggerFactory.CreateLogger(nameof(LocalizationManager)); + _logger = logger; } + /// <summary> + /// Loads all resources into memory. + /// </summary> + /// <returns><see cref="Task" />.</returns> public async Task LoadAll() { const string RatingsResource = "Emby.Server.Implementations.Localization.Ratings."; @@ -82,9 +88,10 @@ namespace Emby.Server.Implementations.Localization string[] parts = line.Split(','); if (parts.Length == 2 - && int.TryParse(parts[1], NumberStyles.Integer, UsCulture, out var value)) + && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) { - dict.Add(parts[0], new ParentalRating { Name = parts[0], Value = value }); + var name = parts[0]; + dict.Add(name, new ParentalRating(name, value)); } #if DEBUG else @@ -101,16 +108,11 @@ namespace Emby.Server.Implementations.Localization await LoadCultures().ConfigureAwait(false); } - public string NormalizeFormKD(string text) - => text.Normalize(NormalizationForm.FormKD); - - private CultureDto[] _cultures; - /// <summary> /// Gets the cultures. /// </summary> - /// <returns>IEnumerable{CultureDto}.</returns> - public CultureDto[] GetCultures() + /// <returns><see cref="IEnumerable{CultureDto}" />.</returns> + public IEnumerable<CultureDto> GetCultures() => _cultures; private async Task LoadCultures() @@ -168,9 +170,10 @@ namespace Emby.Server.Implementations.Localization } } - _cultures = list.ToArray(); + _cultures = list; } + /// <inheritdoc /> public CultureDto FindLanguageInfo(string language) => GetCultures() .FirstOrDefault(i => @@ -179,25 +182,19 @@ namespace Emby.Server.Implementations.Localization || i.ThreeLetterISOLanguageNames.Contains(language, StringComparer.OrdinalIgnoreCase) || string.Equals(i.TwoLetterISOLanguageName, language, StringComparison.OrdinalIgnoreCase)); - /// <summary> - /// Gets the countries. - /// </summary> - /// <returns>IEnumerable{CountryInfo}.</returns> - public Task<CountryInfo[]> GetCountries() - => _jsonSerializer.DeserializeFromStreamAsync<CountryInfo[]>( + /// <inheritdoc /> + public IEnumerable<CountryInfo> GetCountries() + => _jsonSerializer.DeserializeFromStream<IEnumerable<CountryInfo>>( _assembly.GetManifestResourceStream("Emby.Server.Implementations.Localization.countries.json")); - /// <summary> - /// Gets the parental ratings. - /// </summary> - /// <returns>IEnumerable{ParentalRating}.</returns> + /// <inheritdoc /> public IEnumerable<ParentalRating> GetParentalRatings() => GetParentalRatingsDictionary().Values; /// <summary> /// Gets the parental ratings dictionary. /// </summary> - /// <returns>Dictionary{System.StringParentalRating}.</returns> + /// <returns><see cref="Dictionary{String, ParentalRating}" />.</returns> private Dictionary<string, ParentalRating> GetParentalRatingsDictionary() { var countryCode = _configurationManager.Configuration.MetadataCountryCode; @@ -207,14 +204,14 @@ namespace Emby.Server.Implementations.Localization countryCode = "us"; } - return GetRatings(countryCode) ?? GetRatings("us"); + return GetRatings(countryCode) ?? GetRatings("us"); } /// <summary> /// Gets the ratings. /// </summary> /// <param name="countryCode">The country code.</param> - /// <returns>The ratings</returns> + /// <returns>The ratings.</returns> private Dictionary<string, ParentalRating> GetRatings(string countryCode) { _allParentalRatings.TryGetValue(countryCode, out var value); @@ -222,14 +219,7 @@ namespace Emby.Server.Implementations.Localization return value; } - private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" }; - /// <inheritdoc /> - /// <summary> - /// Gets the rating level. - /// </summary> - /// <param name="rating">Rating field</param> - /// <returns>The rating level</returns>> public int? GetRatingLevel(string rating) { if (string.IsNullOrEmpty(rating)) @@ -277,6 +267,7 @@ namespace Emby.Server.Implementations.Localization return null; } + /// <inheritdoc /> public bool HasUnicodeCategory(string value, UnicodeCategory category) { foreach (var chr in value) @@ -290,11 +281,13 @@ namespace Emby.Server.Implementations.Localization return false; } + /// <inheritdoc /> public string GetLocalizedString(string phrase) { return GetLocalizedString(phrase, _configurationManager.Configuration.UICulture); } + /// <inheritdoc /> public string GetLocalizedString(string phrase, string culture) { if (string.IsNullOrEmpty(culture)) @@ -317,12 +310,7 @@ namespace Emby.Server.Implementations.Localization return phrase; } - private const string DefaultCulture = "en-US"; - - private readonly ConcurrentDictionary<string, Dictionary<string, string>> _dictionaries = - new ConcurrentDictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase); - - public Dictionary<string, string> GetLocalizationDictionary(string culture) + private Dictionary<string, string> GetLocalizationDictionary(string culture) { if (string.IsNullOrEmpty(culture)) { @@ -332,8 +320,9 @@ namespace Emby.Server.Implementations.Localization const string prefix = "Core"; var key = prefix + culture; - return _dictionaries.GetOrAdd(key, - f => GetDictionary(prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult()); + return _dictionaries.GetOrAdd( + key, + f => GetDictionary(prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult()); } private async Task<Dictionary<string, string>> GetDictionary(string prefix, string culture, string baseFilename) @@ -390,45 +379,45 @@ namespace Emby.Server.Implementations.Localization return culture + ".json"; } - public LocalizationOption[] GetLocalizationOptions() - => new LocalizationOption[] - { - new LocalizationOption("Arabic", "ar"), - new LocalizationOption("Bulgarian (Bulgaria)", "bg-BG"), - new LocalizationOption("Catalan", "ca"), - new LocalizationOption("Chinese Simplified", "zh-CN"), - new LocalizationOption("Chinese Traditional", "zh-TW"), - new LocalizationOption("Croatian", "hr"), - new LocalizationOption("Czech", "cs"), - new LocalizationOption("Danish", "da"), - new LocalizationOption("Dutch", "nl"), - new LocalizationOption("English (United Kingdom)", "en-GB"), - new LocalizationOption("English (United States)", "en-US"), - new LocalizationOption("French", "fr"), - new LocalizationOption("French (Canada)", "fr-CA"), - new LocalizationOption("German", "de"), - new LocalizationOption("Greek", "el"), - new LocalizationOption("Hebrew", "he"), - new LocalizationOption("Hungarian", "hu"), - new LocalizationOption("Italian", "it"), - new LocalizationOption("Kazakh", "kk"), - new LocalizationOption("Korean", "ko"), - new LocalizationOption("Lithuanian", "lt-LT"), - new LocalizationOption("Malay", "ms"), - new LocalizationOption("Norwegian Bokmål", "nb"), - new LocalizationOption("Persian", "fa"), - new LocalizationOption("Polish", "pl"), - new LocalizationOption("Portuguese (Brazil)", "pt-BR"), - new LocalizationOption("Portuguese (Portugal)", "pt-PT"), - new LocalizationOption("Russian", "ru"), - new LocalizationOption("Slovak", "sk"), - new LocalizationOption("Slovenian (Slovenia)", "sl-SI"), - new LocalizationOption("Spanish", "es"), - new LocalizationOption("Spanish (Argentina)", "es-AR"), - new LocalizationOption("Spanish (Mexico)", "es-MX"), - new LocalizationOption("Swedish", "sv"), - new LocalizationOption("Swiss German", "gsw"), - new LocalizationOption("Turkish", "tr") - }; + /// <inheritdoc /> + public IEnumerable<LocalizationOption> GetLocalizationOptions() + { + yield return new LocalizationOption("Arabic", "ar"); + yield return new LocalizationOption("Bulgarian (Bulgaria)", "bg-BG"); + yield return new LocalizationOption("Catalan", "ca"); + yield return new LocalizationOption("Chinese Simplified", "zh-CN"); + yield return new LocalizationOption("Chinese Traditional", "zh-TW"); + yield return new LocalizationOption("Croatian", "hr"); + yield return new LocalizationOption("Czech", "cs"); + yield return new LocalizationOption("Danish", "da"); + yield return new LocalizationOption("Dutch", "nl"); + yield return new LocalizationOption("English (United Kingdom)", "en-GB"); + yield return new LocalizationOption("English (United States)", "en-US"); + yield return new LocalizationOption("French", "fr"); + yield return new LocalizationOption("French (Canada)", "fr-CA"); + yield return new LocalizationOption("German", "de"); + yield return new LocalizationOption("Greek", "el"); + yield return new LocalizationOption("Hebrew", "he"); + yield return new LocalizationOption("Hungarian", "hu"); + yield return new LocalizationOption("Italian", "it"); + yield return new LocalizationOption("Kazakh", "kk"); + yield return new LocalizationOption("Korean", "ko"); + yield return new LocalizationOption("Lithuanian", "lt-LT"); + yield return new LocalizationOption("Malay", "ms"); + yield return new LocalizationOption("Norwegian Bokmål", "nb"); + yield return new LocalizationOption("Persian", "fa"); + yield return new LocalizationOption("Polish", "pl"); + yield return new LocalizationOption("Portuguese (Brazil)", "pt-BR"); + yield return new LocalizationOption("Portuguese (Portugal)", "pt-PT"); + yield return new LocalizationOption("Russian", "ru"); + yield return new LocalizationOption("Slovak", "sk"); + yield return new LocalizationOption("Slovenian (Slovenia)", "sl-SI"); + yield return new LocalizationOption("Spanish", "es"); + yield return new LocalizationOption("Spanish (Argentina)", "es-AR"); + yield return new LocalizationOption("Spanish (Mexico)", "es-MX"); + yield return new LocalizationOption("Swedish", "sv"); + yield return new LocalizationOption("Swiss German", "gsw"); + yield return new LocalizationOption("Turkish", "tr"); + } } } diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index 52d07d784..840aca7a6 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -139,7 +139,7 @@ namespace Emby.Server.Implementations.MediaEncoder var protocol = MediaProtocol.File; - var inputPath = MediaEncoderHelpers.GetInputArgument(_fileSystem, video.Path, protocol, null, Array.Empty<string>()); + var inputPath = MediaEncoderHelpers.GetInputArgument(_fileSystem, video.Path, null, Array.Empty<string>()); Directory.CreateDirectory(Path.GetDirectoryName(path)); diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs index 42ffa4e22..0870db003 100644 --- a/Emby.Server.Implementations/Net/SocketFactory.cs +++ b/Emby.Server.Implementations/Net/SocketFactory.cs @@ -2,54 +2,12 @@ using System; using System.IO; using System.Net; using System.Net.Sockets; -using Emby.Server.Implementations.Networking; using MediaBrowser.Model.Net; namespace Emby.Server.Implementations.Net { public class SocketFactory : ISocketFactory { - // THIS IS A LINKED FILE - SHARED AMONGST MULTIPLE PLATFORMS - // Be careful to check any changes compile and work for all platform projects it is shared in. - - // Not entirely happy with this. Would have liked to have done something more generic/reusable, - // but that wasn't really the point so kept to YAGNI principal for now, even if the - // interfaces are a bit ugly, specific and make assumptions. - - public ISocket CreateTcpSocket(IPAddress remoteAddress, int remotePort) - { - if (remotePort < 0) - { - throw new ArgumentException("remotePort cannot be less than zero.", nameof(remotePort)); - } - - var addressFamily = remoteAddress.AddressFamily == AddressFamily.InterNetwork - ? AddressFamily.InterNetwork - : AddressFamily.InterNetworkV6; - - var retVal = new Socket(addressFamily, System.Net.Sockets.SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp); - - try - { - retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - } - catch (SocketException) - { - // This is not supported on all operating systems (qnap) - } - - try - { - return new UdpSocket(retVal, new IPEndPoint(remoteAddress, remotePort)); - } - catch - { - retVal?.Dispose(); - - throw; - } - } - /// <summary> /// Creates a new UDP acceptSocket and binds it to the specified local port. /// </summary> @@ -61,7 +19,8 @@ namespace Emby.Server.Implementations.Net throw new ArgumentException("localPort cannot be less than zero.", nameof(localPort)); } - var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp); + var retVal = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + try { retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs index 82add242a..7d85a0666 100644 --- a/Emby.Server.Implementations/Networking/NetworkManager.cs +++ b/Emby.Server.Implementations/Networking/NetworkManager.cs @@ -95,9 +95,8 @@ namespace Emby.Server.Implementations.Networking private static bool FilterIpAddress(IPAddress address) { - var addressString = address.ToString(); - - if (addressString.StartsWith("169.", StringComparison.OrdinalIgnoreCase)) + if (address.IsIPv6LinkLocal + || address.ToString().StartsWith("169.", StringComparison.OrdinalIgnoreCase)) { return false; } @@ -426,47 +425,27 @@ namespace Emby.Server.Implementations.Networking var localEndPoint = new IPEndPoint(IPAddress.Any, 0); using (var udpClient = new UdpClient(localEndPoint)) { - var port = ((IPEndPoint)(udpClient.Client.LocalEndPoint)).Port; + var port = ((IPEndPoint)udpClient.Client.LocalEndPoint).Port; return port; } } - private List<string> _macAddresses; - public List<string> GetMacAddresses() + private List<PhysicalAddress> _macAddresses; + public List<PhysicalAddress> GetMacAddresses() { if (_macAddresses == null) { - _macAddresses = GetMacAddressesInternal(); + _macAddresses = GetMacAddressesInternal().ToList(); } + return _macAddresses; } - private static List<string> GetMacAddressesInternal() - { - return NetworkInterface.GetAllNetworkInterfaces() + private static IEnumerable<PhysicalAddress> GetMacAddressesInternal() + => NetworkInterface.GetAllNetworkInterfaces() .Where(i => i.NetworkInterfaceType != NetworkInterfaceType.Loopback) - .Select(i => - { - try - { - var physicalAddress = i.GetPhysicalAddress(); - - if (physicalAddress == null) - { - return null; - } - - return physicalAddress.ToString(); - } - catch (Exception) - { - //TODO Log exception. - return null; - } - }) - .Where(i => i != null) - .ToList(); - } + .Select(x => x.GetPhysicalAddress()) + .Where(x => x != null && x != PhysicalAddress.None); /// <summary> /// Parses the specified endpointstring. diff --git a/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs index f4decc856..de51a37ab 100644 --- a/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs +++ b/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs @@ -1,9 +1,9 @@ using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Playlists; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; namespace Emby.Server.Implementations.Playlists { @@ -24,13 +24,13 @@ namespace Emby.Server.Implementations.Playlists return base.GetEligibleChildrenForRecursiveChildren(user).OfType<Playlist>(); } - [IgnoreDataMember] + [JsonIgnore] public override bool IsHidden => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => false; - [IgnoreDataMember] + [JsonIgnore] public override string CollectionType => MediaBrowser.Model.Entities.CollectionType.Playlists; protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 29836e0bf..40b568b40 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading; @@ -129,7 +130,7 @@ namespace Emby.Server.Implementations.Playlists { new Share { - UserId = options.UserId.Equals(Guid.Empty) ? null : options.UserId.ToString("N"), + UserId = options.UserId.Equals(Guid.Empty) ? null : options.UserId.ToString("N", CultureInfo.InvariantCulture), CanEdit = true } } @@ -144,7 +145,7 @@ namespace Emby.Server.Implementations.Playlists if (options.ItemIdList.Length > 0) { - AddToPlaylistInternal(playlist.Id.ToString("N"), options.ItemIdList, user, new DtoOptions(false) + AddToPlaylistInternal(playlist.Id.ToString("N", CultureInfo.InvariantCulture), options.ItemIdList, user, new DtoOptions(false) { EnableImages = true }); @@ -152,7 +153,7 @@ namespace Emby.Server.Implementations.Playlists return new PlaylistCreationResult { - Id = playlist.Id.ToString("N") + Id = playlist.Id.ToString("N", CultureInfo.InvariantCulture) }; } finally diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index 08bb39578..83226b07f 100644 --- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading; @@ -287,7 +287,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { if (_id == null) { - _id = ScheduledTask.GetType().FullName.GetMD5().ToString("N"); + _id = ScheduledTask.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture); } return _id; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs new file mode 100644 index 000000000..c343a7d48 --- /dev/null +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Tasks; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.ScheduledTasks.Tasks +{ + /// <summary> + /// Deletes all transcoding temp files + /// </summary> + public class DeleteTranscodeFileTask : IScheduledTask, IConfigurableScheduledTask + { + /// <summary> + /// Gets or sets the application paths. + /// </summary> + /// <value>The application paths.</value> + private ServerApplicationPaths ApplicationPaths { get; set; } + + private readonly ILogger _logger; + + private readonly IFileSystem _fileSystem; + + /// <summary> + /// Initializes a new instance of the <see cref="DeleteTranscodeFileTask" /> class. + /// </summary> + public DeleteTranscodeFileTask(ServerApplicationPaths appPaths, ILogger logger, IFileSystem fileSystem) + { + ApplicationPaths = appPaths; + _logger = logger; + _fileSystem = fileSystem; + } + + /// <summary> + /// Creates the triggers that define when the task will run + /// </summary> + /// <returns>IEnumerable{BaseTaskTrigger}.</returns> + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() => new List<TaskTriggerInfo>(); + + /// <summary> + /// Returns the task to be executed + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="progress">The progress.</param> + /// <returns>Task.</returns> + public Task Execute(CancellationToken cancellationToken, IProgress<double> progress) + { + var minDateModified = DateTime.UtcNow.AddDays(-1); + progress.Report(50); + + try + { + DeleteTempFilesFromDirectory(cancellationToken, ApplicationPaths.TranscodingTempPath, minDateModified, progress); + } + catch (DirectoryNotFoundException) + { + // No biggie here. Nothing to delete + } + + return Task.CompletedTask; + } + + + /// <summary> + /// Deletes the transcoded temp files from directory with a last write time less than a given date + /// </summary> + /// <param name="cancellationToken">The task cancellation token.</param> + /// <param name="directory">The directory.</param> + /// <param name="minDateModified">The min date modified.</param> + /// <param name="progress">The progress.</param> + private void DeleteTempFilesFromDirectory(CancellationToken cancellationToken, string directory, DateTime minDateModified, IProgress<double> progress) + { + var filesToDelete = _fileSystem.GetFiles(directory, true) + .Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified) + .ToList(); + + var index = 0; + + foreach (var file in filesToDelete) + { + double percent = index; + percent /= filesToDelete.Count; + + progress.Report(100 * percent); + + cancellationToken.ThrowIfCancellationRequested(); + + DeleteFile(file.FullName); + + index++; + } + + DeleteEmptyFolders(directory); + + progress.Report(100); + } + + private void DeleteEmptyFolders(string parent) + { + foreach (var directory in _fileSystem.GetDirectoryPaths(parent)) + { + DeleteEmptyFolders(directory); + if (!_fileSystem.GetFileSystemEntryPaths(directory).Any()) + { + try + { + Directory.Delete(directory, false); + } + catch (UnauthorizedAccessException ex) + { + _logger.LogError(ex, "Error deleting directory {path}", directory); + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting directory {path}", directory); + } + } + } + } + + private void DeleteFile(string path) + { + try + { + _fileSystem.DeleteFile(path); + } + catch (UnauthorizedAccessException ex) + { + _logger.LogError(ex, "Error deleting file {path}", path); + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting file {path}", path); + } + } + + public string Name => "Transcoding temp cleanup"; + + public string Description => "Deletes transcoding temp files older than 24 hours."; + + public string Category => "Maintenance"; + + public string Key => "DeleteTranscodingTempFiles"; + + public bool IsHidden => false; + + public bool IsEnabled => false; + + public bool IsLogged => true; + } +} diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs index c6431c311..7afeba9dd 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs @@ -1,4 +1,3 @@ -using MediaBrowser.Common; using MediaBrowser.Common.Updates; using MediaBrowser.Model.Net; using System; @@ -25,13 +24,10 @@ namespace Emby.Server.Implementations.ScheduledTasks private readonly IInstallationManager _installationManager; - private readonly IApplicationHost _appHost; - - public PluginUpdateTask(ILogger logger, IInstallationManager installationManager, IApplicationHost appHost) + public PluginUpdateTask(ILogger logger, IInstallationManager installationManager) { _logger = logger; _installationManager = installationManager; - _appHost = appHost; } /// <summary> @@ -40,14 +36,11 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <returns>IEnumerable{BaseTaskTrigger}.</returns> public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() { - return new[] { - - // At startup - new TaskTriggerInfo {Type = TaskTriggerInfo.TriggerStartup}, - - // Every so often - new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks} - }; + // At startup + yield return new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerStartup }; + + // Every so often + yield return new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }; } /// <summary> @@ -72,7 +65,7 @@ namespace Emby.Server.Implementations.ScheduledTasks try { - await _installationManager.InstallPackage(package, true, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false); + await _installationManager.InstallPackage(package, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -94,8 +87,7 @@ namespace Emby.Server.Implementations.ScheduledTasks // Update progress lock (progress) { - numComplete++; - progress.Report(90.0 * numComplete / packagesToInstall.Count + 10); + progress.Report((90.0 * ++numComplete / packagesToInstall.Count) + 10); } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs index 1a3d85ad7..3e6d251c9 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs @@ -10,7 +10,7 @@ using MediaBrowser.Model.Tasks; namespace Emby.Server.Implementations.ScheduledTasks { /// <summary> - /// Class RefreshMediaLibraryTask + /// Class RefreshMediaLibraryTask. /// </summary> public class RefreshMediaLibraryTask : IScheduledTask { @@ -31,15 +31,14 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// <summary> - /// Creates the triggers that define when the task will run + /// Creates the triggers that define when the task will run. /// </summary> /// <returns>IEnumerable{BaseTaskTrigger}.</returns> public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() { - return new[] { - - // Every so often - new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(12).Ticks} + yield return new TaskTriggerInfo + { + Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(12).Ticks }; } @@ -60,7 +59,7 @@ namespace Emby.Server.Implementations.ScheduledTasks public string Name => "Scan media library"; - public string Description => "Scans your media library and refreshes metatata based on configuration."; + public string Description => "Scans your media library for new files and refreshes metadata."; public string Category => "Library"; diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs index 545e11bf9..1ef5c4b99 100644 --- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs +++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs @@ -23,22 +23,22 @@ namespace Emby.Server.Implementations.Security public void Initialize() { - using (var connection = GetConnection()) + string[] queries = { - var tableNewlyCreated = !TableExists(connection, "Tokens"); + "create table if not exists Tokens (Id INTEGER PRIMARY KEY, AccessToken TEXT NOT NULL, DeviceId TEXT NOT NULL, AppName TEXT NOT NULL, AppVersion TEXT NOT NULL, DeviceName TEXT NOT NULL, UserId TEXT, UserName TEXT, IsActive BIT NOT NULL, DateCreated DATETIME NOT NULL, DateLastActivity DATETIME NOT NULL)", + "create table if not exists Devices (Id TEXT NOT NULL PRIMARY KEY, CustomName TEXT, Capabilities TEXT)", + "drop index if exists idx_AccessTokens", + "drop index if exists Tokens1", + "drop index if exists Tokens2", - string[] queries = { - - "create table if not exists Tokens (Id INTEGER PRIMARY KEY, AccessToken TEXT NOT NULL, DeviceId TEXT NOT NULL, AppName TEXT NOT NULL, AppVersion TEXT NOT NULL, DeviceName TEXT NOT NULL, UserId TEXT, UserName TEXT, IsActive BIT NOT NULL, DateCreated DATETIME NOT NULL, DateLastActivity DATETIME NOT NULL)", - "create table if not exists Devices (Id TEXT NOT NULL PRIMARY KEY, CustomName TEXT, Capabilities TEXT)", + "create index if not exists Tokens3 on Tokens (AccessToken, DateLastActivity)", + "create index if not exists Tokens4 on Tokens (Id, DateLastActivity)", + "create index if not exists Devices1 on Devices (Id)" + }; - "drop index if exists idx_AccessTokens", - "drop index if exists Tokens1", - "drop index if exists Tokens2", - "create index if not exists Tokens3 on Tokens (AccessToken, DateLastActivity)", - "create index if not exists Tokens4 on Tokens (Id, DateLastActivity)", - "create index if not exists Devices1 on Devices (Id)" - }; + using (var connection = GetConnection()) + { + var tableNewlyCreated = !TableExists(connection, "Tokens"); connection.RunQueries(queries); @@ -97,7 +97,7 @@ namespace Emby.Server.Implementations.Security statement.TryBind("@AppName", info.AppName); statement.TryBind("@AppVersion", info.AppVersion); statement.TryBind("@DeviceName", info.DeviceName); - statement.TryBind("@UserId", (info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N"))); + statement.TryBind("@UserId", (info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N", CultureInfo.InvariantCulture))); statement.TryBind("@UserName", info.UserName); statement.TryBind("@IsActive", true); statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue()); @@ -131,7 +131,7 @@ namespace Emby.Server.Implementations.Security statement.TryBind("@AppName", info.AppName); statement.TryBind("@AppVersion", info.AppVersion); statement.TryBind("@DeviceName", info.DeviceName); - statement.TryBind("@UserId", (info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N"))); + statement.TryBind("@UserId", (info.UserId.Equals(Guid.Empty) ? null : info.UserId.ToString("N", CultureInfo.InvariantCulture))); statement.TryBind("@UserName", info.UserName); statement.TryBind("@DateCreated", info.DateCreated.ToDateTimeParamValue()); statement.TryBind("@DateLastActivity", info.DateLastActivity.ToDateTimeParamValue()); @@ -174,7 +174,7 @@ namespace Emby.Server.Implementations.Security if (!query.UserId.Equals(Guid.Empty)) { - statement.TryBind("@UserId", query.UserId.ToString("N")); + statement.TryBind("@UserId", query.UserId.ToString("N", CultureInfo.InvariantCulture)); } if (!string.IsNullOrEmpty(query.DeviceId)) @@ -244,45 +244,46 @@ namespace Emby.Server.Implementations.Security } } - var list = new List<AuthenticationInfo>(); + var statementTexts = new[] + { + commandText, + "select count (Id) from Tokens" + whereTextWithoutPaging + }; + var list = new List<AuthenticationInfo>(); + var result = new QueryResult<AuthenticationInfo>(); using (var connection = GetConnection(true)) { - return connection.RunInTransaction(db => - { - var result = new QueryResult<AuthenticationInfo>(); - - var statementTexts = new List<string>(); - statementTexts.Add(commandText); - statementTexts.Add("select count (Id) from Tokens" + whereTextWithoutPaging); - - var statements = PrepareAll(db, statementTexts) - .ToList(); - - using (var statement = statements[0]) + connection.RunInTransaction( + db => { - BindAuthenticationQueryParams(query, statement); + var statements = PrepareAll(db, statementTexts) + .ToList(); - foreach (var row in statement.ExecuteQuery()) + using (var statement = statements[0]) { - list.Add(Get(row)); - } + BindAuthenticationQueryParams(query, statement); - using (var totalCountStatement = statements[1]) - { - BindAuthenticationQueryParams(query, totalCountStatement); - - result.TotalRecordCount = totalCountStatement.ExecuteQuery() - .SelectScalarInt() - .First(); - } - } + foreach (var row in statement.ExecuteQuery()) + { + list.Add(Get(row)); + } - result.Items = list.ToArray(); - return result; + using (var totalCountStatement = statements[1]) + { + BindAuthenticationQueryParams(query, totalCountStatement); - }, ReadTransactionMode); + result.TotalRecordCount = totalCountStatement.ExecuteQuery() + .SelectScalarInt() + .First(); + } + } + }, + ReadTransactionMode); } + + result.Items = list.ToArray(); + return result; } private static AuthenticationInfo Get(IReadOnlyList<IResultSetValue> reader) diff --git a/Emby.Server.Implementations/Serialization/JsonSerializer.cs b/Emby.Server.Implementations/Serialization/JsonSerializer.cs index 8ae7fd90c..36196ee36 100644 --- a/Emby.Server.Implementations/Serialization/JsonSerializer.cs +++ b/Emby.Server.Implementations/Serialization/JsonSerializer.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.IO; using System.Threading.Tasks; using MediaBrowser.Model.IO; @@ -245,7 +246,7 @@ namespace Emby.Server.Implementations.Serialization return null; } - return guid.ToString("N"); + return guid.ToString("N", CultureInfo.InvariantCulture); } /// <summary> diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs index adaf23234..2f5a8af80 100644 --- a/Emby.Server.Implementations/ServerApplicationPaths.cs +++ b/Emby.Server.Implementations/ServerApplicationPaths.cs @@ -10,8 +10,12 @@ namespace Emby.Server.Implementations /// </summary> public class ServerApplicationPaths : BaseApplicationPaths, IServerApplicationPaths { + private string _defaultTranscodingTempPath; + private string _transcodingTempPath; + private string _internalMetadataPath; + /// <summary> - /// Initializes a new instance of the <see cref="BaseApplicationPaths" /> class. + /// Initializes a new instance of the <see cref="ServerApplicationPaths" /> class. /// </summary> public ServerApplicationPaths( string programDataPath, @@ -30,7 +34,7 @@ namespace Emby.Server.Implementations public string ApplicationResourcesPath { get; } = AppContext.BaseDirectory; /// <summary> - /// Gets the path to the base root media directory + /// Gets the path to the base root media directory. /// </summary> /// <value>The root folder path.</value> public string RootFolderPath => Path.Combine(ProgramDataPath, "root"); @@ -48,7 +52,7 @@ namespace Emby.Server.Implementations public string LocalizationPath => Path.Combine(ProgramDataPath, "localization"); /// <summary> - /// Gets the path to the People directory + /// Gets the path to the People directory. /// </summary> /// <value>The people path.</value> public string PeoplePath => Path.Combine(InternalMetadataPath, "People"); @@ -56,37 +60,37 @@ namespace Emby.Server.Implementations public string ArtistsPath => Path.Combine(InternalMetadataPath, "artists"); /// <summary> - /// Gets the path to the Genre directory + /// Gets the path to the Genre directory. /// </summary> /// <value>The genre path.</value> public string GenrePath => Path.Combine(InternalMetadataPath, "Genre"); /// <summary> - /// Gets the path to the Genre directory + /// Gets the path to the Genre directory. /// </summary> /// <value>The genre path.</value> public string MusicGenrePath => Path.Combine(InternalMetadataPath, "MusicGenre"); /// <summary> - /// Gets the path to the Studio directory + /// Gets the path to the Studio directory. /// </summary> /// <value>The studio path.</value> public string StudioPath => Path.Combine(InternalMetadataPath, "Studio"); /// <summary> - /// Gets the path to the Year directory + /// Gets the path to the Year directory. /// </summary> /// <value>The year path.</value> public string YearPath => Path.Combine(InternalMetadataPath, "Year"); /// <summary> - /// Gets the path to the General IBN directory + /// Gets the path to the General IBN directory. /// </summary> /// <value>The general path.</value> public string GeneralPath => Path.Combine(InternalMetadataPath, "general"); /// <summary> - /// Gets the path to the Ratings IBN directory + /// Gets the path to the Ratings IBN directory. /// </summary> /// <value>The ratings path.</value> public string RatingsPath => Path.Combine(InternalMetadataPath, "ratings"); @@ -98,15 +102,13 @@ namespace Emby.Server.Implementations public string MediaInfoImagesPath => Path.Combine(InternalMetadataPath, "mediainfo"); /// <summary> - /// Gets the path to the user configuration directory + /// Gets the path to the user configuration directory. /// </summary> /// <value>The user configuration directory path.</value> public string UserConfigurationDirectoryPath => Path.Combine(ConfigurationDirectoryPath, "users"); - private string _defaultTranscodingTempPath; public string DefaultTranscodingTempPath => _defaultTranscodingTempPath ?? (_defaultTranscodingTempPath = Path.Combine(ProgramDataPath, "transcoding-temp")); - private string _transcodingTempPath; public string TranscodingTempPath { get => _transcodingTempPath ?? (_transcodingTempPath = DefaultTranscodingTempPath); @@ -139,7 +141,6 @@ namespace Emby.Server.Implementations return path; } - private string _internalMetadataPath; public string InternalMetadataPath { get => _internalMetadataPath ?? (_internalMetadataPath = Path.Combine(DataPath, "metadata")); diff --git a/Emby.Server.Implementations/Services/HttpResult.cs b/Emby.Server.Implementations/Services/HttpResult.cs index 2b5963a77..095193828 100644 --- a/Emby.Server.Implementations/Services/HttpResult.cs +++ b/Emby.Server.Implementations/Services/HttpResult.cs @@ -10,8 +10,6 @@ namespace Emby.Server.Implementations.Services public class HttpResult : IHttpResult, IAsyncStreamWriter { - public object Response { get; set; } - public HttpResult(object response, string contentType, HttpStatusCode statusCode) { this.Headers = new Dictionary<string, string>(); @@ -21,6 +19,8 @@ namespace Emby.Server.Implementations.Services this.StatusCode = statusCode; } + public object Response { get; set; } + public string ContentType { get; set; } public IDictionary<string, string> Headers { get; private set; } @@ -37,7 +37,7 @@ namespace Emby.Server.Implementations.Services public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) { - var response = RequestContext == null ? null : RequestContext.Response; + var response = RequestContext?.Response; if (this.Response is byte[] bytesResponse) { @@ -45,13 +45,14 @@ namespace Emby.Server.Implementations.Services if (response != null) { - response.OriginalResponse.ContentLength = contentLength; + response.ContentLength = contentLength; } if (contentLength > 0) { await responseStream.WriteAsync(bytesResponse, 0, contentLength, cancellationToken).ConfigureAwait(false); } + return; } diff --git a/Emby.Server.Implementations/Services/ResponseHelper.cs b/Emby.Server.Implementations/Services/ResponseHelper.cs index 251ba3529..a566b18dd 100644 --- a/Emby.Server.Implementations/Services/ResponseHelper.cs +++ b/Emby.Server.Implementations/Services/ResponseHelper.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Globalization; using System.IO; using System.Net; @@ -8,12 +7,13 @@ using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.HttpServer; using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; namespace Emby.Server.Implementations.Services { public static class ResponseHelper { - public static Task WriteToResponse(IResponse response, IRequest request, object result, CancellationToken cancellationToken) + public static Task WriteToResponse(HttpResponse response, IRequest request, object result, CancellationToken cancellationToken) { if (result == null) { @@ -22,7 +22,7 @@ namespace Emby.Server.Implementations.Services response.StatusCode = (int)HttpStatusCode.NoContent; } - response.OriginalResponse.ContentLength = 0; + response.ContentLength = 0; return Task.CompletedTask; } @@ -41,7 +41,6 @@ namespace Emby.Server.Implementations.Services httpResult.RequestContext = request; response.StatusCode = httpResult.Status; - response.StatusDescription = httpResult.StatusCode.ToString(); } var responseOptions = result as IHasHeaders; @@ -51,11 +50,11 @@ namespace Emby.Server.Implementations.Services { if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase)) { - response.OriginalResponse.ContentLength = long.Parse(responseHeaders.Value, CultureInfo.InvariantCulture); + response.ContentLength = long.Parse(responseHeaders.Value, CultureInfo.InvariantCulture); continue; } - response.AddHeader(responseHeaders.Key, responseHeaders.Value); + response.Headers.Add(responseHeaders.Key, responseHeaders.Value); } } @@ -74,31 +73,31 @@ namespace Emby.Server.Implementations.Services switch (result) { case IAsyncStreamWriter asyncStreamWriter: - return asyncStreamWriter.WriteToAsync(response.OutputStream, cancellationToken); + return asyncStreamWriter.WriteToAsync(response.Body, cancellationToken); case IStreamWriter streamWriter: - streamWriter.WriteTo(response.OutputStream); + streamWriter.WriteTo(response.Body); return Task.CompletedTask; case FileWriter fileWriter: return fileWriter.WriteToAsync(response, cancellationToken); case Stream stream: - return CopyStream(stream, response.OutputStream); + return CopyStream(stream, response.Body); case byte[] bytes: response.ContentType = "application/octet-stream"; - response.OriginalResponse.ContentLength = bytes.Length; + response.ContentLength = bytes.Length; if (bytes.Length > 0) { - return response.OutputStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken); + return response.Body.WriteAsync(bytes, 0, bytes.Length, cancellationToken); } return Task.CompletedTask; case string responseText: var responseTextAsBytes = Encoding.UTF8.GetBytes(responseText); - response.OriginalResponse.ContentLength = responseTextAsBytes.Length; + response.ContentLength = responseTextAsBytes.Length; if (responseTextAsBytes.Length > 0) { - return response.OutputStream.WriteAsync(responseTextAsBytes, 0, responseTextAsBytes.Length, cancellationToken); + return response.Body.WriteAsync(responseTextAsBytes, 0, responseTextAsBytes.Length, cancellationToken); } return Task.CompletedTask; @@ -115,7 +114,7 @@ namespace Emby.Server.Implementations.Services } } - public static async Task WriteObject(IRequest request, object result, IResponse response) + public static async Task WriteObject(IRequest request, object result, HttpResponse response) { var contentType = request.ResponseContentType; var serializer = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType); @@ -127,11 +126,11 @@ namespace Emby.Server.Implementations.Services ms.Position = 0; var contentLength = ms.Length; - response.OriginalResponse.ContentLength = contentLength; + response.ContentLength = contentLength; if (contentLength > 0) { - await ms.CopyToAsync(response.OutputStream).ConfigureAwait(false); + await ms.CopyToAsync(response.Body).ConfigureAwait(false); } } } diff --git a/Emby.Server.Implementations/Services/ServiceController.cs b/Emby.Server.Implementations/Services/ServiceController.cs index 5e3d529c6..d963f9043 100644 --- a/Emby.Server.Implementations/Services/ServiceController.cs +++ b/Emby.Server.Implementations/Services/ServiceController.cs @@ -147,7 +147,6 @@ namespace Emby.Server.Implementations.Services public Task<object> Execute(HttpListenerHost httpHost, object requestDto, IRequest req) { - req.Dto = requestDto; var requestType = requestDto.GetType(); req.OperationName = requestType.Name; @@ -161,9 +160,6 @@ namespace Emby.Server.Implementations.Services serviceRequiresContext.Request = req; } - if (req.Dto == null) // Don't override existing batched DTO[] - req.Dto = requestDto; - //Executes the service and returns the result return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName()); } diff --git a/Emby.Server.Implementations/Services/ServiceExec.cs b/Emby.Server.Implementations/Services/ServiceExec.cs index 38952628d..9f5f97028 100644 --- a/Emby.Server.Implementations/Services/ServiceExec.cs +++ b/Emby.Server.Implementations/Services/ServiceExec.cs @@ -78,7 +78,7 @@ namespace Emby.Server.Implementations.Services foreach (var requestFilter in actionContext.RequestFilters) { requestFilter.RequestFilter(request, request.Response, requestDto); - if (request.Response.OriginalResponse.HasStarted) + if (request.Response.HasStarted) { Task.FromResult<object>(null); } @@ -87,8 +87,7 @@ namespace Emby.Server.Implementations.Services var response = actionContext.ServiceAction(instance, requestDto); - var taskResponse = response as Task; - if (taskResponse != null) + if (response is Task taskResponse) { return GetTaskResult(taskResponse); } @@ -104,8 +103,7 @@ namespace Emby.Server.Implementations.Services { try { - var taskObject = task as Task<object>; - if (taskObject != null) + if (task is Task<object> taskObject) { return await taskObject.ConfigureAwait(false); } @@ -136,7 +134,7 @@ namespace Emby.Server.Implementations.Services } catch (TypeAccessException) { - return null; //return null for void Task's + return null; // return null for void Task's } } @@ -155,29 +153,22 @@ namespace Emby.Server.Implementations.Services Id = ServiceMethod.Key(serviceType, actionName, requestType.GetMethodName()) }; - try - { - actionCtx.ServiceAction = CreateExecFn(serviceType, requestType, mi); - } - catch - { - //Potential problems with MONO, using reflection for fallback - actionCtx.ServiceAction = (service, request) => - mi.Invoke(service, new[] { request }); - } + actionCtx.ServiceAction = CreateExecFn(serviceType, requestType, mi); var reqFilters = new List<IHasRequestFilter>(); foreach (var attr in mi.GetCustomAttributes(true)) { - var hasReqFilter = attr as IHasRequestFilter; - - if (hasReqFilter != null) + if (attr is IHasRequestFilter hasReqFilter) + { reqFilters.Add(hasReqFilter); + } } if (reqFilters.Count > 0) + { actionCtx.RequestFilters = reqFilters.OrderBy(i => i.Priority).ToArray(); + } actions.Add(actionCtx); } @@ -198,15 +189,19 @@ namespace Emby.Server.Implementations.Services if (mi.ReturnType != typeof(void)) { - var executeFunc = Expression.Lambda<ActionInvokerFn> - (callExecute, serviceParam, requestDtoParam).Compile(); + var executeFunc = Expression.Lambda<ActionInvokerFn>( + callExecute, + serviceParam, + requestDtoParam).Compile(); return executeFunc; } else { - var executeFunc = Expression.Lambda<VoidActionInvokerFn> - (callExecute, serviceParam, requestDtoParam).Compile(); + var executeFunc = Expression.Lambda<VoidActionInvokerFn>( + callExecute, + serviceParam, + requestDtoParam).Compile(); return (service, request) => { diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs index d32fce1c7..934560de3 100644 --- a/Emby.Server.Implementations/Services/ServiceHandler.cs +++ b/Emby.Server.Implementations/Services/ServiceHandler.cs @@ -5,20 +5,21 @@ using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.HttpServer; using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Services { public class ServiceHandler { - public RestPath RestPath { get; } + private RestPath _restPath; - public string ResponseContentType { get; } + private string _responseContentType; internal ServiceHandler(RestPath restPath, string responseContentType) { - RestPath = restPath; - ResponseContentType = responseContentType; + _restPath = restPath; + _responseContentType = responseContentType; } protected static Task<object> CreateContentTypeRequest(HttpListenerHost host, IRequest httpReq, Type requestType, string contentType) @@ -54,7 +55,7 @@ namespace Emby.Server.Implementations.Services private static string GetFormatContentType(string format) { - //built-in formats + // built-in formats switch (format) { case "json": return "application/json"; @@ -63,16 +64,16 @@ namespace Emby.Server.Implementations.Services } } - public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, IResponse httpRes, ILogger logger, CancellationToken cancellationToken) + public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, ILogger logger, CancellationToken cancellationToken) { - httpReq.Items["__route"] = RestPath; + httpReq.Items["__route"] = _restPath; - if (ResponseContentType != null) + if (_responseContentType != null) { - httpReq.ResponseContentType = ResponseContentType; + httpReq.ResponseContentType = _responseContentType; } - var request = httpReq.Dto = await CreateRequest(httpHost, httpReq, RestPath, logger).ConfigureAwait(false); + var request = await CreateRequest(httpHost, httpReq, _restPath, logger).ConfigureAwait(false); httpHost.ApplyRequestFilters(httpReq, httpRes, request); @@ -94,7 +95,7 @@ namespace Emby.Server.Implementations.Services if (RequireqRequestStream(requestType)) { // Used by IRequiresRequestStream - var requestParams = await GetRequestParams(httpReq).ConfigureAwait(false); + var requestParams = GetRequestParams(httpReq.Response.HttpContext.Request); var request = ServiceHandler.CreateRequest(httpReq, restPath, requestParams, host.CreateInstance(requestType)); var rawReq = (IRequiresRequestStream)request; @@ -103,7 +104,7 @@ namespace Emby.Server.Implementations.Services } else { - var requestParams = await GetFlattenedRequestParams(httpReq).ConfigureAwait(false); + var requestParams = GetFlattenedRequestParams(httpReq.Response.HttpContext.Request); var requestDto = await CreateContentTypeRequest(host, httpReq, restPath.RequestType, httpReq.ContentType).ConfigureAwait(false); @@ -121,7 +122,7 @@ namespace Emby.Server.Implementations.Services public static object CreateRequest(IRequest httpReq, RestPath restPath, Dictionary<string, string> requestParams, object requestDto) { var pathInfo = !restPath.IsWildCardPath - ? GetSanitizedPathInfo(httpReq.PathInfo, out string contentType) + ? GetSanitizedPathInfo(httpReq.PathInfo, out _) : httpReq.PathInfo; return restPath.CreateRequest(pathInfo, requestParams, requestDto); @@ -130,56 +131,41 @@ namespace Emby.Server.Implementations.Services /// <summary> /// Duplicate Params are given a unique key by appending a #1 suffix /// </summary> - private static async Task<Dictionary<string, string>> GetRequestParams(IRequest request) + private static Dictionary<string, string> GetRequestParams(HttpRequest request) { var map = new Dictionary<string, string>(); - foreach (var name in request.QueryString.Keys) + foreach (var pair in request.Query) { - if (name == null) - { - // thank you ASP.NET - continue; - } - - var values = request.QueryString[name]; + var values = pair.Value; if (values.Count == 1) { - map[name] = values[0]; + map[pair.Key] = values[0]; } else { for (var i = 0; i < values.Count; i++) { - map[name + (i == 0 ? "" : "#" + i)] = values[i]; + map[pair.Key + (i == 0 ? string.Empty : "#" + i)] = values[i]; } } } - if ((IsMethod(request.Verb, "POST") || IsMethod(request.Verb, "PUT"))) + if ((IsMethod(request.Method, "POST") || IsMethod(request.Method, "PUT")) + && request.HasFormContentType) { - var formData = await request.GetFormData().ConfigureAwait(false); - if (formData != null) + foreach (var pair in request.Form) { - foreach (var name in formData.Keys) + var values = pair.Value; + if (values.Count == 1) { - if (name == null) - { - // thank you ASP.NET - continue; - } - - var values = formData.GetValues(name); - if (values.Count == 1) - { - map[name] = values[0]; - } - else + map[pair.Key] = values[0]; + } + else + { + for (var i = 0; i < values.Count; i++) { - for (var i = 0; i < values.Count; i++) - { - map[name + (i == 0 ? "" : "#" + i)] = values[i]; - } + map[pair.Key + (i == 0 ? string.Empty : "#" + i)] = values[i]; } } } @@ -189,43 +175,26 @@ namespace Emby.Server.Implementations.Services } private static bool IsMethod(string method, string expected) - { - return string.Equals(method, expected, StringComparison.OrdinalIgnoreCase); - } + => string.Equals(method, expected, StringComparison.OrdinalIgnoreCase); /// <summary> /// Duplicate params have their values joined together in a comma-delimited string /// </summary> - private static async Task<Dictionary<string, string>> GetFlattenedRequestParams(IRequest request) + private static Dictionary<string, string> GetFlattenedRequestParams(HttpRequest request) { var map = new Dictionary<string, string>(); - foreach (var name in request.QueryString.Keys) + foreach (var pair in request.Query) { - if (name == null) - { - // thank you ASP.NET - continue; - } - - map[name] = request.QueryString[name]; + map[pair.Key] = pair.Value; } - if ((IsMethod(request.Verb, "POST") || IsMethod(request.Verb, "PUT"))) + if ((IsMethod(request.Method, "POST") || IsMethod(request.Method, "PUT")) + && request.HasFormContentType) { - var formData = await request.GetFormData().ConfigureAwait(false); - if (formData != null) + foreach (var pair in request.Form) { - foreach (var name in formData.Keys) - { - if (name == null) - { - // thank you ASP.NET - continue; - } - - map[name] = formData[name]; - } + map[pair.Key] = pair.Value; } } diff --git a/Emby.Server.Implementations/Services/ServicePath.cs b/Emby.Server.Implementations/Services/ServicePath.cs index ccb28e8df..0b67669b9 100644 --- a/Emby.Server.Implementations/Services/ServicePath.cs +++ b/Emby.Server.Implementations/Services/ServicePath.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using System.Reflection; using System.Text; +using System.Text.Json.Serialization; namespace Emby.Server.Implementations.Services { @@ -28,6 +29,13 @@ namespace Emby.Server.Implementations.Services private readonly bool[] isWildcard; private readonly int wildcardCount = 0; + internal static string[] IgnoreAttributesNamed = new[] + { + nameof(JsonIgnoreAttribute) + }; + + private static Type _excludeType = typeof(Stream); + public int VariableArgsCount { get; set; } /// <summary> @@ -190,21 +198,12 @@ namespace Emby.Server.Implementations.Services StringComparer.OrdinalIgnoreCase); } - internal static string[] IgnoreAttributesNamed = new[] - { - "IgnoreDataMemberAttribute", - "JsonIgnoreAttribute" - }; - - - private static Type excludeType = typeof(Stream); - internal static IEnumerable<PropertyInfo> GetSerializableProperties(Type type) { foreach (var prop in GetPublicProperties(type)) { if (prop.GetMethod == null - || excludeType == prop.PropertyType) + || _excludeType == prop.PropertyType) { continue; } diff --git a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs index c27eb7686..23e22afd5 100644 --- a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs +++ b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs @@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Services { PropertySetFn = propertySetFn; PropertyParseStringFn = propertyParseStringFn; - PropertyType = PropertyType; + PropertyType = propertyType; } public Action<object, object> PropertySetFn { get; private set; } diff --git a/Emby.Server.Implementations/Services/UrlExtensions.cs b/Emby.Server.Implementations/Services/UrlExtensions.cs index 8899fbfa3..5d4407f3b 100644 --- a/Emby.Server.Implementations/Services/UrlExtensions.cs +++ b/Emby.Server.Implementations/Services/UrlExtensions.cs @@ -12,10 +12,10 @@ namespace Emby.Server.Implementations.Services { public static string GetMethodName(this Type type) { - var typeName = type.FullName != null //can be null, e.g. generic types - ? LeftPart(type.FullName, "[[") //Generic Fullname - .Replace(type.Namespace + ".", "") //Trim Namespaces - .Replace("+", ".") //Convert nested into normal type + var typeName = type.FullName != null // can be null, e.g. generic types + ? LeftPart(type.FullName, "[[") // Generic Fullname + .Replace(type.Namespace + ".", string.Empty) // Trim Namespaces + .Replace("+", ".") // Convert nested into normal type : type.Name; return type.IsGenericParameter ? "'" + typeName : typeName; @@ -23,7 +23,11 @@ namespace Emby.Server.Implementations.Services private static string LeftPart(string strVal, string needle) { - if (strVal == null) return null; + if (strVal == null) + { + return null; + } + var pos = strVal.IndexOf(needle, StringComparison.OrdinalIgnoreCase); return pos == -1 ? strVal diff --git a/Emby.Server.Implementations/Session/HttpSessionController.cs b/Emby.Server.Implementations/Session/HttpSessionController.cs index 9281f82b3..1104a7a85 100644 --- a/Emby.Server.Implementations/Session/HttpSessionController.cs +++ b/Emby.Server.Implementations/Session/HttpSessionController.cs @@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.Session { var dict = new Dictionary<string, string>(); - dict["ItemIds"] = string.Join(",", command.ItemIds.Select(i => i.ToString("N")).ToArray()); + dict["ItemIds"] = string.Join(",", command.ItemIds.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray()); if (command.StartPositionTicks.HasValue) { diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 53ed5fc22..61329160a 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -327,7 +327,7 @@ namespace Emby.Server.Implementations.Session { if (string.IsNullOrEmpty(info.MediaSourceId)) { - info.MediaSourceId = info.ItemId.ToString("N"); + info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture); } if (!info.ItemId.Equals(Guid.Empty) && info.Item == null && libraryItem != null) @@ -463,7 +463,7 @@ namespace Emby.Server.Implementations.Session Client = appName, DeviceId = deviceId, ApplicationVersion = appVersion, - Id = key.GetMD5().ToString("N"), + Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture), ServerId = _appHost.SystemId }; @@ -845,7 +845,7 @@ namespace Emby.Server.Implementations.Session // Normalize if (string.IsNullOrEmpty(info.MediaSourceId)) { - info.MediaSourceId = info.ItemId.ToString("N"); + info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture); } if (!info.ItemId.Equals(Guid.Empty) && info.Item == null && libraryItem != null) @@ -1029,7 +1029,7 @@ namespace Emby.Server.Implementations.Session private static async Task SendMessageToSession<T>(SessionInfo session, string name, T data, CancellationToken cancellationToken) { var controllers = session.SessionControllers.ToArray(); - var messageId = Guid.NewGuid().ToString("N"); + var messageId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); foreach (var controller in controllers) { @@ -1041,7 +1041,7 @@ namespace Emby.Server.Implementations.Session { IEnumerable<Task> GetTasks() { - var messageId = Guid.NewGuid().ToString("N"); + var messageId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); foreach (var session in sessions) { var controllers = session.SessionControllers; @@ -1234,7 +1234,7 @@ namespace Emby.Server.Implementations.Session AssertCanControl(session, controllingSession); if (!controllingSession.UserId.Equals(Guid.Empty)) { - command.ControllingUserId = controllingSession.UserId.ToString("N"); + command.ControllingUserId = controllingSession.UserId.ToString("N", CultureInfo.InvariantCulture); } } @@ -1375,16 +1375,14 @@ namespace Emby.Server.Implementations.Session CheckDisposed(); User user = null; - if (!request.UserId.Equals(Guid.Empty)) + if (request.UserId != Guid.Empty) { - user = _userManager.Users - .FirstOrDefault(i => i.Id == request.UserId); + user = _userManager.GetUserById(request.UserId); } if (user == null) { - user = _userManager.Users - .FirstOrDefault(i => string.Equals(request.Username, i.Name, StringComparison.OrdinalIgnoreCase)); + user = _userManager.GetUserByName(request.Username); } if (user != null) @@ -1484,7 +1482,7 @@ namespace Emby.Server.Implementations.Session DeviceId = deviceId, DeviceName = deviceName, UserId = user.Id, - AccessToken = Guid.NewGuid().ToString("N"), + AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), UserName = user.Name }; @@ -1822,6 +1820,7 @@ namespace Emby.Server.Implementations.Session CheckDisposed(); var sessions = Sessions.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)); + return SendMessageToSessions(sessions, name, data, cancellationToken); } @@ -1831,6 +1830,7 @@ namespace Emby.Server.Implementations.Session var sessions = Sessions .Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase) || IsAdminSession(i)); + return SendMessageToSessions(sessions, name, data, cancellationToken); } diff --git a/Emby.Server.Implementations/SocketSharp/RequestMono.cs b/Emby.Server.Implementations/SocketSharp/RequestMono.cs deleted file mode 100644 index ec637186f..000000000 --- a/Emby.Server.Implementations/SocketSharp/RequestMono.cs +++ /dev/null @@ -1,647 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Net; -using System.Text; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; - -namespace Emby.Server.Implementations.SocketSharp -{ - public partial class WebSocketSharpRequest : IHttpRequest - { - internal static string GetParameter(ReadOnlySpan<char> header, string attr) - { - int ap = header.IndexOf(attr.AsSpan(), StringComparison.Ordinal); - if (ap == -1) - { - return null; - } - - ap += attr.Length; - if (ap >= header.Length) - { - return null; - } - - char ending = header[ap]; - if (ending != '"') - { - ending = ' '; - } - - var slice = header.Slice(ap + 1); - int end = slice.IndexOf(ending); - if (end == -1) - { - return ending == '"' ? null : header.Slice(ap).ToString(); - } - - return slice.Slice(0, end - ap - 1).ToString(); - } - - private async Task LoadMultiPart(WebROCollection form) - { - string boundary = GetParameter(ContentType.AsSpan(), "; boundary="); - if (boundary == null) - { - return; - } - - using (var requestStream = InputStream) - { - // DB: 30/01/11 - Hack to get around non-seekable stream and received HTTP request - // Not ending with \r\n? - var ms = new MemoryStream(32 * 1024); - await requestStream.CopyToAsync(ms).ConfigureAwait(false); - - var input = ms; - ms.WriteByte((byte)'\r'); - ms.WriteByte((byte)'\n'); - - input.Position = 0; - - // Uncomment to debug - // var content = new StreamReader(ms).ReadToEnd(); - // Console.WriteLine(boundary + "::" + content); - // input.Position = 0; - - var multi_part = new HttpMultipart(input, boundary, ContentEncoding); - - HttpMultipart.Element e; - while ((e = multi_part.ReadNextElement()) != null) - { - if (e.Filename == null) - { - byte[] copy = new byte[e.Length]; - - input.Position = e.Start; - await input.ReadAsync(copy, 0, (int)e.Length).ConfigureAwait(false); - - form.Add(e.Name, (e.Encoding ?? ContentEncoding).GetString(copy, 0, copy.Length)); - } - else - { - // We use a substream, as in 2.x we will support large uploads streamed to disk, - files[e.Name] = new HttpPostedFile(e.Filename, e.ContentType, input, e.Start, e.Length); - } - } - } - } - - public async Task<QueryParamCollection> GetFormData() - { - var form = new WebROCollection(); - files = new Dictionary<string, HttpPostedFile>(); - - if (IsContentType("multipart/form-data")) - { - await LoadMultiPart(form).ConfigureAwait(false); - } - else if (IsContentType("application/x-www-form-urlencoded")) - { - await LoadWwwForm(form).ConfigureAwait(false); - } - - if (validate_form && !checked_form) - { - checked_form = true; - ValidateNameValueCollection("Form", form); - } - - return form; - } - - public string Accept => StringValues.IsNullOrEmpty(request.Headers[HeaderNames.Accept]) ? null : request.Headers[HeaderNames.Accept].ToString(); - - public string Authorization => StringValues.IsNullOrEmpty(request.Headers[HeaderNames.Authorization]) ? null : request.Headers[HeaderNames.Authorization].ToString(); - - protected bool validate_form { get; set; } - protected bool checked_form { get; set; } - - private static void ThrowValidationException(string name, string key, string value) - { - string v = "\"" + value + "\""; - if (v.Length > 20) - { - v = v.Substring(0, 16) + "...\""; - } - - string msg = string.Format( - CultureInfo.InvariantCulture, - "A potentially dangerous Request.{0} value was detected from the client ({1}={2}).", - name, - key, - v); - - throw new Exception(msg); - } - - private static void ValidateNameValueCollection(string name, QueryParamCollection coll) - { - if (coll == null) - { - return; - } - - foreach (var pair in coll) - { - var key = pair.Name; - var val = pair.Value; - if (val != null && val.Length > 0 && IsInvalidString(val)) - { - ThrowValidationException(name, key, val); - } - } - } - - internal static bool IsInvalidString(string val) - => IsInvalidString(val, out var validationFailureIndex); - - internal static bool IsInvalidString(string val, out int validationFailureIndex) - { - validationFailureIndex = 0; - - int len = val.Length; - if (len < 2) - { - return false; - } - - char current = val[0]; - for (int idx = 1; idx < len; idx++) - { - char next = val[idx]; - - // See http://secunia.com/advisories/14325 - if (current == '<' || current == '\xff1c') - { - if (next == '!' || next < ' ' - || (next >= 'a' && next <= 'z') - || (next >= 'A' && next <= 'Z')) - { - validationFailureIndex = idx - 1; - return true; - } - } - else if (current == '&' && next == '#') - { - validationFailureIndex = idx - 1; - return true; - } - - current = next; - } - - return false; - } - - private bool IsContentType(string ct) - { - if (ContentType == null) - { - return false; - } - - return ContentType.StartsWith(ct, StringComparison.OrdinalIgnoreCase); - } - - private async Task LoadWwwForm(WebROCollection form) - { - using (var input = InputStream) - { - using (var ms = new MemoryStream()) - { - await input.CopyToAsync(ms).ConfigureAwait(false); - ms.Position = 0; - - using (var s = new StreamReader(ms, ContentEncoding)) - { - var key = new StringBuilder(); - var value = new StringBuilder(); - int c; - - while ((c = s.Read()) != -1) - { - if (c == '=') - { - value.Length = 0; - while ((c = s.Read()) != -1) - { - if (c == '&') - { - AddRawKeyValue(form, key, value); - break; - } - else - { - value.Append((char)c); - } - } - - if (c == -1) - { - AddRawKeyValue(form, key, value); - return; - } - } - else if (c == '&') - { - AddRawKeyValue(form, key, value); - } - else - { - key.Append((char)c); - } - } - - if (c == -1) - { - AddRawKeyValue(form, key, value); - } - } - } - } - } - - private static void AddRawKeyValue(WebROCollection form, StringBuilder key, StringBuilder value) - { - form.Add(WebUtility.UrlDecode(key.ToString()), WebUtility.UrlDecode(value.ToString())); - - key.Length = 0; - value.Length = 0; - } - - private Dictionary<string, HttpPostedFile> files; - - private class WebROCollection : QueryParamCollection - { - public override string ToString() - { - var result = new StringBuilder(); - foreach (var pair in this) - { - if (result.Length > 0) - { - result.Append('&'); - } - - var key = pair.Name; - if (key != null && key.Length > 0) - { - result.Append(key); - result.Append('='); - } - - result.Append(pair.Value); - } - - return result.ToString(); - } - } - private class HttpMultipart - { - - public class Element - { - public string ContentType { get; set; } - - public string Name { get; set; } - - public string Filename { get; set; } - - public Encoding Encoding { get; set; } - - public long Start { get; set; } - - public long Length { get; set; } - - public override string ToString() - { - return "ContentType " + ContentType + ", Name " + Name + ", Filename " + Filename + ", Start " + - Start.ToString(CultureInfo.CurrentCulture) + ", Length " + Length.ToString(CultureInfo.CurrentCulture); - } - } - - private const byte LF = (byte)'\n'; - - private const byte CR = (byte)'\r'; - - private Stream data; - - private string boundary; - - private byte[] boundaryBytes; - - private byte[] buffer; - - private bool atEof; - - private Encoding encoding; - - private StringBuilder sb; - - // See RFC 2046 - // In the case of multipart entities, in which one or more different - // sets of data are combined in a single body, a "multipart" media type - // field must appear in the entity's header. The body must then contain - // one or more body parts, each preceded by a boundary delimiter line, - // and the last one followed by a closing boundary delimiter line. - // After its boundary delimiter line, each body part then consists of a - // header area, a blank line, and a body area. Thus a body part is - // similar to an RFC 822 message in syntax, but different in meaning. - - public HttpMultipart(Stream data, string b, Encoding encoding) - { - this.data = data; - boundary = b; - boundaryBytes = encoding.GetBytes(b); - buffer = new byte[boundaryBytes.Length + 2]; // CRLF or '--' - this.encoding = encoding; - sb = new StringBuilder(); - } - - public Element ReadNextElement() - { - if (atEof || ReadBoundary()) - { - return null; - } - - var elem = new Element(); - ReadOnlySpan<char> header; - while ((header = ReadLine().AsSpan()).Length != 0) - { - if (header.StartsWith("Content-Disposition:".AsSpan(), StringComparison.OrdinalIgnoreCase)) - { - elem.Name = GetContentDispositionAttribute(header, "name"); - elem.Filename = StripPath(GetContentDispositionAttributeWithEncoding(header, "filename")); - } - else if (header.StartsWith("Content-Type:".AsSpan(), StringComparison.OrdinalIgnoreCase)) - { - elem.ContentType = header.Slice("Content-Type:".Length).Trim().ToString(); - elem.Encoding = GetEncoding(elem.ContentType); - } - } - - long start = data.Position; - elem.Start = start; - long pos = MoveToNextBoundary(); - if (pos == -1) - { - return null; - } - - elem.Length = pos - start; - return elem; - } - - private string ReadLine() - { - // CRLF or LF are ok as line endings. - bool got_cr = false; - int b = 0; - sb.Length = 0; - while (true) - { - b = data.ReadByte(); - if (b == -1) - { - return null; - } - - if (b == LF) - { - break; - } - - got_cr = b == CR; - sb.Append((char)b); - } - - if (got_cr) - { - sb.Length--; - } - - return sb.ToString(); - } - - private static string GetContentDispositionAttribute(ReadOnlySpan<char> l, string name) - { - int idx = l.IndexOf((name + "=\"").AsSpan(), StringComparison.Ordinal); - if (idx < 0) - { - return null; - } - - int begin = idx + name.Length + "=\"".Length; - int end = l.Slice(begin).IndexOf('"'); - if (end < 0) - { - return null; - } - - if (begin == end) - { - return string.Empty; - } - - return l.Slice(begin, end - begin).ToString(); - } - - private string GetContentDispositionAttributeWithEncoding(ReadOnlySpan<char> l, string name) - { - int idx = l.IndexOf((name + "=\"").AsSpan(), StringComparison.Ordinal); - if (idx < 0) - { - return null; - } - - int begin = idx + name.Length + "=\"".Length; - int end = l.Slice(begin).IndexOf('"'); - if (end < 0) - { - return null; - } - - if (begin == end) - { - return string.Empty; - } - - ReadOnlySpan<char> temp = l.Slice(begin, end - begin); - byte[] source = new byte[temp.Length]; - for (int i = temp.Length - 1; i >= 0; i--) - { - source[i] = (byte)temp[i]; - } - - return encoding.GetString(source, 0, source.Length); - } - - private bool ReadBoundary() - { - try - { - string line; - do - { - line = ReadLine(); - } - while (line.Length == 0); - - if (line[0] != '-' || line[1] != '-') - { - return false; - } - - if (!line.EndsWith(boundary, StringComparison.Ordinal)) - { - return true; - } - } - catch - { - - } - - return false; - } - - private static bool CompareBytes(byte[] orig, byte[] other) - { - for (int i = orig.Length - 1; i >= 0; i--) - { - if (orig[i] != other[i]) - { - return false; - } - } - - return true; - } - - private long MoveToNextBoundary() - { - long retval = 0; - bool got_cr = false; - - int state = 0; - int c = data.ReadByte(); - while (true) - { - if (c == -1) - { - return -1; - } - - if (state == 0 && c == LF) - { - retval = data.Position - 1; - if (got_cr) - { - retval--; - } - - state = 1; - c = data.ReadByte(); - } - else if (state == 0) - { - got_cr = c == CR; - c = data.ReadByte(); - } - else if (state == 1 && c == '-') - { - c = data.ReadByte(); - if (c == -1) - { - return -1; - } - - if (c != '-') - { - state = 0; - got_cr = false; - continue; // no ReadByte() here - } - - int nread = data.Read(buffer, 0, buffer.Length); - int bl = buffer.Length; - if (nread != bl) - { - return -1; - } - - if (!CompareBytes(boundaryBytes, buffer)) - { - state = 0; - data.Position = retval + 2; - if (got_cr) - { - data.Position++; - got_cr = false; - } - - c = data.ReadByte(); - continue; - } - - if (buffer[bl - 2] == '-' && buffer[bl - 1] == '-') - { - atEof = true; - } - else if (buffer[bl - 2] != CR || buffer[bl - 1] != LF) - { - state = 0; - data.Position = retval + 2; - if (got_cr) - { - data.Position++; - got_cr = false; - } - - c = data.ReadByte(); - continue; - } - - data.Position = retval + 2; - if (got_cr) - { - data.Position++; - } - - break; - } - else - { - // state == 1 - state = 0; // no ReadByte() here - } - } - - return retval; - } - - private static string StripPath(string path) - { - if (path == null || path.Length == 0) - { - return path; - } - - if (path.IndexOf(":\\", StringComparison.Ordinal) != 1 - && !path.StartsWith("\\\\", StringComparison.Ordinal)) - { - return path; - } - - return path.Substring(path.LastIndexOf('\\') + 1); - } - } - } -} diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs index dd313b336..e93bff124 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs @@ -117,7 +117,7 @@ namespace Emby.Server.Implementations.SocketSharp /// <summary> /// Releases the unmanaged resources and disposes of the managed resources used. /// </summary> - /// <param name="disposing">Whether or not the managed resources should be disposed</param> + /// <param name="disposing">Whether or not the managed resources should be disposed.</param> protected virtual void Dispose(bool disposing) { if (_disposed) diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index 7a630bf10..332ce3903 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -1,57 +1,56 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Net; using System.Linq; -using System.Text; using MediaBrowser.Common.Net; -using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; -using IHttpFile = MediaBrowser.Model.Services.IHttpFile; using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest; -using IResponse = MediaBrowser.Model.Services.IResponse; namespace Emby.Server.Implementations.SocketSharp { public partial class WebSocketSharpRequest : IHttpRequest { - private readonly HttpRequest request; + public const string FormUrlEncoded = "application/x-www-form-urlencoded"; + public const string MultiPartFormData = "multipart/form-data"; + public const string Soap11 = "text/xml; charset=utf-8"; - public WebSocketSharpRequest(HttpRequest httpContext, HttpResponse response, string operationName, ILogger logger) + private string _remoteIp; + private Dictionary<string, object> _items; + private string _responseContentType; + + public WebSocketSharpRequest(HttpRequest httpRequest, HttpResponse httpResponse, string operationName, ILogger logger) { this.OperationName = operationName; - this.request = httpContext; - this.Response = new WebSocketSharpResponse(logger, response); + this.Request = httpRequest; + this.Response = httpResponse; } - public HttpRequest HttpRequest => request; + public string Accept => StringValues.IsNullOrEmpty(Request.Headers[HeaderNames.Accept]) ? null : Request.Headers[HeaderNames.Accept].ToString(); - public IResponse Response { get; } + public string Authorization => StringValues.IsNullOrEmpty(Request.Headers[HeaderNames.Authorization]) ? null : Request.Headers[HeaderNames.Authorization].ToString(); - public string OperationName { get; set; } + public HttpRequest Request { get; } - public object Dto { get; set; } + public HttpResponse Response { get; } - public string RawUrl => request.GetEncodedPathAndQuery(); + public string OperationName { get; set; } - public string AbsoluteUri => request.GetDisplayUrl().TrimEnd('/'); - // Header[name] returns "" when undefined + public string RawUrl => Request.GetEncodedPathAndQuery(); - private string GetHeader(string name) => request.Headers[name].ToString(); + public string AbsoluteUri => Request.GetDisplayUrl().TrimEnd('/'); - private string remoteIp; public string RemoteIp { get { - if (remoteIp != null) + if (_remoteIp != null) { - return remoteIp; + return _remoteIp; } IPAddress ip; @@ -62,14 +61,51 @@ namespace Emby.Server.Implementations.SocketSharp { if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XRealIP), out ip)) { - ip = request.HttpContext.Connection.RemoteIpAddress; + ip = Request.HttpContext.Connection.RemoteIpAddress; } } - return remoteIp = NormalizeIp(ip).ToString(); + return _remoteIp = NormalizeIp(ip).ToString(); } } + public string[] AcceptTypes => Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept); + + public Dictionary<string, object> Items => _items ?? (_items = new Dictionary<string, object>()); + + public string ResponseContentType + { + get => + _responseContentType + ?? (_responseContentType = GetResponseContentType(Request)); + set => this._responseContentType = value; + } + + public string PathInfo => Request.Path.Value; + + public string UserAgent => Request.Headers[HeaderNames.UserAgent]; + + public IHeaderDictionary Headers => Request.Headers; + + public IQueryCollection QueryString => Request.Query; + + public bool IsLocal => Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress); + + + public string HttpMethod => Request.Method; + + public string Verb => HttpMethod; + + public string ContentType => Request.ContentType; + + public Uri UrlReferrer => Request.GetTypedHeaders().Referer; + + public Stream InputStream => Request.Body; + + public long ContentLength => Request.ContentLength ?? 0; + + private string GetHeader(string name) => Request.Headers[name].ToString(); + private static IPAddress NormalizeIp(IPAddress ip) { if (ip.IsIPv4MappedToIPv6) @@ -80,22 +116,6 @@ namespace Emby.Server.Implementations.SocketSharp return ip; } - public string[] AcceptTypes => request.Headers.GetCommaSeparatedValues(HeaderNames.Accept); - - private Dictionary<string, object> items; - public Dictionary<string, object> Items => items ?? (items = new Dictionary<string, object>()); - - private string responseContentType; - public string ResponseContentType - { - get => - responseContentType - ?? (responseContentType = GetResponseContentType(HttpRequest)); - set => this.responseContentType = value; - } - - public const string FormUrlEncoded = "application/x-www-form-urlencoded"; - public const string MultiPartFormData = "multipart/form-data"; public static string GetResponseContentType(HttpRequest httpReq) { var specifiedContentType = GetQueryStringContentType(httpReq); @@ -152,8 +172,6 @@ namespace Emby.Server.Implementations.SocketSharp return serverDefaultContentType; } - public const string Soap11 = "text/xml; charset=utf-8"; - public static bool HasAnyOfContentTypes(HttpRequest request, params string[] contentTypes) { if (contentTypes == null || request.ContentType == null) @@ -224,105 +242,5 @@ namespace Emby.Server.Implementations.SocketSharp var pos = strVal.IndexOf(needle); return pos == -1 ? strVal : strVal.Slice(0, pos); } - - public string PathInfo => this.request.Path.Value; - - public string UserAgent => request.Headers[HeaderNames.UserAgent]; - - public IHeaderDictionary Headers => request.Headers; - - public IQueryCollection QueryString => request.Query; - - public bool IsLocal => string.Equals(request.HttpContext.Connection.LocalIpAddress.ToString(), request.HttpContext.Connection.RemoteIpAddress.ToString()); - - private string httpMethod; - public string HttpMethod => - httpMethod - ?? (httpMethod = request.Method); - - public string Verb => HttpMethod; - - public string ContentType => request.ContentType; - - private Encoding ContentEncoding - { - get - { - // TODO is this necessary? - if (UserAgent != null && CultureInfo.InvariantCulture.CompareInfo.IsPrefix(UserAgent, "UP")) - { - string postDataCharset = Headers["x-up-devcap-post-charset"]; - if (!string.IsNullOrEmpty(postDataCharset)) - { - try - { - return Encoding.GetEncoding(postDataCharset); - } - catch (ArgumentException) - { - } - } - } - - return request.GetTypedHeaders().ContentType.Encoding ?? Encoding.UTF8; - } - } - - public Uri UrlReferrer => request.GetTypedHeaders().Referer; - - public static Encoding GetEncoding(string contentTypeHeader) - { - var param = GetParameter(contentTypeHeader.AsSpan(), "charset="); - if (param == null) - { - return null; - } - - try - { - return Encoding.GetEncoding(param); - } - catch (ArgumentException) - { - return null; - } - } - - public Stream InputStream => request.Body; - - public long ContentLength => request.ContentLength ?? 0; - - private IHttpFile[] httpFiles; - public IHttpFile[] Files - { - get - { - if (httpFiles != null) - { - return httpFiles; - } - - if (files == null) - { - return httpFiles = Array.Empty<IHttpFile>(); - } - - var values = files.Values; - httpFiles = new IHttpFile[values.Count]; - for (int i = 0; i < values.Count; i++) - { - var reqFile = values.ElementAt(i); - httpFiles[i] = new HttpFile - { - ContentType = reqFile.ContentType, - ContentLength = reqFile.ContentLength, - FileName = reqFile.FileName, - InputStream = reqFile.InputStream, - }; - } - - return httpFiles; - } - } } } diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs deleted file mode 100644 index 0f67eaa62..000000000 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpResponse.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using IRequest = MediaBrowser.Model.Services.IRequest; - -namespace Emby.Server.Implementations.SocketSharp -{ - public class WebSocketSharpResponse : IResponse - { - private readonly ILogger _logger; - - public WebSocketSharpResponse(ILogger logger, HttpResponse response) - { - _logger = logger; - OriginalResponse = response; - } - - public HttpResponse OriginalResponse { get; } - - public int StatusCode - { - get => OriginalResponse.StatusCode; - set => OriginalResponse.StatusCode = value; - } - - public string StatusDescription { get; set; } - - public string ContentType - { - get => OriginalResponse.ContentType; - set => OriginalResponse.ContentType = value; - } - - public void AddHeader(string name, string value) - { - if (string.Equals(name, "Content-Type", StringComparison.OrdinalIgnoreCase)) - { - ContentType = value; - return; - } - - OriginalResponse.Headers.Add(name, value); - } - - public void Redirect(string url) - { - OriginalResponse.Redirect(url); - } - - public Stream OutputStream => OriginalResponse.Body; - - public bool SendChunked { get; set; } - - const int StreamCopyToBufferSize = 81920; - public async Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, IFileSystem fileSystem, IStreamHelper streamHelper, CancellationToken cancellationToken) - { - var allowAsync = !RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - - //if (count <= 0) - //{ - // allowAsync = true; - //} - - var fileOpenOptions = FileOpenOptions.SequentialScan; - - if (allowAsync) - { - fileOpenOptions |= FileOpenOptions.Asynchronous; - } - - // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 - - using (var fs = fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShareMode, fileOpenOptions)) - { - if (offset > 0) - { - fs.Position = offset; - } - - if (count > 0) - { - await streamHelper.CopyToAsync(fs, OutputStream, count, cancellationToken).ConfigureAwait(false); - } - else - { - await fs.CopyToAsync(OutputStream, StreamCopyToBufferSize, cancellationToken).ConfigureAwait(false); - } - } - } - } -} diff --git a/Emby.Server.Implementations/Sorting/ArtistComparer.cs b/Emby.Server.Implementations/Sorting/ArtistComparer.cs index 9d5befc9a..756d3c5b6 100644 --- a/Emby.Server.Implementations/Sorting/ArtistComparer.cs +++ b/Emby.Server.Implementations/Sorting/ArtistComparer.cs @@ -7,16 +7,14 @@ using MediaBrowser.Model.Querying; namespace Emby.Server.Implementations.Sorting { /// <summary> - /// Class ArtistComparer + /// Class ArtistComparer. /// </summary> public class ArtistComparer : IBaseItemComparer { - /// <summary> - /// Compares the specified x. - /// </summary> - /// <param name="x">The x.</param> - /// <param name="y">The y.</param> - /// <returns>System.Int32.</returns> + /// <inheritdoc /> + public string Name => ItemSortBy.Artist; + + /// <inheritdoc /> public int Compare(BaseItem x, BaseItem y) { return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase); @@ -29,20 +27,12 @@ namespace Emby.Server.Implementations.Sorting /// <returns>System.String.</returns> private static string GetValue(BaseItem x) { - var audio = x as Audio; - - if (audio == null) + if (!(x is Audio audio)) { return string.Empty; } - return audio.Artists.Length == 0 ? null : audio.Artists[0]; + return audio.Artists.Count == 0 ? null : audio.Artists[0]; } - - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> - public string Name => ItemSortBy.Artist; } } diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index 630ef4893..4c2f24e6f 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; @@ -73,7 +74,7 @@ namespace Emby.Server.Implementations.TV { parents = _libraryManager.GetUserRootFolder().GetChildren(user, true) .Where(i => i is Folder) - .Where(i => !user.Configuration.LatestItemsExcludes.Contains(i.Id.ToString("N"))) + .Where(i => !user.Configuration.LatestItemsExcludes.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))) .ToArray(); } diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 6833c20c3..0c0c77cda 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -1,15 +1,17 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; +using System.Net.Http; +using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; -using MediaBrowser.Common.Progress; using MediaBrowser.Common.Updates; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Events; @@ -17,6 +19,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Updates; using Microsoft.Extensions.Logging; +using static MediaBrowser.Common.HexHelper; namespace Emby.Server.Implementations.Updates { @@ -33,7 +36,7 @@ namespace Emby.Server.Implementations.Updates /// <summary> /// The current installations /// </summary> - public List<Tuple<InstallationInfo, CancellationTokenSource>> CurrentInstallations { get; set; } + private List<(InstallationInfo info, CancellationTokenSource token)> _currentInstallations { get; set; } /// <summary> /// The completed installations @@ -48,48 +51,14 @@ namespace Emby.Server.Implementations.Updates public event EventHandler<GenericEventArgs<IPlugin>> PluginUninstalled; /// <summary> - /// Called when [plugin uninstalled]. - /// </summary> - /// <param name="plugin">The plugin.</param> - private void OnPluginUninstalled(IPlugin plugin) - { - PluginUninstalled?.Invoke(this, new GenericEventArgs<IPlugin> { Argument = plugin }); - } - - /// <summary> /// Occurs when [plugin updated]. /// </summary> - public event EventHandler<GenericEventArgs<Tuple<IPlugin, PackageVersionInfo>>> PluginUpdated; - /// <summary> - /// Called when [plugin updated]. - /// </summary> - /// <param name="plugin">The plugin.</param> - /// <param name="newVersion">The new version.</param> - private void OnPluginUpdated(IPlugin plugin, PackageVersionInfo newVersion) - { - _logger.LogInformation("Plugin updated: {0} {1} {2}", newVersion.name, newVersion.versionStr ?? string.Empty, newVersion.classification); - - PluginUpdated?.Invoke(this, new GenericEventArgs<Tuple<IPlugin, PackageVersionInfo>> { Argument = new Tuple<IPlugin, PackageVersionInfo>(plugin, newVersion) }); - - _applicationHost.NotifyPendingRestart(); - } + public event EventHandler<GenericEventArgs<(IPlugin, PackageVersionInfo)>> PluginUpdated; /// <summary> /// Occurs when [plugin updated]. /// </summary> public event EventHandler<GenericEventArgs<PackageVersionInfo>> PluginInstalled; - /// <summary> - /// Called when [plugin installed]. - /// </summary> - /// <param name="package">The package.</param> - private void OnPluginInstalled(PackageVersionInfo package) - { - _logger.LogInformation("New plugin installed: {0} {1} {2}", package.name, package.versionStr ?? string.Empty, package.classification); - - PluginInstalled?.Invoke(this, new GenericEventArgs<PackageVersionInfo> { Argument = package }); - - _applicationHost.NotifyPendingRestart(); - } /// <summary> /// The _logger @@ -111,7 +80,7 @@ namespace Emby.Server.Implementations.Updates private readonly IZipClient _zipClient; public InstallationManager( - ILoggerFactory loggerFactory, + ILogger<InstallationManager> logger, IApplicationHost appHost, IApplicationPaths appPaths, IHttpClient httpClient, @@ -120,15 +89,15 @@ namespace Emby.Server.Implementations.Updates IFileSystem fileSystem, IZipClient zipClient) { - if (loggerFactory == null) + if (logger == null) { - throw new ArgumentNullException(nameof(loggerFactory)); + throw new ArgumentNullException(nameof(logger)); } - CurrentInstallations = new List<Tuple<InstallationInfo, CancellationTokenSource>>(); + _currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>(); _completedInstallationsInternal = new ConcurrentBag<InstallationInfo>(); - _logger = loggerFactory.CreateLogger(nameof(InstallationManager)); + _logger = logger; _applicationHost = appHost; _appPaths = appPaths; _httpClient = httpClient; @@ -138,21 +107,12 @@ namespace Emby.Server.Implementations.Updates _zipClient = zipClient; } - private static Version GetPackageVersion(PackageVersionInfo version) - { - return new Version(ValueOrDefault(version.versionStr, "0.0.0.1")); - } - - private static string ValueOrDefault(string str, string def) - { - return string.IsNullOrEmpty(str) ? def : str; - } - /// <summary> /// Gets all available packages. /// </summary> /// <returns>Task{List{PackageInfo}}.</returns> - public async Task<List<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken, + public async Task<List<PackageInfo>> GetAvailablePackages( + CancellationToken cancellationToken, bool withRegistration = true, string packageType = null, Version applicationVersion = null) @@ -168,26 +128,21 @@ namespace Emby.Server.Implementations.Updates /// <returns>Task{List{PackageInfo}}.</returns> public async Task<List<PackageInfo>> GetAvailablePackagesWithoutRegistrationInfo(CancellationToken cancellationToken) { - using (var response = await _httpClient.SendAsync(new HttpRequestOptions - { - Url = "https://repo.jellyfin.org/releases/plugin/manifest.json", - CancellationToken = cancellationToken, - Progress = new SimpleProgress<double>(), - CacheLength = GetCacheLength() - }, "GET").ConfigureAwait(false)) - { - using (var stream = response.Content) + using (var response = await _httpClient.SendAsync( + new HttpRequestOptions { - return FilterPackages(await _jsonSerializer.DeserializeFromStreamAsync<PackageInfo[]>(stream).ConfigureAwait(false)); - } + Url = "https://repo.jellyfin.org/releases/plugin/manifest.json", + CancellationToken = cancellationToken, + CacheMode = CacheMode.Unconditional, + CacheLength = GetCacheLength() + }, + HttpMethod.Get).ConfigureAwait(false)) + using (Stream stream = response.Content) + { + return FilterPackages(await _jsonSerializer.DeserializeFromStreamAsync<PackageInfo[]>(stream).ConfigureAwait(false)); } } - private PackageVersionClass GetSystemUpdateLevel() - { - return _applicationHost.SystemUpdateLevel; - } - private static TimeSpan GetCacheLength() { return TimeSpan.FromMinutes(3); @@ -211,7 +166,7 @@ namespace Emby.Server.Implementations.Updates } package.versions = versions - .OrderByDescending(GetPackageVersion) + .OrderByDescending(x => x.Version) .ToArray(); if (package.versions.Length == 0) @@ -294,7 +249,7 @@ namespace Emby.Server.Implementations.Updates return null; } - return package.versions.FirstOrDefault(v => GetPackageVersion(v).Equals(version) && v.classification == classification); + return package.versions.FirstOrDefault(v => v.Version == version && v.classification == classification); } /// <summary> @@ -325,13 +280,8 @@ namespace Emby.Server.Implementations.Updates var package = availablePackages.FirstOrDefault(p => string.Equals(p.guid, guid ?? "none", StringComparison.OrdinalIgnoreCase)) ?? availablePackages.FirstOrDefault(p => p.name.Equals(name, StringComparison.OrdinalIgnoreCase)); - if (package == null) - { - return null; - } - - return package.versions - .OrderByDescending(GetPackageVersion) + return package?.versions + .OrderByDescending(x => x.Version) .FirstOrDefault(v => v.classification <= classification && IsPackageVersionUpToDate(v, currentServerVersion)); } @@ -346,40 +296,26 @@ namespace Emby.Server.Implementations.Updates { var catalog = await GetAvailablePackagesWithoutRegistrationInfo(cancellationToken).ConfigureAwait(false); - var systemUpdateLevel = GetSystemUpdateLevel(); + var systemUpdateLevel = _applicationHost.SystemUpdateLevel; // Figure out what needs to be installed return _applicationHost.Plugins.Select(p => { var latestPluginInfo = GetLatestCompatibleVersion(catalog, p.Name, p.Id.ToString(), applicationVersion, systemUpdateLevel); - return latestPluginInfo != null && GetPackageVersion(latestPluginInfo) > p.Version ? latestPluginInfo : null; - + return latestPluginInfo != null && latestPluginInfo.Version > p.Version ? latestPluginInfo : null; }).Where(i => i != null) .Where(p => !string.IsNullOrEmpty(p.sourceUrl) && !CompletedInstallations.Any(i => string.Equals(i.AssemblyGuid, p.guid, StringComparison.OrdinalIgnoreCase))); } - /// <summary> - /// Installs the package. - /// </summary> - /// <param name="package">The package.</param> - /// <param name="isPlugin">if set to <c>true</c> [is plugin].</param> - /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException">package</exception> - public async Task InstallPackage(PackageVersionInfo package, bool isPlugin, IProgress<double> progress, CancellationToken cancellationToken) + /// <inheritdoc /> + public async Task InstallPackage(PackageVersionInfo package, CancellationToken cancellationToken) { if (package == null) { throw new ArgumentNullException(nameof(package)); } - if (progress == null) - { - throw new ArgumentNullException(nameof(progress)); - } - var installationInfo = new InstallationInfo { Id = Guid.NewGuid(), @@ -391,24 +327,14 @@ namespace Emby.Server.Implementations.Updates var innerCancellationTokenSource = new CancellationTokenSource(); - var tuple = new Tuple<InstallationInfo, CancellationTokenSource>(installationInfo, innerCancellationTokenSource); + var tuple = (installationInfo, innerCancellationTokenSource); // Add it to the in-progress list - lock (CurrentInstallations) + lock (_currentInstallations) { - CurrentInstallations.Add(tuple); + _currentInstallations.Add(tuple); } - var innerProgress = new ActionableProgress<double>(); - - // Whenever the progress updates, update the outer progress object and InstallationInfo - innerProgress.RegisterAction(percent => - { - progress.Report(percent); - - installationInfo.PercentComplete = percent; - }); - var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, innerCancellationTokenSource.Token).Token; var installationEventArgs = new InstallationEventArgs @@ -421,11 +347,11 @@ namespace Emby.Server.Implementations.Updates try { - await InstallPackageInternal(package, isPlugin, innerProgress, linkedToken).ConfigureAwait(false); + await InstallPackageInternal(package, linkedToken).ConfigureAwait(false); - lock (CurrentInstallations) + lock (_currentInstallations) { - CurrentInstallations.Remove(tuple); + _currentInstallations.Remove(tuple); } _completedInstallationsInternal.Add(installationInfo); @@ -434,9 +360,9 @@ namespace Emby.Server.Implementations.Updates } catch (OperationCanceledException) { - lock (CurrentInstallations) + lock (_currentInstallations) { - CurrentInstallations.Remove(tuple); + _currentInstallations.Remove(tuple); } _logger.LogInformation("Package installation cancelled: {0} {1}", package.name, package.versionStr); @@ -449,9 +375,9 @@ namespace Emby.Server.Implementations.Updates { _logger.LogError(ex, "Package installation failed"); - lock (CurrentInstallations) + lock (_currentInstallations) { - CurrentInstallations.Remove(tuple); + _currentInstallations.Remove(tuple); } PackageInstallationFailed?.Invoke(this, new InstallationFailedEventArgs @@ -473,110 +399,89 @@ namespace Emby.Server.Implementations.Updates /// Installs the package internal. /// </summary> /// <param name="package">The package.</param> - /// <param name="isPlugin">if set to <c>true</c> [is plugin].</param> - /// <param name="progress">The progress.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - private async Task InstallPackageInternal(PackageVersionInfo package, bool isPlugin, IProgress<double> progress, CancellationToken cancellationToken) + /// <returns><see cref="Task" />.</returns> + private async Task InstallPackageInternal(PackageVersionInfo package, CancellationToken cancellationToken) { - IPlugin plugin = null; - - if (isPlugin) - { - // Set last update time if we were installed before - plugin = _applicationHost.Plugins.FirstOrDefault(p => string.Equals(p.Id.ToString(), package.guid, StringComparison.OrdinalIgnoreCase)) + // Set last update time if we were installed before + IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => string.Equals(p.Id.ToString(), package.guid, StringComparison.OrdinalIgnoreCase)) ?? _applicationHost.Plugins.FirstOrDefault(p => p.Name.Equals(package.name, StringComparison.OrdinalIgnoreCase)); - } - - string targetPath = plugin == null ? null : plugin.AssemblyFilePath; // Do the install - await PerformPackageInstallation(progress, targetPath, package, cancellationToken).ConfigureAwait(false); + await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false); // Do plugin-specific processing - if (isPlugin) + if (plugin == null) { - if (plugin == null) - { - OnPluginInstalled(package); - } - else - { - OnPluginUpdated(plugin, package); - } + _logger.LogInformation("New plugin installed: {0} {1} {2}", package.name, package.versionStr ?? string.Empty, package.classification); + + PluginInstalled?.Invoke(this, new GenericEventArgs<PackageVersionInfo>(package)); + } + else + { + _logger.LogInformation("Plugin updated: {0} {1} {2}", package.name, package.versionStr ?? string.Empty, package.classification); + + PluginUpdated?.Invoke(this, new GenericEventArgs<(IPlugin, PackageVersionInfo)>((plugin, package))); } + + _applicationHost.NotifyPendingRestart(); } - private async Task PerformPackageInstallation(IProgress<double> progress, string target, PackageVersionInfo package, CancellationToken cancellationToken) + private async Task PerformPackageInstallation(PackageVersionInfo package, CancellationToken cancellationToken) { - // TODO: Remove the `string target` argument as it is not used any longer - var extension = Path.GetExtension(package.targetFilename); - var isArchive = string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase); - - if (!isArchive) + if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase)) { _logger.LogError("Only zip packages are supported. {Filename} is not a zip archive.", package.targetFilename); return; } // Always override the passed-in target (which is a file) and figure it out again - target = Path.Combine(_appPaths.PluginsPath, package.name); - _logger.LogDebug("Installing plugin to {Filename}.", target); + string targetDir = Path.Combine(_appPaths.PluginsPath, package.name); - // Download to temporary file so that, if interrupted, it won't destroy the existing installation - _logger.LogDebug("Downloading ZIP."); - var tempFile = await _httpClient.GetTempFile(new HttpRequestOptions - { - Url = package.sourceUrl, - CancellationToken = cancellationToken, - Progress = progress - - }).ConfigureAwait(false); - - cancellationToken.ThrowIfCancellationRequested(); - - // TODO: Validate with a checksum, *properly* - - // Check if the target directory already exists, and remove it if so - if (Directory.Exists(target)) - { - _logger.LogDebug("Deleting existing plugin at {Filename}.", target); - Directory.Delete(target, true); - } +// CA5351: Do Not Use Broken Cryptographic Algorithms +#pragma warning disable CA5351 + using (var res = await _httpClient.SendAsync( + new HttpRequestOptions + { + Url = package.sourceUrl, + CancellationToken = cancellationToken, + // We need it to be buffered for setting the position + BufferContent = true + }, + HttpMethod.Get).ConfigureAwait(false)) + using (var stream = res.Content) + using (var md5 = MD5.Create()) + { + cancellationToken.ThrowIfCancellationRequested(); + + var hash = ToHexString(md5.ComputeHash(stream)); + if (!string.Equals(package.checksum, hash, StringComparison.OrdinalIgnoreCase)) + { + _logger.LogError( + "The checksums didn't match while installing {Package}, expected: {Expected}, got: {Received}", + package.name, + package.checksum, + hash); + throw new InvalidDataException("The checksum of the received data doesn't match."); + } - // Success - move it to the real target - try - { - _logger.LogDebug("Extracting ZIP {TempFile} to {Filename}.", tempFile, target); - using (var stream = File.OpenRead(tempFile)) + if (Directory.Exists(targetDir)) { - _zipClient.ExtractAllFromZip(stream, target, true); + Directory.Delete(targetDir, true); } - } - catch (IOException ex) - { - _logger.LogError(ex, "Error attempting to extract {TempFile} to {TargetFile}", tempFile, target); - throw; - } - try - { - _logger.LogDebug("Deleting temporary file {Filename}.", tempFile); - _fileSystem.DeleteFile(tempFile); - } - catch (IOException ex) - { - // Don't fail because of this - _logger.LogError(ex, "Error deleting temp file {TempFile}", tempFile); + stream.Position = 0; + _zipClient.ExtractAllFromZip(stream, targetDir, true); } + +#pragma warning restore CA5351 } /// <summary> /// Uninstalls a plugin /// </summary> /// <param name="plugin">The plugin.</param> - /// <exception cref="ArgumentException"></exception> public void UninstallPlugin(IPlugin plugin) { plugin.OnUninstalling(); @@ -622,11 +527,34 @@ namespace Emby.Server.Implementations.Updates _config.SaveConfiguration(); } - OnPluginUninstalled(plugin); + PluginUninstalled?.Invoke(this, new GenericEventArgs<IPlugin> { Argument = plugin }); _applicationHost.NotifyPendingRestart(); } + /// <inheritdoc/> + public bool CancelInstallation(Guid id) + { + lock (_currentInstallations) + { + var install = _currentInstallations.Find(x => x.Item1.Id == id); + if (install == default((InstallationInfo, CancellationTokenSource))) + { + return false; + } + + install.Item2.Cancel(); + _currentInstallations.Remove(install); + return true; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + /// <summary> /// Releases unmanaged and - optionally - managed resources. /// </summary> @@ -635,21 +563,16 @@ namespace Emby.Server.Implementations.Updates { if (dispose) { - lock (CurrentInstallations) + lock (_currentInstallations) { - foreach (var tuple in CurrentInstallations) + foreach (var tuple in _currentInstallations) { tuple.Item2.Dispose(); } - CurrentInstallations.Clear(); + _currentInstallations.Clear(); } } } - - public void Dispose() - { - Dispose(true); - } } } diff --git a/Emby.XmlTv/Emby.XmlTv/Emby.XmlTv.csproj b/Emby.XmlTv/Emby.XmlTv/Emby.XmlTv.csproj index 0225be2c2..04f558173 100644 --- a/Emby.XmlTv/Emby.XmlTv/Emby.XmlTv.csproj +++ b/Emby.XmlTv/Emby.XmlTv/Emby.XmlTv.csproj @@ -3,6 +3,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> <ItemGroup> diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index f023bc55d..396bdd4b7 100644 --- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -3,6 +3,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> <ItemGroup> diff --git a/Jellyfin.Drawing.Skia/SkiaCodecException.cs b/Jellyfin.Drawing.Skia/SkiaCodecException.cs new file mode 100644 index 000000000..f848636bc --- /dev/null +++ b/Jellyfin.Drawing.Skia/SkiaCodecException.cs @@ -0,0 +1,46 @@ +using System.Globalization; +using SkiaSharp; + +namespace Jellyfin.Drawing.Skia +{ + /// <summary> + /// Represents errors that occur during interaction with Skia codecs. + /// </summary> + public class SkiaCodecException : SkiaException + { + /// <summary> + /// Returns the non-successfull codec result returned by Skia. + /// </summary> + /// <value>The non-successfull codec result returned by Skia.</value> + public SKCodecResult CodecResult { get; } + + /// <summary> + /// Initializes a new instance of the <see cref="SkiaCodecException" /> class. + /// </summary> + /// <param name="result">The non-successfull codec result returned by Skia.</param> + public SkiaCodecException(SKCodecResult result) : base() + { + CodecResult = result; + } + + /// <summary> + /// Initializes a new instance of the <see cref="SkiaCodecException" /> class + /// with a specified error message. + /// </summary> + /// <param name="result">The non-successfull codec result returned by Skia.</param> + /// <param name="message">The message that describes the error.</param> + public SkiaCodecException(SKCodecResult result, string message) + : base(message) + { + CodecResult = result; + } + + /// <inheritdoc /> + public override string ToString() + => string.Format( + CultureInfo.InvariantCulture, + "Non-success codec result: {0}\n{1}", + CodecResult, + base.ToString()); + } +} diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs index 80b9974fa..66b814f6e 100644 --- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -9,6 +9,7 @@ using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Globalization; using Microsoft.Extensions.Logging; using SkiaSharp; +using static Jellyfin.Drawing.Skia.SkiaHelper; namespace Jellyfin.Drawing.Skia { @@ -184,16 +185,23 @@ namespace Jellyfin.Drawing.Skia } } + /// <inheritdoc /> public ImageDimensions GetImageSize(string path) { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + if (!File.Exists(path)) { throw new FileNotFoundException("File not found", path); } - using (var s = new SKFileStream(path)) - using (var codec = SKCodec.Create(s)) + using (var codec = SKCodec.Create(path, out SKCodecResult result)) { + EnsureSuccess(result); + var info = codec.Info; return new ImageDimensions(info.Width, info.Height); diff --git a/Jellyfin.Drawing.Skia/SkiaException.cs b/Jellyfin.Drawing.Skia/SkiaException.cs new file mode 100644 index 000000000..7aeaf083e --- /dev/null +++ b/Jellyfin.Drawing.Skia/SkiaException.cs @@ -0,0 +1,26 @@ +using System; + +namespace Jellyfin.Drawing.Skia +{ + /// <summary> + /// Represents errors that occur during interaction with Skia. + /// </summary> + public class SkiaException : Exception + { + /// <inheritdoc /> + public SkiaException() : base() + { + } + + /// <inheritdoc /> + public SkiaException(string message) : base(message) + { + } + + /// <inheritdoc /> + public SkiaException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/Jellyfin.Drawing.Skia/SkiaHelper.cs b/Jellyfin.Drawing.Skia/SkiaHelper.cs new file mode 100644 index 000000000..f9c79c855 --- /dev/null +++ b/Jellyfin.Drawing.Skia/SkiaHelper.cs @@ -0,0 +1,23 @@ +using SkiaSharp; + +namespace Jellyfin.Drawing.Skia +{ + /// <summary> + /// Class containing helper methods for working with SkiaSharp. + /// </summary> + public static class SkiaHelper + { + /// <summary> + /// Ensures the result is a success + /// by throwing an exception when that's not the case. + /// </summary> + /// <param name="result">The result returned by Skia.</param> + public static void EnsureSuccess(SKCodecResult result) + { + if (result != SKCodecResult.Success) + { + throw new SkiaCodecException(result); + } + } + } +} diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index b9b0cc382..8b4b61e29 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -9,8 +9,21 @@ using Microsoft.Extensions.Logging; namespace Jellyfin.Server { + /// <summary> + /// Implementation of the abstract <see cref="ApplicationHost" /> class. + /// </summary> public class CoreAppHost : ApplicationHost { + /// <summary> + /// Initializes a new instance of the <see cref="CoreAppHost" /> class. + /// </summary> + /// <param name="applicationPaths">The <see cref="ServerApplicationPaths" /> to be used by the <see cref="CoreAppHost" />.</param> + /// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param> + /// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param> + /// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param> + /// <param name="imageEncoder">The <see cref="IImageEncoder" /> to be used by the <see cref="CoreAppHost" />.</param> + /// <param name="networkManager">The <see cref="INetworkManager" /> to be used by the <see cref="CoreAppHost" />.</param> + /// <param name="configuration">The <see cref="IConfiguration" /> to be used by the <see cref="CoreAppHost" />.</param> public CoreAppHost( ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, @@ -30,15 +43,19 @@ namespace Jellyfin.Server { } + /// <inheritdoc /> public override bool CanSelfRestart => StartupOptions.RestartPath != null; + /// <inheritdoc /> protected override void RestartInternal() => Program.Restart(); + /// <inheritdoc /> protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal() { yield return typeof(CoreAppHost).Assembly; } + /// <inheritdoc /> protected override void ShutdownInternal() => Program.Shutdown(); } } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 641b3f182..efdb75f98 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -3,16 +3,14 @@ <PropertyGroup> <AssemblyName>jellyfin</AssemblyName> <OutputType>Exe</OutputType> - <TargetFramework>netcoreapp2.1</TargetFramework> + <TargetFramework>netcoreapp3.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> <PropertyGroup> - <!-- We need C# 7.1 for async main--> + <!-- We need at least C# 7.1 for async main--> <LangVersion>latest</LangVersion> - <!-- Disable documentation warnings (for now) --> - <NoWarn>SA1600;SA1601;SA1629;CS1591</NoWarn> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> @@ -24,9 +22,9 @@ <EmbeddedResource Include="Resources/Configuration/*" /> </ItemGroup> - <!-- Code analysers--> + <!-- Code analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.3" /> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.4" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" /> </ItemGroup> @@ -36,22 +34,21 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="CommandLineParser" Version="2.5.0" /> + <PackageReference Include="CommandLineParser" Version="2.6.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.2.4" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0" /> - <PackageReference Include="Serilog.AspNetCore" Version="2.1.1" /> + <PackageReference Include="Serilog.AspNetCore" Version="3.0.0" /> <PackageReference Include="Serilog.Settings.Configuration" Version="3.1.0" /> <PackageReference Include="Serilog.Sinks.Async" Version="1.4.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" /> <PackageReference Include="Serilog.Sinks.File" Version="4.0.0" /> <PackageReference Include="SkiaSharp" Version="1.68.0" /> - <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="1.1.14" /> + <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.1" /> <PackageReference Include="SQLitePCLRaw.provider.sqlite3.netstandard11" Version="1.1.14" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" /> - <ProjectReference Include="..\Emby.IsoMounting\IsoMounter\IsoMounter.csproj" /> <ProjectReference Include="..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" /> <ProjectReference Include="..\Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj" /> </ItemGroup> diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 08c0983be..e8bd0cd30 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -18,17 +18,19 @@ using Jellyfin.Drawing.Skia; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.IO; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Serilog; -using Serilog.AspNetCore; +using Serilog.Extensions.Logging; using SQLitePCL; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Jellyfin.Server { + /// <summary> + /// Class containing the entry point of the application. + /// </summary> public static class Program { private static readonly CancellationTokenSource _tokenSource = new CancellationTokenSource(); @@ -36,17 +38,22 @@ namespace Jellyfin.Server private static ILogger _logger; private static bool _restartOnShutdown; + /// <summary> + /// The entry point of the application. + /// </summary> + /// <param name="args">The command line arguments passed.</param> + /// <returns><see cref="Task" />.</returns> public static Task Main(string[] args) { // For backwards compatibility. // Modify any input arguments now which start with single-hyphen to POSIX standard // double-hyphen to allow parsing by CommandLineParser package. - const string pattern = @"^(-[^-\s]{2})"; // Match -xx, not -x, not --xx, not xx - const string substitution = @"-$1"; // Prepend with additional single-hyphen - var regex = new Regex(pattern); + const string Pattern = @"^(-[^-\s]{2})"; // Match -xx, not -x, not --xx, not xx + const string Substitution = @"-$1"; // Prepend with additional single-hyphen + var regex = new Regex(Pattern); for (var i = 0; i < args.Length; i++) { - args[i] = regex.Replace(args[i], substitution); + args[i] = regex.Replace(args[i], Substitution); } // Parse the command line arguments and either start the app or exit indicating error @@ -54,7 +61,10 @@ namespace Jellyfin.Server .MapResult(StartApp, _ => Task.CompletedTask); } - public static void Shutdown() + /// <summary> + /// Shuts down the application. + /// </summary> + internal static void Shutdown() { if (!_tokenSource.IsCancellationRequested) { @@ -62,7 +72,10 @@ namespace Jellyfin.Server } } - public static void Restart() + /// <summary> + /// Restarts the application. + /// </summary> + internal static void Restart() { _restartOnShutdown = true; @@ -71,6 +84,8 @@ namespace Jellyfin.Server private static async Task StartApp(StartupOptions options) { + var stopWatch = new Stopwatch(); + stopWatch.Start(); ServerApplicationPaths appPaths = CreateApplicationPaths(options); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager @@ -112,7 +127,9 @@ namespace Jellyfin.Server Shutdown(); }; - _logger.LogInformation("Jellyfin version: {Version}", Assembly.GetEntryAssembly().GetName().Version); + _logger.LogInformation( + "Jellyfin version: {Version}", + Assembly.GetEntryAssembly().GetName().Version.ToString(3)); ApplicationHost.LogEnvironmentInfo(_logger, appPaths); @@ -134,17 +151,18 @@ namespace Jellyfin.Server Batteries_V2.Init(); if (raw.sqlite3_enable_shared_cache(1) != raw.SQLITE_OK) { - Console.WriteLine("WARN: Failed to enable shared cache for SQLite"); + _logger.LogWarning("Failed to enable shared cache for SQLite"); } - using (var appHost = new CoreAppHost( + var appHost = new CoreAppHost( appPaths, _loggerFactory, options, new ManagedFileSystem(_loggerFactory.CreateLogger<ManagedFileSystem>(), appPaths), new NullImageEncoder(), new NetworkManager(_loggerFactory.CreateLogger<NetworkManager>()), - appConfig)) + appConfig); + try { await appHost.InitAsync(new ServiceCollection()).ConfigureAwait(false); @@ -152,15 +170,24 @@ namespace Jellyfin.Server await appHost.RunStartupTasksAsync().ConfigureAwait(false); - try - { - // Block main thread until shutdown - await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false); - } - catch (TaskCanceledException) - { - // Don't throw on cancellation - } + stopWatch.Stop(); + + _logger.LogInformation("Startup complete {Time:g}", stopWatch.Elapsed); + + // Block main thread until shutdown + await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false); + } + catch (TaskCanceledException) + { + // Don't throw on cancellation + } + catch (Exception ex) + { + _logger.LogCritical(ex, "Error while starting server."); + } + finally + { + appHost?.Dispose(); } if (_restartOnShutdown) @@ -172,11 +199,12 @@ namespace Jellyfin.Server /// <summary> /// Create the data, config and log paths from the variety of inputs(command line args, /// environment variables) or decide on what default to use. For Windows it's %AppPath% - /// for everything else the XDG approach is followed: - /// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + /// for everything else the + /// <a href="https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html">XDG approach</a> + /// is followed. /// </summary> - /// <param name="options">StartupOptions</param> - /// <returns>ServerApplicationPaths</returns> + /// <param name="options">The <see cref="StartupOptions" /> for this instance.</param> + /// <returns><see cref="ServerApplicationPaths" />.</returns> private static ServerApplicationPaths CreateApplicationPaths(StartupOptions options) { // dataDir @@ -292,7 +320,7 @@ namespace Jellyfin.Server if (string.IsNullOrEmpty(webDir)) { // Use default location under ResourcesPath - webDir = Path.Combine(AppContext.BaseDirectory, "jellyfin-web", "src"); + webDir = Path.Combine(AppContext.BaseDirectory, "jellyfin-web"); } } @@ -348,7 +376,7 @@ namespace Jellyfin.Server return new ConfigurationBuilder() .SetBasePath(appPaths.ConfigurationDirectoryPath) - .AddJsonFile("logging.json") + .AddJsonFile("logging.json", false, true) .AddEnvironmentVariables("JELLYFIN_") .AddInMemoryCollection(ConfigurationOptions.Configuration) .Build(); diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index 8296d414e..bb0adaf63 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -8,36 +8,62 @@ namespace Jellyfin.Server /// </summary> public class StartupOptions : IStartupOptions { + /// <summary> + /// Gets or sets the path to the data directory. + /// </summary> + /// <value>The path to the data directory.</value> [Option('d', "datadir", Required = false, HelpText = "Path to use for the data folder (database files, etc.).")] public string DataDir { get; set; } + /// <summary> + /// Gets or sets the path to the web directory. + /// </summary> + /// <value>The path to the web directory.</value> [Option('w', "webdir", Required = false, HelpText = "Path to the Jellyfin web UI resources.")] public string WebDir { get; set; } + /// <summary> + /// Gets or sets the path to the cache directory. + /// </summary> + /// <value>The path to the cache directory.</value> [Option('C', "cachedir", Required = false, HelpText = "Path to use for caching.")] public string CacheDir { get; set; } + /// <summary> + /// Gets or sets the path to the config directory. + /// </summary> + /// <value>The path to the config directory.</value> [Option('c', "configdir", Required = false, HelpText = "Path to use for configuration data (user settings and pictures).")] public string ConfigDir { get; set; } + /// <summary> + /// Gets or sets the path to the log directory. + /// </summary> + /// <value>The path to the log directory.</value> [Option('l', "logdir", Required = false, HelpText = "Path to use for writing log files.")] public string LogDir { get; set; } + /// <inheritdoc /> [Option("ffmpeg", Required = false, HelpText = "Path to external FFmpeg executable to use in place of default found in PATH.")] public string FFmpegPath { get; set; } + /// <inheritdoc /> [Option("service", Required = false, HelpText = "Run as headless service.")] public bool IsService { get; set; } + /// <inheritdoc /> [Option("noautorunwebapp", Required = false, HelpText = "Run headless if startup wizard is complete.")] public bool NoAutoRunWebApp { get; set; } + /// <inheritdoc /> [Option("package-name", Required = false, HelpText = "Used when packaging Jellyfin (example, synology).")] public string PackageName { get; set; } + /// <inheritdoc /> [Option("restartpath", Required = false, HelpText = "Path to restart script.")] public string RestartPath { get; set; } + /// <inheritdoc /> [Option("restartargs", Required = false, HelpText = "Arguments for restart script.")] public string RestartArgs { get; set; } } diff --git a/MediaBrowser.Api/ApiEntryPoint.cs b/MediaBrowser.Api/ApiEntryPoint.cs index a223a4fe3..7dca7e814 100644 --- a/MediaBrowser.Api/ApiEntryPoint.cs +++ b/MediaBrowser.Api/ApiEntryPoint.cs @@ -610,9 +610,8 @@ namespace MediaBrowser.Api process.Kill(); } } - catch (Exception ex) + catch (InvalidOperationException) { - Logger.LogError(ex, "Error killing transcoding job for {Path}", job.Path); } } } diff --git a/MediaBrowser.Api/Devices/DeviceService.cs b/MediaBrowser.Api/Devices/DeviceService.cs index dc211af6b..697a84f5c 100644 --- a/MediaBrowser.Api/Devices/DeviceService.cs +++ b/MediaBrowser.Api/Devices/DeviceService.cs @@ -133,12 +133,15 @@ namespace MediaBrowser.Api.Devices var album = Request.QueryString["Album"]; var id = Request.QueryString["Id"]; var name = Request.QueryString["Name"]; + var req = Request.Response.HttpContext.Request; - if (Request.ContentType.IndexOf("multi", StringComparison.OrdinalIgnoreCase) == -1) + if (req.HasFormContentType) { - return _deviceManager.AcceptCameraUpload(deviceId, request.RequestStream, new LocalFileInfo + var file = req.Form.Files.Count == 0 ? null : req.Form.Files[0]; + + return _deviceManager.AcceptCameraUpload(deviceId, file.OpenReadStream(), new LocalFileInfo { - MimeType = Request.ContentType, + MimeType = file.ContentType, Album = album, Name = name, Id = id @@ -146,11 +149,9 @@ namespace MediaBrowser.Api.Devices } else { - var file = Request.Files.Length == 0 ? null : Request.Files[0]; - - return _deviceManager.AcceptCameraUpload(deviceId, file.InputStream, new LocalFileInfo + return _deviceManager.AcceptCameraUpload(deviceId, request.RequestStream, new LocalFileInfo { - MimeType = file.ContentType, + MimeType = Request.ContentType, Album = album, Name = name, Id = id diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index 10bbc9e5d..6d3037b24 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading; @@ -537,7 +538,7 @@ namespace MediaBrowser.Api.Images if (item == null) { - throw new ResourceNotFoundException(string.Format("Item {0} not found.", itemId.ToString("N"))); + throw new ResourceNotFoundException(string.Format("Item {0} not found.", itemId.ToString("N", CultureInfo.InvariantCulture))); } } @@ -549,14 +550,14 @@ namespace MediaBrowser.Api.Images } IImageEnhancer[] supportedImageEnhancers; - if (_imageProcessor.ImageEnhancers.Length > 0) + if (_imageProcessor.ImageEnhancers.Count > 0) { if (item == null) { item = _libraryManager.GetItemById(itemId); } - supportedImageEnhancers = request.EnableImageEnhancers ? _imageProcessor.GetSupportedEnhancers(item, request.Type) : Array.Empty<IImageEnhancer>(); + supportedImageEnhancers = request.EnableImageEnhancers ? _imageProcessor.GetSupportedEnhancers(item, request.Type).ToArray() : Array.Empty<IImageEnhancer>(); } else { @@ -605,8 +606,8 @@ namespace MediaBrowser.Api.Images ImageRequest request, ItemImageInfo image, bool cropwhitespace, - ImageFormat[] supportedFormats, - IImageEnhancer[] enhancers, + IReadOnlyCollection<ImageFormat> supportedFormats, + IReadOnlyCollection<IImageEnhancer> enhancers, TimeSpan? cacheDuration, IDictionary<string, string> headers, bool isHeadRequest) diff --git a/MediaBrowser.Api/ItemUpdateService.cs b/MediaBrowser.Api/ItemUpdateService.cs index 825732888..d6514d62e 100644 --- a/MediaBrowser.Api/ItemUpdateService.cs +++ b/MediaBrowser.Api/ItemUpdateService.cs @@ -69,8 +69,8 @@ namespace MediaBrowser.Api { ParentalRatingOptions = _localizationManager.GetParentalRatings().ToArray(), ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(), - Countries = await _localizationManager.GetCountries(), - Cultures = _localizationManager.GetCultures() + Countries = _localizationManager.GetCountries().ToArray(), + Cultures = _localizationManager.GetCultures().ToArray() }; if (!item.IsVirtualItem && !(item is ICollectionFolder) && !(item is UserView) && !(item is AggregateFolder) && !(item is LiveTvChannel) && !(item is IItemByName) && diff --git a/MediaBrowser.Api/Library/LibraryStructureService.cs b/MediaBrowser.Api/Library/LibraryStructureService.cs index d6bcf7878..7266bf9f9 100644 --- a/MediaBrowser.Api/Library/LibraryStructureService.cs +++ b/MediaBrowser.Api/Library/LibraryStructureService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading; @@ -272,7 +273,7 @@ namespace MediaBrowser.Api.Library // Changing capitalization. Handle windows case insensitivity if (string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase)) { - var tempPath = Path.Combine(rootFolderPath, Guid.NewGuid().ToString("N")); + var tempPath = Path.Combine(rootFolderPath, Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)); Directory.Move(currentPath, tempPath); currentPath = tempPath; } diff --git a/MediaBrowser.Api/LiveTv/LiveTvService.cs b/MediaBrowser.Api/LiveTv/LiveTvService.cs index e41ad540a..b05e8c949 100644 --- a/MediaBrowser.Api/LiveTv/LiveTvService.cs +++ b/MediaBrowser.Api/LiveTv/LiveTvService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -24,6 +25,7 @@ using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Services; using Microsoft.Net.Http.Headers; +using static MediaBrowser.Common.HexHelper; namespace MediaBrowser.Api.LiveTv { @@ -742,7 +744,7 @@ namespace MediaBrowser.Api.LiveTv var result = new QueryResult<BaseItemDto> { Items = returnArray, - TotalRecordCount = returnArray.Length + TotalRecordCount = returnArray.Count }; return ToOptimizedResult(result); @@ -883,10 +885,12 @@ namespace MediaBrowser.Api.LiveTv /// </summary> private string GetHashedString(string str) { - // legacy - return BitConverter.ToString( - _cryptographyProvider.ComputeSHA1(Encoding.UTF8.GetBytes(str))) - .Replace("-", string.Empty).ToLowerInvariant(); + // SchedulesDirect requires a SHA1 hash of the user's password + // https://github.com/SchedulesDirect/JSON-Service/wiki/API-20141201#obtain-a-token + using (SHA1 sha = SHA1.Create()) { + return ToHexString( + sha.ComputeHash(Encoding.UTF8.GetBytes(str))); + } } public void Delete(DeleteListingProvider request) diff --git a/MediaBrowser.Api/LocalizationService.cs b/MediaBrowser.Api/LocalizationService.cs index eeff67e13..3b2e18852 100644 --- a/MediaBrowser.Api/LocalizationService.cs +++ b/MediaBrowser.Api/LocalizationService.cs @@ -1,4 +1,3 @@ -using System.Threading.Tasks; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; @@ -82,9 +81,9 @@ namespace MediaBrowser.Api /// </summary> /// <param name="request">The request.</param> /// <returns>System.Object.</returns> - public async Task<object> Get(GetCountries request) + public object Get(GetCountries request) { - var result = await _localization.GetCountries(); + var result = _localization.GetCountries(); return ToOptimizedResult(result); } diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index ba29c656b..f653270a6 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -12,6 +12,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> </Project> diff --git a/MediaBrowser.Api/Movies/MoviesService.cs b/MediaBrowser.Api/Movies/MoviesService.cs index 91766255f..c1c6ffc2e 100644 --- a/MediaBrowser.Api/Movies/MoviesService.cs +++ b/MediaBrowser.Api/Movies/MoviesService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; @@ -11,7 +12,6 @@ using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Services; @@ -243,7 +243,7 @@ namespace MediaBrowser.Api.Movies } } - return categories.OrderBy(i => i.RecommendationType).ThenBy(i => Guid.NewGuid()); + return categories.OrderBy(i => i.RecommendationType); } private IEnumerable<RecommendationDto> GetWithDirector(User user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type) @@ -268,7 +268,7 @@ namespace MediaBrowser.Api.Movies EnableGroupByMetadataKey = true, DtoOptions = dtoOptions - }).GroupBy(i => i.GetProviderId(MetadataProviders.Imdb) ?? Guid.NewGuid().ToString("N")) + }).GroupBy(i => i.GetProviderId(MetadataProviders.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)) .Select(x => x.First()) .Take(itemLimit) .ToList(); @@ -309,7 +309,7 @@ namespace MediaBrowser.Api.Movies EnableGroupByMetadataKey = true, DtoOptions = dtoOptions - }).GroupBy(i => i.GetProviderId(MetadataProviders.Imdb) ?? Guid.NewGuid().ToString("N")) + }).GroupBy(i => i.GetProviderId(MetadataProviders.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)) .Select(x => x.First()) .Take(itemLimit) .ToList(); diff --git a/MediaBrowser.Api/Music/AlbumsService.cs b/MediaBrowser.Api/Music/AlbumsService.cs index c48bcde5c..2cd3a1003 100644 --- a/MediaBrowser.Api/Music/AlbumsService.cs +++ b/MediaBrowser.Api/Music/AlbumsService.cs @@ -104,16 +104,15 @@ namespace MediaBrowser.Api.Music var album2 = (MusicAlbum)item2; var artists1 = album1 - .AllArtists + .GetAllArtists() .DistinctNames() .ToList(); - var artists2 = album2 - .AllArtists - .DistinctNames() - .ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); + var artists2 = new HashSet<string>( + album2.GetAllArtists().DistinctNames(), + StringComparer.OrdinalIgnoreCase); - return points + artists1.Where(artists2.ContainsKey).Sum(i => 5); + return points + artists1.Where(artists2.Contains).Sum(i => 5); } } } diff --git a/MediaBrowser.Api/PackageService.cs b/MediaBrowser.Api/PackageService.cs index fbb876dea..baa6f7bb9 100644 --- a/MediaBrowser.Api/PackageService.cs +++ b/MediaBrowser.Api/PackageService.cs @@ -197,7 +197,7 @@ namespace MediaBrowser.Api throw new ResourceNotFoundException(string.Format("Package not found: {0}", request.Name)); } - await _installationManager.InstallPackage(package, true, new SimpleProgress<double>(), CancellationToken.None); + await _installationManager.InstallPackage(package); } /// <summary> @@ -206,13 +206,7 @@ namespace MediaBrowser.Api /// <param name="request">The request.</param> public void Delete(CancelPackageInstallation request) { - var info = _installationManager.CurrentInstallations.FirstOrDefault(i => i.Item1.Id.Equals(request.Id)); - - if (info != null) - { - info.Item2.Cancel(); - } + _installationManager.CancelInstallation(new Guid(request.Id)); } } - } diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 399401624..8c4ccfa22 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -137,12 +137,9 @@ namespace MediaBrowser.Api.Playback /// </summary> private string GetOutputFilePath(StreamState state, EncodingOptions encodingOptions, string outputFileExtension) { - var data = GetCommandLineArguments("dummy\\dummy", encodingOptions, state, false); + var data = $"{state.MediaPath}-{state.UserAgent}-{state.Request.DeviceId}-{state.Request.PlaySessionId}"; - data += "-" + (state.Request.DeviceId ?? string.Empty) - + "-" + (state.Request.PlaySessionId ?? string.Empty); - - var filename = data.GetMD5().ToString("N"); + var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture); var ext = outputFileExtension.ToLowerInvariant(); var folder = ServerConfigurationManager.ApplicationPaths.TranscodingTempPath; @@ -154,7 +151,12 @@ namespace MediaBrowser.Api.Playback return Path.Combine(folder, filename + ext); } - protected virtual string GetDefaultH264Preset() => "superfast"; + protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); + + protected virtual string GetDefaultEncoderPreset() + { + return "superfast"; + } private async Task AcquireResources(StreamState state, CancellationTokenSource cancellationTokenSource) { @@ -240,7 +242,7 @@ namespace MediaBrowser.Api.Playback var transcodingJob = ApiEntryPoint.Instance.OnTranscodeBeginning(outputPath, state.Request.PlaySessionId, state.MediaSource.LiveStreamId, - Guid.NewGuid().ToString("N"), + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), TranscodingJobType, process, state.Request.DeviceId, @@ -632,7 +634,7 @@ namespace MediaBrowser.Api.Playback } else { - value = value.Substring(Npt.Length, index); + value = value.Substring(Npt.Length, index - Npt.Length); } if (value.IndexOf(':') == -1) @@ -690,8 +692,8 @@ namespace MediaBrowser.Api.Playback request.AudioCodec = EncodingHelper.InferAudioCodec(url); } - var enableDlnaHeaders = !string.IsNullOrWhiteSpace(request.Params) /*|| - string.Equals(Request.Headers.Get("GetContentFeatures.DLNA.ORG"), "1", StringComparison.OrdinalIgnoreCase)*/; + var enableDlnaHeaders = !string.IsNullOrWhiteSpace(request.Params) || + string.Equals(GetHeader("GetContentFeatures.DLNA.ORG"), "1", StringComparison.OrdinalIgnoreCase); var state = new StreamState(MediaSourceManager, TranscodingJobType) { @@ -954,7 +956,10 @@ namespace MediaBrowser.Api.Playback if (string.Equals(GetHeader("getMediaInfo.sec"), "1", StringComparison.OrdinalIgnoreCase)) { var ms = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalMilliseconds; - responseHeaders["MediaInfo.sec"] = string.Format("SEC_Duration={0};", Convert.ToInt32(ms).ToString(CultureInfo.InvariantCulture)); + responseHeaders["MediaInfo.sec"] = string.Format( + CultureInfo.InvariantCulture, + "SEC_Duration={0};", + Convert.ToInt32(ms)); } if (!isStaticallyStreamed && profile != null) @@ -972,8 +977,7 @@ namespace MediaBrowser.Api.Playback if (state.VideoRequest == null) { - responseHeaders["contentFeatures.dlna.org"] = new ContentFeatureBuilder(profile) - .BuildAudioHeader( + responseHeaders["contentFeatures.dlna.org"] = new ContentFeatureBuilder(profile).BuildAudioHeader( state.OutputContainer, audioCodec, state.OutputAudioBitrate, @@ -982,15 +986,13 @@ namespace MediaBrowser.Api.Playback state.OutputAudioBitDepth, isStaticallyStreamed, state.RunTimeTicks, - state.TranscodeSeekInfo - ); + state.TranscodeSeekInfo); } else { var videoCodec = state.ActualOutputVideoCodec; - responseHeaders["contentFeatures.dlna.org"] = new ContentFeatureBuilder(profile) - .BuildVideoHeader( + responseHeaders["contentFeatures.dlna.org"] = new ContentFeatureBuilder(profile).BuildVideoHeader( state.OutputContainer, videoCodec, audioCodec, @@ -1012,14 +1014,7 @@ namespace MediaBrowser.Api.Playback state.TargetVideoStreamCount, state.TargetAudioStreamCount, state.TargetVideoCodecTag, - state.IsTargetAVC - - ).FirstOrDefault() ?? string.Empty; - } - - foreach (var item in responseHeaders) - { - Request.Response.AddHeader(item.Key, item.Value); + state.IsTargetAVC).FirstOrDefault() ?? string.Empty; } } @@ -1028,8 +1023,16 @@ namespace MediaBrowser.Api.Playback var runtimeSeconds = TimeSpan.FromTicks(state.RunTimeTicks.Value).TotalSeconds.ToString(CultureInfo.InvariantCulture); var startSeconds = TimeSpan.FromTicks(state.Request.StartTimeTicks ?? 0).TotalSeconds.ToString(CultureInfo.InvariantCulture); - responseHeaders["TimeSeekRange.dlna.org"] = string.Format("npt={0}-{1}/{1}", startSeconds, runtimeSeconds); - responseHeaders["X-AvailableSeekRange"] = string.Format("1 npt={0}-{1}", startSeconds, runtimeSeconds); + responseHeaders["TimeSeekRange.dlna.org"] = string.Format( + CultureInfo.InvariantCulture, + "npt={0}-{1}/{1}", + startSeconds, + runtimeSeconds); + responseHeaders["X-AvailableSeekRange"] = string.Format( + CultureInfo.InvariantCulture, + "1 npt={0}-{1}", + startSeconds, + runtimeSeconds); } } } diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index 3c6d33e7e..27eb67ee6 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -304,7 +304,7 @@ namespace MediaBrowser.Api.Playback.Hls return args; } - protected override string GetDefaultH264Preset() + protected override string GetDefaultEncoderPreset() { return "veryfast"; } diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index fdae59f56..f5f753684 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -895,21 +895,27 @@ namespace MediaBrowser.Api.Playback.Hls // See if we can save come cpu cycles by avoiding encoding if (string.Equals(codec, "copy", StringComparison.OrdinalIgnoreCase)) { - if (state.VideoStream != null && EncodingHelper.IsH264(state.VideoStream) && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) + if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) { - args += " -bsf:v h264_mp4toannexb"; + string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream); + if (!string.IsNullOrEmpty(bitStreamArgs)) + { + args += " " + bitStreamArgs; + } } //args += " -flags -global_header"; } else { - var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"", + var keyFrameArg = string.Format( + " -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"", + GetStartNumber(state) * state.SegmentLength, state.SegmentLength.ToString(CultureInfo.InvariantCulture)); var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; - args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultH264Preset()) + keyFrameArg; + args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultEncoderPreset()) + keyFrameArg; //args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0"; @@ -961,10 +967,10 @@ namespace MediaBrowser.Api.Playback.Hls var timeDeltaParam = string.Empty; - if (isEncoding && startNumber > 0) + if (isEncoding && state.TargetFramerate > 0) { - var startTime = state.SegmentLength * startNumber; - timeDeltaParam = string.Format("-segment_time_delta -{0}", startTime); + float startTime = 1 / (state.TargetFramerate.Value * 2); + timeDeltaParam = string.Format(CultureInfo.InvariantCulture, "-segment_time_delta {0:F3}", startTime); } var segmentFormat = GetSegmentFileExtension(state.Request).TrimStart('.'); @@ -973,11 +979,8 @@ namespace MediaBrowser.Api.Playback.Hls segmentFormat = "mpegts"; } - var breakOnNonKeyFrames = state.EnableBreakOnNonKeyFrames(videoCodec); - - var breakOnNonKeyFramesArg = breakOnNonKeyFrames ? " -break_non_keyframes 1" : ""; - - return string.Format("{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0{12} -segment_format {11} -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"", + return string.Format( + "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {10} -individual_header_trailer 0 -segment_format {11} -segment_list_type m3u8 -segment_start_number {7} -segment_list \"{8}\" -y \"{9}\"", inputModifier, EncodingHelper.GetInputArgument(state, encodingOptions), threads, @@ -989,8 +992,7 @@ namespace MediaBrowser.Api.Playback.Hls outputPath, outputTsArg, timeDeltaParam, - segmentFormat, - breakOnNonKeyFramesArg + segmentFormat ).Trim(); } } diff --git a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs index 3c715c5ad..4a5f4025b 100644 --- a/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/VideoHlsService.cs @@ -92,10 +92,14 @@ namespace MediaBrowser.Api.Playback.Hls if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase)) { // if h264_mp4toannexb is ever added, do not use it for live tv - if (state.VideoStream != null && EncodingHelper.IsH264(state.VideoStream) && + if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) { - args += " -bsf:v h264_mp4toannexb"; + string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream); + if (!string.IsNullOrEmpty(bitStreamArgs)) + { + args += " " + bitStreamArgs; + } } } else @@ -105,7 +109,7 @@ namespace MediaBrowser.Api.Playback.Hls var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; - args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultH264Preset()) + keyFrameArg; + args += " " + EncodingHelper.GetVideoQualityParam(state, codec, encodingOptions, GetDefaultEncoderPreset()) + keyFrameArg; // Add resolution params, if specified if (!hasGraphicalSubs) diff --git a/MediaBrowser.Api/Playback/MediaInfoService.cs b/MediaBrowser.Api/Playback/MediaInfoService.cs index ab3994a63..da8f99a3d 100644 --- a/MediaBrowser.Api/Playback/MediaInfoService.cs +++ b/MediaBrowser.Api/Playback/MediaInfoService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -306,7 +307,7 @@ namespace MediaBrowser.Api.Playback { result.MediaSources = Clone(result.MediaSources); - result.PlaySessionId = Guid.NewGuid().ToString("N"); + result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); } return result; diff --git a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs index 83a3f3e3c..97c1a7a49 100644 --- a/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs +++ b/MediaBrowser.Api/Playback/Progressive/BaseProgressiveStreamingService.cs @@ -77,7 +77,8 @@ namespace MediaBrowser.Api.Playback.Progressive { var videoCodec = state.VideoRequest.VideoCodec; - if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) || + string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase)) { return ".ts"; } @@ -279,18 +280,24 @@ namespace MediaBrowser.Api.Playback.Progressive /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param> /// <param name="cancellationTokenSource">The cancellation token source.</param> /// <returns>Task{System.Object}.</returns> - private async Task<object> GetStaticRemoteStreamResult(StreamState state, Dictionary<string, string> responseHeaders, bool isHeadRequest, CancellationTokenSource cancellationTokenSource) + private async Task<object> GetStaticRemoteStreamResult( + StreamState state, + Dictionary<string, string> responseHeaders, + bool isHeadRequest, + CancellationTokenSource cancellationTokenSource) { - state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent); - var options = new HttpRequestOptions { Url = state.MediaPath, - UserAgent = useragent, BufferContent = false, CancellationToken = cancellationTokenSource.Token }; + if (state.RemoteHttpHeaders.TryGetValue(HeaderNames.UserAgent, out var useragent)) + { + options.UserAgent = useragent; + } + var response = await HttpClient.GetResponse(options).ConfigureAwait(false); responseHeaders[HeaderNames.AcceptRanges] = "none"; @@ -305,7 +312,7 @@ namespace MediaBrowser.Api.Playback.Progressive { using (response) { - return ResultFactory.GetResult(null, new byte[] { }, response.ContentType, responseHeaders); + return ResultFactory.GetResult(null, Array.Empty<byte>(), response.ContentType, responseHeaders); } } diff --git a/MediaBrowser.Api/Playback/Progressive/VideoService.cs b/MediaBrowser.Api/Playback/Progressive/VideoService.cs index ab19fdc26..cfc8a283d 100644 --- a/MediaBrowser.Api/Playback/Progressive/VideoService.cs +++ b/MediaBrowser.Api/Playback/Progressive/VideoService.cs @@ -120,7 +120,7 @@ namespace MediaBrowser.Api.Playback.Progressive protected override string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding) { - return EncodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, GetDefaultH264Preset()); + return EncodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, GetDefaultEncoderPreset()); } } } diff --git a/MediaBrowser.Api/PlaylistService.cs b/MediaBrowser.Api/PlaylistService.cs index 482c1a2a8..483bf98fb 100644 --- a/MediaBrowser.Api/PlaylistService.cs +++ b/MediaBrowser.Api/PlaylistService.cs @@ -189,11 +189,9 @@ namespace MediaBrowser.Api var dtos = _dtoService.GetBaseItemDtos(items.Select(i => i.Item2).ToList(), dtoOptions, user); - var index = 0; - foreach (var item in dtos) + for (int index = 0; index < dtos.Count; index++) { - item.PlaylistItemId = items[index].Item1.Id; - index++; + dtos[index].PlaylistItemId = items[index].Item1.Id; } var result = new QueryResult<BaseItemDto> diff --git a/MediaBrowser.Api/SearchService.cs b/MediaBrowser.Api/SearchService.cs index ecf07c912..6c67d4fb1 100644 --- a/MediaBrowser.Api/SearchService.cs +++ b/MediaBrowser.Api/SearchService.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Linq; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; @@ -305,7 +306,7 @@ namespace MediaBrowser.Api if (tag != null) { hint.ThumbImageTag = tag; - hint.ThumbImageItemId = itemWithImage.Id.ToString("N"); + hint.ThumbImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture); } } } @@ -326,7 +327,7 @@ namespace MediaBrowser.Api if (tag != null) { hint.BackdropImageTag = tag; - hint.BackdropImageItemId = itemWithImage.Id.ToString("N"); + hint.BackdropImageItemId = itemWithImage.Id.ToString("N", CultureInfo.InvariantCulture); } } } diff --git a/MediaBrowser.Api/Session/SessionsService.cs b/MediaBrowser.Api/Session/SessionsService.cs index 4109b12bf..76392e27c 100644 --- a/MediaBrowser.Api/Session/SessionsService.cs +++ b/MediaBrowser.Api/Session/SessionsService.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -316,7 +317,7 @@ namespace MediaBrowser.Api.Session _authRepo.Create(new AuthenticationInfo { AppName = request.App, - AccessToken = Guid.NewGuid().ToString("N"), + AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture), DateCreated = DateTime.UtcNow, DeviceId = _appHost.SystemId, DeviceName = _appHost.FriendlyName, diff --git a/MediaBrowser.Api/StartupWizardService.cs b/MediaBrowser.Api/StartupWizardService.cs index 53ba7eefd..3a9eb7a55 100644 --- a/MediaBrowser.Api/StartupWizardService.cs +++ b/MediaBrowser.Api/StartupWizardService.cs @@ -113,7 +113,8 @@ namespace MediaBrowser.Api _userManager.UpdateUser(user); - if (!string.IsNullOrEmpty(request.Password)) { + if (!string.IsNullOrEmpty(request.Password)) + { await _userManager.ChangePassword(user, request.Password).ConfigureAwait(false); } } diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs index 08aa540a5..52043d3df 100644 --- a/MediaBrowser.Api/Subtitles/SubtitleService.cs +++ b/MediaBrowser.Api/Subtitles/SubtitleService.cs @@ -168,7 +168,7 @@ namespace MediaBrowser.Api.Subtitles builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0"); builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD"); - long positionTicks = 0; + long positionTicks = 0; var accessToken = _authContext.GetAuthorizationInfo(Request).Token; @@ -206,7 +206,7 @@ namespace MediaBrowser.Api.Subtitles { var item = (Video)_libraryManager.GetItemById(request.Id); - var idString = request.Id.ToString("N"); + var idString = request.Id.ToString("N", CultureInfo.InvariantCulture); var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false, null) .First(i => string.Equals(i.Id, request.MediaSourceId ?? idString)); diff --git a/MediaBrowser.Api/SuggestionsService.cs b/MediaBrowser.Api/SuggestionsService.cs index 72fdae365..4e857eafc 100644 --- a/MediaBrowser.Api/SuggestionsService.cs +++ b/MediaBrowser.Api/SuggestionsService.cs @@ -61,11 +61,6 @@ namespace MediaBrowser.Api var dtoList = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user); - if (dtoList == null) - { - throw new InvalidOperationException("GetBaseItemDtos returned null"); - } - return new QueryResult<BaseItemDto> { TotalRecordCount = result.TotalRecordCount, diff --git a/MediaBrowser.Api/TvShowsService.cs b/MediaBrowser.Api/TvShowsService.cs index b0900a554..1340bd8ef 100644 --- a/MediaBrowser.Api/TvShowsService.cs +++ b/MediaBrowser.Api/TvShowsService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dto; @@ -381,13 +382,13 @@ namespace MediaBrowser.Api throw new ResourceNotFoundException("Series not found"); } - var seasons = (series.GetItemList(new InternalItemsQuery(user) + var seasons = series.GetItemList(new InternalItemsQuery(user) { IsMissing = request.IsMissing, IsSpecialSeason = request.IsSpecialSeason, AdjacentTo = request.AdjacentTo - })); + }); var dtoOptions = GetDtoOptions(_authContext, request); @@ -395,7 +396,7 @@ namespace MediaBrowser.Api return new QueryResult<BaseItemDto> { - TotalRecordCount = returnItems.Length, + TotalRecordCount = returnItems.Count, Items = returnItems }; } @@ -470,7 +471,7 @@ namespace MediaBrowser.Api if (!string.IsNullOrWhiteSpace(request.StartItemId)) { - episodes = episodes.SkipWhile(i => !string.Equals(i.Id.ToString("N"), request.StartItemId, StringComparison.OrdinalIgnoreCase)).ToList(); + episodes = episodes.SkipWhile(i => !string.Equals(i.Id.ToString("N", CultureInfo.InvariantCulture), request.StartItemId, StringComparison.OrdinalIgnoreCase)).ToList(); } // This must be the last filter diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs index f842230ee..b4a302648 100644 --- a/MediaBrowser.Api/UserLibrary/ItemsService.cs +++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs @@ -99,7 +99,7 @@ namespace MediaBrowser.Api.UserLibrary { ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true) .Where(i => i is Folder) - .Where(i => !excludeFolderIds.Contains(i.Id.ToString("N"))) + .Where(i => !excludeFolderIds.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))) .Select(i => i.Id) .ToArray(); } @@ -175,11 +175,6 @@ namespace MediaBrowser.Api.UserLibrary var dtoList = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user); - if (dtoList == null) - { - throw new InvalidOperationException("GetBaseItemDtos returned null"); - } - return new QueryResult<BaseItemDto> { TotalRecordCount = result.TotalRecordCount, @@ -228,7 +223,9 @@ namespace MediaBrowser.Api.UserLibrary var collectionFolders = _libraryManager.GetCollectionFolders(item); foreach (var collectionFolder in collectionFolders) { - if (user.Policy.EnabledFolders.Contains(collectionFolder.Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) + if (user.Policy.EnabledFolders.Contains( + collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture), + StringComparer.OrdinalIgnoreCase)) { isInEnabledFolder = true; } @@ -458,9 +455,7 @@ namespace MediaBrowser.Api.UserLibrary IncludeItemTypes = new[] { typeof(MusicAlbum).Name }, Name = i, Limit = 1 - - }).Select(albumId => albumId); - + }); }).ToArray(); } diff --git a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs index a9b06095d..45694a678 100644 --- a/MediaBrowser.Api/UserLibrary/UserLibraryService.cs +++ b/MediaBrowser.Api/UserLibrary/UserLibraryService.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -366,11 +367,21 @@ namespace MediaBrowser.Api.UserLibrary var dtoOptions = GetDtoOptions(_authContext, request); - var dtos = item.GetExtras(new[] { ExtraType.Trailer }) + var dtosExtras = item.GetExtras(new[] { ExtraType.Trailer }) .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)) .ToArray(); - return ToOptimizedResult(dtos); + if (item is IHasTrailers hasTrailers) + { + var trailers = hasTrailers.GetTrailers(); + var dtosTrailers = _dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item); + var allTrailers = new BaseItemDto[dtosExtras.Length + dtosTrailers.Count]; + dtosExtras.CopyTo(allTrailers, 0); + dtosTrailers.CopyTo(allTrailers, dtosExtras.Length); + return ToOptimizedResult(allTrailers); + } + + return ToOptimizedResult(dtosExtras); } /// <summary> diff --git a/MediaBrowser.Api/UserLibrary/UserViewsService.cs b/MediaBrowser.Api/UserLibrary/UserViewsService.cs index 1d61c5c1e..2fa5d8933 100644 --- a/MediaBrowser.Api/UserLibrary/UserViewsService.cs +++ b/MediaBrowser.Api/UserLibrary/UserViewsService.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Linq; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -116,7 +117,7 @@ namespace MediaBrowser.Api.UserLibrary .Select(i => new SpecialViewOption { Name = i.Name, - Id = i.Id.ToString("N") + Id = i.Id.ToString("N", CultureInfo.InvariantCulture) }) .OrderBy(i => i.Name) diff --git a/MediaBrowser.Api/UserService.cs b/MediaBrowser.Api/UserService.cs index fa70a52aa..2c0a0b443 100644 --- a/MediaBrowser.Api/UserService.cs +++ b/MediaBrowser.Api/UserService.cs @@ -13,6 +13,7 @@ using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Services; using MediaBrowser.Model.Users; +using Microsoft.Extensions.Logging; namespace MediaBrowser.Api { @@ -248,7 +249,13 @@ namespace MediaBrowser.Api private readonly IDeviceManager _deviceManager; private readonly IAuthorizationContext _authContext; - public UserService(IUserManager userManager, ISessionManager sessionMananger, IServerConfigurationManager config, INetworkManager networkManager, IDeviceManager deviceManager, IAuthorizationContext authContext) + public UserService( + IUserManager userManager, + ISessionManager sessionMananger, + IServerConfigurationManager config, + INetworkManager networkManager, + IDeviceManager deviceManager, + IAuthorizationContext authContext) { _userManager = userManager; _sessionMananger = sessionMananger; @@ -365,8 +372,8 @@ namespace MediaBrowser.Api } _sessionMananger.RevokeUserTokens(user.Id, null); - - return _userManager.DeleteUser(user); + _userManager.DeleteUser(user); + return Task.CompletedTask; } /// <summary> @@ -399,19 +406,27 @@ namespace MediaBrowser.Api { var auth = _authContext.GetAuthorizationInfo(Request); - var result = await _sessionMananger.AuthenticateNewSession(new AuthenticationRequest + try { - App = auth.Client, - AppVersion = auth.Version, - DeviceId = auth.DeviceId, - DeviceName = auth.Device, - Password = request.Pw, - PasswordSha1 = request.Password, - RemoteEndPoint = Request.RemoteIp, - Username = request.Username - }).ConfigureAwait(false); - - return ToOptimizedResult(result); + var result = await _sessionMananger.AuthenticateNewSession(new AuthenticationRequest + { + App = auth.Client, + AppVersion = auth.Version, + DeviceId = auth.DeviceId, + DeviceName = auth.Device, + Password = request.Pw, + PasswordSha1 = request.Password, + RemoteEndPoint = Request.RemoteIp, + Username = request.Username + }).ConfigureAwait(false); + + return ToOptimizedResult(result); + } + catch (SecurityException e) + { + // rethrow adding IP address to message + throw new SecurityException($"[{Request.RemoteIp}] {e.Message}"); + } } /// <summary> @@ -503,9 +518,14 @@ namespace MediaBrowser.Api } } + /// <summary> + /// Posts the specified request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>System.Object.</returns> public async Task<object> Post(CreateUserByName request) { - var newUser = await _userManager.CreateUser(request.Name).ConfigureAwait(false); + var newUser = _userManager.CreateUser(request.Name); // no need to authenticate password for new user if (request.Password != null) diff --git a/MediaBrowser.Api/VideosService.cs b/MediaBrowser.Api/VideosService.cs index 061f72438..474036f5c 100644 --- a/MediaBrowser.Api/VideosService.cs +++ b/MediaBrowser.Api/VideosService.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Linq; using System.Threading; using MediaBrowser.Controller.Configuration; @@ -168,7 +169,7 @@ namespace MediaBrowser.Api foreach (var item in items.Where(i => i.Id != primaryVersion.Id)) { - item.SetPrimaryVersionId(primaryVersion.Id.ToString("N")); + item.SetPrimaryVersionId(primaryVersion.Id.ToString("N", CultureInfo.InvariantCulture)); item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); diff --git a/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs b/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs index 0b59627cc..344aecf53 100644 --- a/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs +++ b/MediaBrowser.Common/Configuration/ConfigurationUpdateEventArgs.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Common.Configuration @@ -9,6 +11,7 @@ namespace MediaBrowser.Common.Configuration /// </summary> /// <value>The key.</value> public string Key { get; set; } + /// <summary> /// Gets or sets the new configuration. /// </summary> diff --git a/MediaBrowser.Common/Configuration/IApplicationPaths.cs b/MediaBrowser.Common/Configuration/IApplicationPaths.cs index fd11bf904..5bdea7d8b 100644 --- a/MediaBrowser.Common/Configuration/IApplicationPaths.cs +++ b/MediaBrowser.Common/Configuration/IApplicationPaths.cs @@ -77,7 +77,10 @@ namespace MediaBrowser.Common.Configuration /// <value>The temp directory.</value> string TempDirectory { get; } + /// <summary> + /// Gets the magic string used for virtual path manipulation. + /// </summary> + /// <value>The magic string used for virtual path manipulation.</value> string VirtualDataPath { get; } } - } diff --git a/MediaBrowser.Common/Configuration/IConfigurationFactory.cs b/MediaBrowser.Common/Configuration/IConfigurationFactory.cs index 0fb2b83d1..4c4060096 100644 --- a/MediaBrowser.Common/Configuration/IConfigurationFactory.cs +++ b/MediaBrowser.Common/Configuration/IConfigurationFactory.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; diff --git a/MediaBrowser.Common/Configuration/IConfigurationManager.cs b/MediaBrowser.Common/Configuration/IConfigurationManager.cs index 8fed2dcdf..caf2edd83 100644 --- a/MediaBrowser.Common/Configuration/IConfigurationManager.cs +++ b/MediaBrowser.Common/Configuration/IConfigurationManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using MediaBrowser.Model.Configuration; diff --git a/MediaBrowser.Common/Cryptography/Constants.cs b/MediaBrowser.Common/Cryptography/Constants.cs new file mode 100644 index 000000000..354114232 --- /dev/null +++ b/MediaBrowser.Common/Cryptography/Constants.cs @@ -0,0 +1,18 @@ +namespace MediaBrowser.Common.Cryptography +{ + /// <summary> + /// Class containing global constants for Jellyfin Cryptography. + /// </summary> + public static class Constants + { + /// <summary> + /// The default length for new salts. + /// </summary> + public const int DefaultSaltLength = 64; + + /// <summary> + /// The default amount of iterations for hashing passwords. + /// </summary> + public const int DefaultIterations = 1000; + } +} diff --git a/MediaBrowser.Common/Cryptography/Extensions.cs b/MediaBrowser.Common/Cryptography/Extensions.cs new file mode 100644 index 000000000..1e32a6d1a --- /dev/null +++ b/MediaBrowser.Common/Cryptography/Extensions.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using MediaBrowser.Model.Cryptography; +using static MediaBrowser.Common.Cryptography.Constants; + +namespace MediaBrowser.Common.Cryptography +{ + /// <summary> + /// Class containing extension methods for working with Jellyfin cryptography objects. + /// </summary> + public static class Extensions + { + /// <summary> + /// Creates a new <see cref="PasswordHash" /> instance. + /// </summary> + /// <param name="cryptoProvider">The <see cref="ICryptoProvider" /> instance used.</param> + /// <param name="password">The password that will be hashed.</param> + /// <returns>A <see cref="PasswordHash" /> instance with the hash method, hash, salt and number of iterations.</returns> + public static PasswordHash CreatePasswordHash(this ICryptoProvider cryptoProvider, string password) + { + byte[] salt = cryptoProvider.GenerateSalt(); + return new PasswordHash( + cryptoProvider.DefaultHashMethod, + cryptoProvider.ComputeHashWithDefaultMethod( + Encoding.UTF8.GetBytes(password), + salt), + salt, + new Dictionary<string, string> + { + { "iterations", DefaultIterations.ToString(CultureInfo.InvariantCulture) } + }); + } + } +} diff --git a/MediaBrowser.Common/Cryptography/PasswordHash.cs b/MediaBrowser.Common/Cryptography/PasswordHash.cs new file mode 100644 index 000000000..7741571db --- /dev/null +++ b/MediaBrowser.Common/Cryptography/PasswordHash.cs @@ -0,0 +1,157 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using static MediaBrowser.Common.HexHelper; + +namespace MediaBrowser.Common.Cryptography +{ + // Defined from this hash storage spec + // https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md + // $<id>[$<param>=<value>(,<param>=<value>)*][$<salt>[$<hash>]] + // with one slight amendment to ease the transition, we're writing out the bytes in hex + // rather than making them a BASE64 string with stripped padding + public class PasswordHash + { + private readonly Dictionary<string, string> _parameters; + + public PasswordHash(string id, byte[] hash) + : this(id, hash, Array.Empty<byte>()) + { + + } + + public PasswordHash(string id, byte[] hash, byte[] salt) + : this(id, hash, salt, new Dictionary<string, string>()) + { + + } + + public PasswordHash(string id, byte[] hash, byte[] salt, Dictionary<string, string> parameters) + { + Id = id; + Hash = hash; + Salt = salt; + _parameters = parameters; + } + + /// <summary> + /// Gets the symbolic name for the function used. + /// </summary> + /// <value>Returns the symbolic name for the function used.</value> + public string Id { get; } + + /// <summary> + /// Gets the additional parameters used by the hash function. + /// </summary> + /// <value></value> + public IReadOnlyDictionary<string, string> Parameters => _parameters; + + /// <summary> + /// Gets the salt used for hashing the password. + /// </summary> + /// <value>Returns the salt used for hashing the password.</value> + public byte[] Salt { get; } + + /// <summary> + /// Gets the hashed password. + /// </summary> + /// <value>Return the hashed password.</value> + public byte[] Hash { get; } + + public static PasswordHash Parse(string storageString) + { + string[] splitted = storageString.Split('$'); + // The string should at least contain the hash function and the hash itself + if (splitted.Length < 3) + { + throw new ArgumentException("String doesn't contain enough segments", nameof(storageString)); + } + + // Start at 1, the first index shouldn't contain any data + int index = 1; + + // Name of the hash function + string id = splitted[index++]; + + // Optional parameters + Dictionary<string, string> parameters = new Dictionary<string, string>(); + if (splitted[index].IndexOf('=') != -1) + { + foreach (string paramset in splitted[index++].Split(',')) + { + if (string.IsNullOrEmpty(paramset)) + { + continue; + } + + string[] fields = paramset.Split('='); + if (fields.Length != 2) + { + throw new InvalidDataException($"Malformed parameter in password hash string {paramset}"); + } + + parameters.Add(fields[0], fields[1]); + } + } + + byte[] hash; + byte[] salt; + // Check if the string also contains a salt + if (splitted.Length - index == 2) + { + salt = FromHexString(splitted[index++]); + hash = FromHexString(splitted[index++]); + } + else + { + salt = Array.Empty<byte>(); + hash = FromHexString(splitted[index++]); + } + + return new PasswordHash(id, hash, salt, parameters); + } + + private void SerializeParameters(StringBuilder stringBuilder) + { + if (_parameters.Count == 0) + { + return; + } + + stringBuilder.Append('$'); + foreach (var pair in _parameters) + { + stringBuilder.Append(pair.Key); + stringBuilder.Append('='); + stringBuilder.Append(pair.Value); + stringBuilder.Append(','); + } + + // Remove last ',' + stringBuilder.Length -= 1; + } + + /// <inheritdoc /> + public override string ToString() + { + var str = new StringBuilder(); + str.Append('$'); + str.Append(Id); + SerializeParameters(str); + + if (Salt.Length != 0) + { + str.Append('$'); + str.Append(ToHexString(Salt)); + } + + str.Append('$'); + str.Append(ToHexString(Hash)); + + return str.ToString(); + } + } +} diff --git a/MediaBrowser.Common/Events/EventHelper.cs b/MediaBrowser.Common/Events/EventHelper.cs index 0ac7905a1..b67315df6 100644 --- a/MediaBrowser.Common/Events/EventHelper.cs +++ b/MediaBrowser.Common/Events/EventHelper.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Threading.Tasks; using Microsoft.Extensions.Logging; diff --git a/MediaBrowser.Common/Extensions/BaseExtensions.cs b/MediaBrowser.Common/Extensions/BaseExtensions.cs index 40c16b957..33473c2be 100644 --- a/MediaBrowser.Common/Extensions/BaseExtensions.cs +++ b/MediaBrowser.Common/Extensions/BaseExtensions.cs @@ -14,20 +14,20 @@ namespace MediaBrowser.Common.Extensions /// Strips the HTML. /// </summary> /// <param name="htmlString">The HTML string.</param> - /// <returns>System.String.</returns> + /// <returns><see cref="string" />.</returns> public static string StripHtml(this string htmlString) { // http://stackoverflow.com/questions/1349023/how-can-i-strip-html-from-text-in-net - const string pattern = @"<(.|\n)*?>"; + const string Pattern = @"<(.|\n)*?>"; - return Regex.Replace(htmlString, pattern, string.Empty).Trim(); + return Regex.Replace(htmlString, Pattern, string.Empty).Trim(); } /// <summary> - /// Gets the M d5. + /// Gets the Md5. /// </summary> - /// <param name="str">The STR.</param> - /// <returns>Guid.</returns> + /// <param name="str">The string.</param> + /// <returns><see cref="Guid" />.</returns> public static Guid GetMD5(this string str) { using (var provider = MD5.Create()) diff --git a/MediaBrowser.Common/Extensions/CollectionExtensions.cs b/MediaBrowser.Common/Extensions/CollectionExtensions.cs index f7c0e3cf0..215224398 100644 --- a/MediaBrowser.Common/Extensions/CollectionExtensions.cs +++ b/MediaBrowser.Common/Extensions/CollectionExtensions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; namespace MediaBrowser.Common.Extensions @@ -5,10 +7,42 @@ namespace MediaBrowser.Common.Extensions // The MS CollectionExtensions are only available in netcoreapp public static class CollectionExtensions { - public static TValue GetValueOrDefault<TKey, TValue> (this IReadOnlyDictionary<TKey, TValue> dictionary, TKey key) + public static TValue GetValueOrDefault<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, TKey key) { dictionary.TryGetValue(key, out var ret); return ret; } + + /// <summary> + /// Copies all the elements of the current collection to the specified list + /// starting at the specified destination array index. The index is specified as a 32-bit integer. + /// </summary> + /// <param name="source">The current collection that is the source of the elements.</param> + /// <param name="destination">The list that is the destination of the elements copied from the current collection.</param> + /// <param name="index">A 32-bit integer that represents the index in <c>destination</c> at which copying begins.</param> + /// <typeparam name="T"></typeparam> + public static void CopyTo<T>(this IReadOnlyList<T> source, IList<T> destination, int index = 0) + { + for (int i = 0; i < source.Count; i++) + { + destination[index + i] = source[i]; + } + } + + /// <summary> + /// Copies all the elements of the current collection to the specified list + /// starting at the specified destination array index. The index is specified as a 32-bit integer. + /// </summary> + /// <param name="source">The current collection that is the source of the elements.</param> + /// <param name="destination">The list that is the destination of the elements copied from the current collection.</param> + /// <param name="index">A 32-bit integer that represents the index in <c>destination</c> at which copying begins.</param> + /// <typeparam name="T"></typeparam> + public static void CopyTo<T>(this IReadOnlyCollection<T> source, IList<T> destination, int index = 0) + { + foreach (T item in source) + { + destination[index++] = item; + } + } } } diff --git a/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs b/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs index 9f70ae7d8..9b064a40d 100644 --- a/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs +++ b/MediaBrowser.Common/Extensions/ResourceNotFoundException.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Common.Extensions diff --git a/MediaBrowser.Common/HexHelper.cs b/MediaBrowser.Common/HexHelper.cs new file mode 100644 index 000000000..61007b5b2 --- /dev/null +++ b/MediaBrowser.Common/HexHelper.cs @@ -0,0 +1,24 @@ +#pragma warning disable CS1591 + +using System; +using System.Globalization; + +namespace MediaBrowser.Common +{ + public static class HexHelper + { + public static byte[] FromHexString(string str) + { + byte[] bytes = new byte[str.Length / 2]; + for (int i = 0; i < str.Length; i += 2) + { + bytes[i / 2] = byte.Parse(str.Substring(i, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture); + } + + return bytes; + } + + public static string ToHexString(byte[] bytes) + => BitConverter.ToString(bytes).Replace("-", ""); + } +} diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index cb7343440..c8da100f6 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using MediaBrowser.Common.Plugins; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Updates; using Microsoft.Extensions.DependencyInjection; @@ -31,6 +30,10 @@ namespace MediaBrowser.Common /// <value><c>true</c> if this instance has pending kernel reload; otherwise, <c>false</c>.</value> bool HasPendingRestart { get; } + /// <summary> + /// Gets or sets a value indicating whether this instance is currently shutting down. + /// </summary> + /// <value><c>true</c> if this instance is shutting down; otherwise, <c>false</c>.</value> bool IsShuttingDown { get; } /// <summary> @@ -40,6 +43,12 @@ namespace MediaBrowser.Common bool CanSelfRestart { get; } /// <summary> + /// Get the version class of the system. + /// </summary> + /// <value><see cref="PackageVersionClass.Release" /> or <see cref="PackageVersionClass.Beta" />.</value> + PackageVersionClass SystemUpdateLevel { get; } + + /// <summary> /// Occurs when [has pending restart changed]. /// </summary> event EventHandler HasPendingRestartChanged; @@ -75,10 +84,10 @@ namespace MediaBrowser.Common /// <summary> /// Gets the exports. /// </summary> - /// <typeparam name="T"></typeparam> - /// <param name="manageLiftime">if set to <c>true</c> [manage liftime].</param> - /// <returns>IEnumerable{``0}.</returns> - IEnumerable<T> GetExports<T>(bool manageLifetime = true); + /// <typeparam name="T">The type.</typeparam> + /// <param name="manageLifetime">If set to <c>true</c> [manage lifetime].</param> + /// <returns><see cref="IReadOnlyCollection{T}" />.</returns> + IReadOnlyCollection<T> GetExports<T>(bool manageLifetime = true); /// <summary> /// Resolves this instance. @@ -115,7 +124,5 @@ namespace MediaBrowser.Common /// <param name="type">The type.</param> /// <returns>System.Object.</returns> object CreateInstance(Type type); - - PackageVersionClass SystemUpdateLevel { get; } } } diff --git a/MediaBrowser.Common/Json/Converters/GuidConverter.cs b/MediaBrowser.Common/Json/Converters/GuidConverter.cs new file mode 100644 index 000000000..3081e12ee --- /dev/null +++ b/MediaBrowser.Common/Json/Converters/GuidConverter.cs @@ -0,0 +1,20 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MediaBrowser.Common.Json.Converters +{ + /// <summary> + /// Converts a GUID object or value to/from JSON. + /// </summary> + public class GuidConverter : JsonConverter<Guid> + { + /// <inheritdoc /> + public override Guid Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => new Guid(reader.GetString()); + + /// <inheritdoc /> + public override void Write(Utf8JsonWriter writer, Guid value, JsonSerializerOptions options) + => writer.WriteStringValue(value); + } +} diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs new file mode 100644 index 000000000..4ba0d5a1a --- /dev/null +++ b/MediaBrowser.Common/Json/JsonDefaults.cs @@ -0,0 +1,30 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using MediaBrowser.Common.Json.Converters; + +namespace MediaBrowser.Common.Json +{ + /// <summary> + /// Helper class for having compatible JSON throughout the codebase. + /// </summary> + public static class JsonDefaults + { + /// <summary> + /// Gets the default <see cref="JsonSerializerOptions" /> options. + /// </summary> + /// <returns>The default <see cref="JsonSerializerOptions" /> options.</returns> + public static JsonSerializerOptions GetOptions() + { + var options = new JsonSerializerOptions() + { + ReadCommentHandling = JsonCommentHandling.Disallow, + WriteIndented = false + }; + + options.Converters.Add(new GuidConverter()); + options.Converters.Add(new JsonStringEnumConverter()); + + return options; + } + } +} diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 05b48a2a1..cf3f6c2a4 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -23,6 +23,19 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> + <PropertyGroup> + <!-- We need at least C# 7.1 for the "default literal" feature--> + <LangVersion>latest</LangVersion> + </PropertyGroup> + + <ItemGroup> + <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo"> + <_Parameter1>Jellyfin.Common.Tests</_Parameter1> + </AssemblyAttribute> + </ItemGroup> + </Project> diff --git a/MediaBrowser.Common/Net/CustomHeaderNames.cs b/MediaBrowser.Common/Net/CustomHeaderNames.cs index ff148dc80..5ca9897eb 100644 --- a/MediaBrowser.Common/Net/CustomHeaderNames.cs +++ b/MediaBrowser.Common/Net/CustomHeaderNames.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + namespace MediaBrowser.Common.Net { public static class CustomHeaderNames @@ -8,4 +10,4 @@ namespace MediaBrowser.Common.Net public const string XForwardedProto = "X-Forwarded-Proto"; public const string XRealIP = "X-Real-IP"; } -}
\ No newline at end of file +} diff --git a/MediaBrowser.Common/Net/HttpRequestOptions.cs b/MediaBrowser.Common/Net/HttpRequestOptions.cs index 76bd35e57..18c4b181f 100644 --- a/MediaBrowser.Common/Net/HttpRequestOptions.cs +++ b/MediaBrowser.Common/Net/HttpRequestOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading; @@ -64,12 +66,6 @@ namespace MediaBrowser.Common.Net set => RequestHeaders[HeaderNames.Host] = value; } - /// <summary> - /// Gets or sets the progress. - /// </summary> - /// <value>The progress.</value> - public IProgress<double> Progress { get; set; } - public Dictionary<string, string> RequestHeaders { get; private set; } public string RequestContentType { get; set; } @@ -79,10 +75,6 @@ namespace MediaBrowser.Common.Net public bool BufferContent { get; set; } - public bool LogRequest { get; set; } - public bool LogRequestAsDebug { get; set; } - public bool LogErrors { get; set; } - public bool LogErrorResponseBody { get; set; } public bool EnableKeepAlive { get; set; } @@ -105,8 +97,6 @@ namespace MediaBrowser.Common.Net { RequestHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - LogRequest = true; - LogErrors = true; CacheMode = CacheMode.None; DecompressionMethod = CompressionMethod.Deflate; } diff --git a/MediaBrowser.Common/Net/HttpResponseInfo.cs b/MediaBrowser.Common/Net/HttpResponseInfo.cs index d65ce897a..0de034b0e 100644 --- a/MediaBrowser.Common/Net/HttpResponseInfo.cs +++ b/MediaBrowser.Common/Net/HttpResponseInfo.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.IO; using System.Net; @@ -69,6 +71,7 @@ namespace MediaBrowser.Common.Net ContentHeaders = contentHeader; } + /// <inheritdoc /> public void Dispose() { // Only IDisposable for backwards compatibility diff --git a/MediaBrowser.Common/Net/IHttpClient.cs b/MediaBrowser.Common/Net/IHttpClient.cs index db69c6f2c..23ba34173 100644 --- a/MediaBrowser.Common/Net/IHttpClient.cs +++ b/MediaBrowser.Common/Net/IHttpClient.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using System.Threading.Tasks; using System.Net.Http; @@ -25,12 +26,13 @@ namespace MediaBrowser.Common.Net /// <summary> /// Warning: Deprecated function, - /// use 'Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, HttpMethod httpMethod);' instead + /// use 'Task{HttpResponseInfo} SendAsync(HttpRequestOptions options, HttpMethod httpMethod);' instead /// Sends the asynchronous. /// </summary> /// <param name="options">The options.</param> /// <param name="httpMethod">The HTTP method.</param> /// <returns>Task{HttpResponseInfo}.</returns> + [Obsolete("Use 'Task{HttpResponseInfo} SendAsync(HttpRequestOptions options, HttpMethod httpMethod);' instead")] Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string httpMethod); /// <summary> @@ -47,21 +49,5 @@ namespace MediaBrowser.Common.Net /// <param name="options">The options.</param> /// <returns>Task{HttpResponseInfo}.</returns> Task<HttpResponseInfo> Post(HttpRequestOptions options); - - /// <summary> - /// Downloads the contents of a given url into a temporary location - /// </summary> - /// <param name="options">The options.</param> - /// <returns>Task{System.String}.</returns> - /// <exception cref="System.ArgumentNullException">progress</exception> - /// <exception cref="Model.Net.HttpException"></exception> - Task<string> GetTempFile(HttpRequestOptions options); - - /// <summary> - /// Gets the temporary file response. - /// </summary> - /// <param name="options">The options.</param> - /// <returns>Task{HttpResponseInfo}.</returns> - Task<HttpResponseInfo> GetTempFileResponse(HttpRequestOptions options); } } diff --git a/MediaBrowser.Common/Net/INetworkManager.cs b/MediaBrowser.Common/Net/INetworkManager.cs index 61f2bc2f9..97504a471 100644 --- a/MediaBrowser.Common/Net/INetworkManager.cs +++ b/MediaBrowser.Common/Net/INetworkManager.cs @@ -1,7 +1,9 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Net; -using System.Threading.Tasks; +using System.Net.NetworkInformation; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; @@ -25,7 +27,7 @@ namespace MediaBrowser.Common.Net /// Returns MAC Address from first Network Card in Computer /// </summary> /// <returns>[string] MAC Address</returns> - List<string> GetMacAddresses(); + List<PhysicalAddress> GetMacAddresses(); /// <summary> /// Determines whether [is in private address space] [the specified endpoint]. diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs index 1ff2e98ef..6ef891d66 100644 --- a/MediaBrowser.Common/Plugins/BasePlugin.cs +++ b/MediaBrowser.Common/Plugins/BasePlugin.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.IO; using MediaBrowser.Common.Configuration; diff --git a/MediaBrowser.Common/Plugins/IPlugin.cs b/MediaBrowser.Common/Plugins/IPlugin.cs index 32527c299..7bd90c964 100644 --- a/MediaBrowser.Common/Plugins/IPlugin.cs +++ b/MediaBrowser.Common/Plugins/IPlugin.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Model.Plugins; diff --git a/MediaBrowser.Common/Progress/ActionableProgress.cs b/MediaBrowser.Common/Progress/ActionableProgress.cs index 9fe01931f..af69055aa 100644 --- a/MediaBrowser.Common/Progress/ActionableProgress.cs +++ b/MediaBrowser.Common/Progress/ActionableProgress.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Common.Progress @@ -25,16 +27,9 @@ namespace MediaBrowser.Common.Progress public void Report(T value) { - if (ProgressChanged != null) - { - ProgressChanged(this, value); - } + ProgressChanged?.Invoke(this, value); - var action = _action; - if (action != null) - { - action(value); - } + _action?.Invoke(value); } } @@ -44,10 +39,7 @@ namespace MediaBrowser.Common.Progress public void Report(T value) { - if (ProgressChanged != null) - { - ProgressChanged(this, value); - } + ProgressChanged?.Invoke(this, value); } } } diff --git a/MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs b/MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs index 09d974db6..0445397ad 100644 --- a/MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs +++ b/MediaBrowser.Common/Providers/SubtitleConfigurationFactory.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Providers; @@ -6,6 +8,7 @@ namespace MediaBrowser.Common.Providers { public class SubtitleConfigurationFactory : IConfigurationFactory { + /// <inheritdoc /> public IEnumerable<ConfigurationStore> GetConfigurations() { yield return new ConfigurationStore() diff --git a/MediaBrowser.Common/System/OperatingSystem.cs b/MediaBrowser.Common/System/OperatingSystem.cs index 640821d4d..7d38ddb6e 100644 --- a/MediaBrowser.Common/System/OperatingSystem.cs +++ b/MediaBrowser.Common/System/OperatingSystem.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Runtime.InteropServices; using System.Threading; diff --git a/MediaBrowser.Common/Updates/IInstallationManager.cs b/MediaBrowser.Common/Updates/IInstallationManager.cs index a263be35f..b3367f71d 100644 --- a/MediaBrowser.Common/Updates/IInstallationManager.cs +++ b/MediaBrowser.Common/Updates/IInstallationManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading; @@ -16,11 +18,6 @@ namespace MediaBrowser.Common.Updates event EventHandler<InstallationEventArgs> PackageInstallationCancelled; /// <summary> - /// The current installations - /// </summary> - List<Tuple<InstallationInfo, CancellationTokenSource>> CurrentInstallations { get; set; } - - /// <summary> /// The completed installations /// </summary> IEnumerable<InstallationInfo> CompletedInstallations { get; } @@ -33,7 +30,7 @@ namespace MediaBrowser.Common.Updates /// <summary> /// Occurs when [plugin updated]. /// </summary> - event EventHandler<GenericEventArgs<Tuple<IPlugin, PackageVersionInfo>>> PluginUpdated; + event EventHandler<GenericEventArgs<(IPlugin, PackageVersionInfo)>> PluginUpdated; /// <summary> /// Occurs when [plugin updated]. @@ -102,12 +99,9 @@ namespace MediaBrowser.Common.Updates /// Installs the package. /// </summary> /// <param name="package">The package.</param> - /// <param name="isPlugin">if set to <c>true</c> [is plugin].</param> - /// <param name="progress">The progress.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException">package</exception> - Task InstallPackage(PackageVersionInfo package, bool isPlugin, IProgress<double> progress, CancellationToken cancellationToken); + /// <returns><see cref="Task" />.</returns> + Task InstallPackage(PackageVersionInfo package, CancellationToken cancellationToken = default); /// <summary> /// Uninstalls a plugin @@ -115,5 +109,12 @@ namespace MediaBrowser.Common.Updates /// <param name="plugin">The plugin.</param> /// <exception cref="ArgumentException"></exception> void UninstallPlugin(IPlugin plugin); + + /// <summary> + /// Cancels the installation + /// </summary> + /// <param name="id">The id of the package that is being installed</param> + /// <returns>Returns true if the install was cancelled</returns> + bool CancelInstallation(Guid id); } } diff --git a/MediaBrowser.Common/Updates/InstallationEventArgs.cs b/MediaBrowser.Common/Updates/InstallationEventArgs.cs index 9f215e889..36e124ddf 100644 --- a/MediaBrowser.Common/Updates/InstallationEventArgs.cs +++ b/MediaBrowser.Common/Updates/InstallationEventArgs.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Model.Updates; namespace MediaBrowser.Common.Updates diff --git a/MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs b/MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs index 43adfb02d..46f10c84f 100644 --- a/MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs +++ b/MediaBrowser.Common/Updates/InstallationFailedEventArgs.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace MediaBrowser.Common.Updates diff --git a/MediaBrowser.Controller/Authentication/AuthenticationException.cs b/MediaBrowser.Controller/Authentication/AuthenticationException.cs new file mode 100644 index 000000000..62eca3ea9 --- /dev/null +++ b/MediaBrowser.Controller/Authentication/AuthenticationException.cs @@ -0,0 +1,29 @@ +using System; + +namespace MediaBrowser.Controller.Authentication +{ + /// <summary> + /// The exception that is thrown when an attempt to authenticate fails. + /// </summary> + public class AuthenticationException : Exception + { + /// <inheritdoc /> + public AuthenticationException() : base() + { + + } + + /// <inheritdoc /> + public AuthenticationException(string message) : base(message) + { + + } + + /// <inheritdoc /> + public AuthenticationException(string message, Exception innerException) + : base(message, innerException) + { + + } + } +} diff --git a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs index 2cf531eed..f5571065f 100644 --- a/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs +++ b/MediaBrowser.Controller/Authentication/IAuthenticationProvider.cs @@ -9,10 +9,9 @@ namespace MediaBrowser.Controller.Authentication string Name { get; } bool IsEnabled { get; } Task<ProviderAuthenticationResult> Authenticate(string username, string password); - Task<bool> HasPassword(User user); + bool HasPassword(User user); Task ChangePassword(User user, string newPassword); void ChangeEasyPassword(User user, string newPassword, string newPasswordHash); - string GetPasswordHash(User user); string GetEasyPasswordHash(User user); } diff --git a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs index 9e5cd8816..2639960e7 100644 --- a/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs +++ b/MediaBrowser.Controller/Authentication/IPasswordResetProvider.cs @@ -12,6 +12,7 @@ namespace MediaBrowser.Controller.Authentication Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork); Task<PinRedeemResult> RedeemPasswordResetPin(string pin); } + public class PasswordPinCreationResult { public string PinFile { get; set; } diff --git a/MediaBrowser.Controller/Channels/Channel.cs b/MediaBrowser.Controller/Channels/Channel.cs index adf03fb66..cdf2ca69e 100644 --- a/MediaBrowser.Controller/Channels/Channel.cs +++ b/MediaBrowser.Controller/Channels/Channel.cs @@ -1,10 +1,11 @@ using System; +using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Channels { @@ -14,14 +15,14 @@ namespace MediaBrowser.Controller.Channels { if (user.Policy.BlockedChannels != null) { - if (user.Policy.BlockedChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) + if (user.Policy.BlockedChannels.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase)) { return false; } } else { - if (!user.Policy.EnableAllChannels && !user.Policy.EnabledChannels.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) + if (!user.Policy.EnableAllChannels && !user.Policy.EnabledChannels.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase)) { return false; } @@ -30,10 +31,10 @@ namespace MediaBrowser.Controller.Channels return base.IsVisible(user); } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => false; - [IgnoreDataMember] + [JsonIgnore] public override SourceType SourceType => SourceType.Channel; protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query) @@ -60,7 +61,7 @@ namespace MediaBrowser.Controller.Channels public static string GetInternalMetadataPath(string basePath, Guid id) { - return System.IO.Path.Combine(basePath, "channels", id.ToString("N"), "metadata"); + return System.IO.Path.Combine(basePath, "channels", id.ToString("N", CultureInfo.InvariantCulture), "metadata"); } public override bool CanDelete() diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs index 4eaecd0a0..a0f9ae46e 100644 --- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs +++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs @@ -18,16 +18,6 @@ namespace MediaBrowser.Controller.Drawing IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; } /// <summary> - /// Encodes the image. - /// </summary> - string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat); - - /// <summary> - /// Creates the image collage. - /// </summary> - /// <param name="options">The options.</param> - void CreateImageCollage(ImageCollageOptions options); - /// <summary> /// Gets the name. /// </summary> /// <value>The name.</value> @@ -46,5 +36,16 @@ namespace MediaBrowser.Controller.Drawing bool SupportsImageEncoding { get; } ImageDimensions GetImageSize(string path); + + /// <summary> + /// Encodes the image. + /// </summary> + string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat); + + /// <summary> + /// Creates the image collage. + /// </summary> + /// <param name="options">The options.</param> + void CreateImageCollage(ImageCollageOptions options); } } diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index a11e2186f..a58a11bd1 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -24,7 +24,15 @@ namespace MediaBrowser.Controller.Drawing /// Gets the image enhancers. /// </summary> /// <value>The image enhancers.</value> - IImageEnhancer[] ImageEnhancers { get; } + IReadOnlyCollection<IImageEnhancer> ImageEnhancers { get; set; } + + /// <summary> + /// Gets a value indicating whether [supports image collage creation]. + /// </summary> + /// <value><c>true</c> if [supports image collage creation]; otherwise, <c>false</c>.</value> + bool SupportsImageCollageCreation { get; } + + IImageEncoder ImageEncoder { get; set; } /// <summary> /// Gets the dimensions of the image. @@ -51,18 +59,12 @@ namespace MediaBrowser.Controller.Drawing ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info, bool updateItem); /// <summary> - /// Adds the parts. - /// </summary> - /// <param name="enhancers">The enhancers.</param> - void AddParts(IEnumerable<IImageEnhancer> enhancers); - - /// <summary> /// Gets the supported enhancers. /// </summary> /// <param name="item">The item.</param> /// <param name="imageType">Type of the image.</param> /// <returns>IEnumerable{IImageEnhancer}.</returns> - IImageEnhancer[] GetSupportedEnhancers(BaseItem item, ImageType imageType); + IEnumerable<IImageEnhancer> GetSupportedEnhancers(BaseItem item, ImageType imageType); /// <summary> /// Gets the image cache tag. @@ -80,7 +82,7 @@ namespace MediaBrowser.Controller.Drawing /// <param name="image">The image.</param> /// <param name="imageEnhancers">The image enhancers.</param> /// <returns>Guid.</returns> - string GetImageCacheTag(BaseItem item, ItemImageInfo image, IImageEnhancer[] imageEnhancers); + string GetImageCacheTag(BaseItem item, ItemImageInfo image, IReadOnlyCollection<IImageEnhancer> imageEnhancers); /// <summary> /// Processes the image. @@ -109,7 +111,7 @@ namespace MediaBrowser.Controller.Drawing /// <summary> /// Gets the supported image output formats. /// </summary> - /// <returns>IReadOnlyCollection{ImageOutput}.</returns> + /// <returns><see cref="IReadOnlyCollection{ImageOutput}" />.</returns> IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats(); /// <summary> @@ -118,14 +120,6 @@ namespace MediaBrowser.Controller.Drawing /// <param name="options">The options.</param> void CreateImageCollage(ImageCollageOptions options); - /// <summary> - /// Gets a value indicating whether [supports image collage creation]. - /// </summary> - /// <value><c>true</c> if [supports image collage creation]; otherwise, <c>false</c>.</value> - bool SupportsImageCollageCreation { get; } - - IImageEncoder ImageEncoder { get; set; } - bool SupportsTransparency(string path); } } diff --git a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs index db432f500..29addf6e6 100644 --- a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs +++ b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using MediaBrowser.Controller.Entities; @@ -33,9 +34,9 @@ namespace MediaBrowser.Controller.Drawing public int Quality { get; set; } - public IImageEnhancer[] Enhancers { get; set; } + public IReadOnlyCollection<IImageEnhancer> Enhancers { get; set; } - public ImageFormat[] SupportedOutputFormats { get; set; } + public IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; set; } public bool AddPlayedIndicator { get; set; } diff --git a/MediaBrowser.Controller/Dto/IDtoService.cs b/MediaBrowser.Controller/Dto/IDtoService.cs index 4b6fd58fe..ba693a065 100644 --- a/MediaBrowser.Controller/Dto/IDtoService.cs +++ b/MediaBrowser.Controller/Dto/IDtoService.cs @@ -57,7 +57,7 @@ namespace MediaBrowser.Controller.Dto /// <param name="options">The options.</param> /// <param name="user">The user.</param> /// <param name="owner">The owner.</param> - BaseItemDto[] GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null); + IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null); /// <summary> /// Gets the item by name dto. diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs index 054df21e5..cacda8140 100644 --- a/MediaBrowser.Controller/Entities/AggregateFolder.cs +++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs @@ -2,13 +2,13 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { @@ -23,7 +23,7 @@ namespace MediaBrowser.Controller.Entities PhysicalLocationsList = Array.Empty<string>(); } - [IgnoreDataMember] + [JsonIgnore] public override bool IsPhysicalRoot => true; public override bool CanDelete() @@ -31,7 +31,7 @@ namespace MediaBrowser.Controller.Entities return false; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => false; /// <summary> @@ -45,7 +45,7 @@ namespace MediaBrowser.Controller.Entities /// <value>The virtual children.</value> public ConcurrentBag<BaseItem> VirtualChildren => _virtualChildren; - [IgnoreDataMember] + [JsonIgnore] public override string[] PhysicalLocations => PhysicalLocationsList; public string[] PhysicalLocationsList { get; set; } diff --git a/MediaBrowser.Controller/Entities/Audio/Audio.cs b/MediaBrowser.Controller/Entities/Audio/Audio.cs index 13a6fe44a..a700d0be4 100644 --- a/MediaBrowser.Controller/Entities/Audio/Audio.cs +++ b/MediaBrowser.Controller/Entities/Audio/Audio.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities.Audio { @@ -19,15 +20,13 @@ namespace MediaBrowser.Controller.Entities.Audio IHasLookupInfo<SongInfo>, IHasMediaSources { - /// <summary> - /// Gets or sets the artist. - /// </summary> - /// <value>The artist.</value> - [IgnoreDataMember] - public string[] Artists { get; set; } + /// <inheritdoc /> + [JsonIgnore] + public IReadOnlyList<string> Artists { get; set; } - [IgnoreDataMember] - public string[] AlbumArtists { get; set; } + /// <inheritdoc /> + [JsonIgnore] + public IReadOnlyList<string> AlbumArtists { get; set; } public Audio() { @@ -40,22 +39,22 @@ namespace MediaBrowser.Controller.Entities.Audio return 1; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsAddingToPlaylist => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => true; - [IgnoreDataMember] + [JsonIgnore] protected override bool SupportsOwnedItems => false; - [IgnoreDataMember] + [JsonIgnore] public override Folder LatestItemsIndexContainer => AlbumEntity; public override bool CanDownload() @@ -63,38 +62,14 @@ namespace MediaBrowser.Controller.Entities.Audio return IsFileProtocol; } - [IgnoreDataMember] - public string[] AllArtists - { - get - { - var list = new string[AlbumArtists.Length + Artists.Length]; - - var index = 0; - foreach (var artist in AlbumArtists) - { - list[index] = artist; - index++; - } - foreach (var artist in Artists) - { - list[index] = artist; - index++; - } - - return list; - - } - } - - [IgnoreDataMember] + [JsonIgnore] public MusicAlbum AlbumEntity => FindParent<MusicAlbum>(); /// <summary> /// Gets the type of the media. /// </summary> /// <value>The type of the media.</value> - [IgnoreDataMember] + [JsonIgnore] public override string MediaType => Model.Entities.MediaType.Audio; /// <summary> @@ -125,7 +100,7 @@ namespace MediaBrowser.Controller.Entities.Audio songKey = Album + "-" + songKey; } - var albumArtist = AlbumArtists.Length == 0 ? null : AlbumArtists[0]; + var albumArtist = AlbumArtists.FirstOrDefault(); if (!string.IsNullOrEmpty(albumArtist)) { songKey = albumArtist + "-" + songKey; diff --git a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs index a269b3486..056f31f78 100644 --- a/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/IHasAlbumArtist.cs @@ -1,14 +1,35 @@ +using System.Collections.Generic; + namespace MediaBrowser.Controller.Entities.Audio { public interface IHasAlbumArtist { - string[] AlbumArtists { get; set; } + IReadOnlyList<string> AlbumArtists { get; set; } } public interface IHasArtist { - string[] AllArtists { get; } + /// <summary> + /// Gets or sets the artists. + /// </summary> + /// <value>The artists.</value> + IReadOnlyList<string> Artists { get; set; } + } + + public static class Extentions + { + public static IEnumerable<string> GetAllArtists<T>(this T item) + where T : IHasArtist, IHasAlbumArtist + { + foreach (var i in item.AlbumArtists) + { + yield return i; + } - string[] Artists { get; set; } + foreach (var i in item.Artists) + { + yield return i; + } + } } } diff --git a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs index 5b2939b75..c216176e7 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicAlbum.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Dto; @@ -8,7 +9,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Users; namespace MediaBrowser.Controller.Entities.Audio @@ -18,8 +18,11 @@ namespace MediaBrowser.Controller.Entities.Audio /// </summary> public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLookupInfo<AlbumInfo>, IMetadataContainer { - public string[] AlbumArtists { get; set; } - public string[] Artists { get; set; } + /// <inheritdoc /> + public IReadOnlyList<string> AlbumArtists { get; set; } + + /// <inheritdoc /> + public IReadOnlyList<string> Artists { get; set; } public MusicAlbum() { @@ -27,13 +30,13 @@ namespace MediaBrowser.Controller.Entities.Audio AlbumArtists = Array.Empty<string>(); } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsAddingToPlaylist => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => true; - [IgnoreDataMember] + [JsonIgnore] public MusicArtist MusicArtist => GetMusicArtist(new DtoOptions(true)); public MusicArtist GetMusicArtist(DtoOptions options) @@ -41,8 +44,7 @@ namespace MediaBrowser.Controller.Entities.Audio var parents = GetParents(); foreach (var parent in parents) { - var artist = parent as MusicArtist; - if (artist != null) + if (parent is MusicArtist artist) { return artist; } @@ -56,46 +58,23 @@ namespace MediaBrowser.Controller.Entities.Audio return null; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsCumulativeRunTimeTicks => true; - [IgnoreDataMember] - public string[] AllArtists - { - get - { - var list = new string[AlbumArtists.Length + Artists.Length]; - - var index = 0; - foreach (var artist in AlbumArtists) - { - list[index] = artist; - index++; - } - foreach (var artist in Artists) - { - list[index] = artist; - index++; - } - - return list; - } - } - - [IgnoreDataMember] - public string AlbumArtist => AlbumArtists.Length == 0 ? null : AlbumArtists[0]; + [JsonIgnore] + public string AlbumArtist => AlbumArtists.FirstOrDefault(); - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => false; /// <summary> /// Gets the tracks. /// </summary> /// <value>The tracks.</value> - [IgnoreDataMember] + [JsonIgnore] public IEnumerable<Audio> Tracks => GetRecursiveChildren(i => i is Audio).Cast<Audio>(); protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) @@ -216,8 +195,7 @@ namespace MediaBrowser.Controller.Entities.Audio private async Task RefreshArtists(MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) { - var all = AllArtists; - foreach (var i in all) + foreach (var i in this.GetAllArtists()) { // This should not be necessary but we're seeing some cases of it if (string.IsNullOrEmpty(i)) diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 2d464bd32..efe0d3cf7 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Users; using Microsoft.Extensions.Logging; @@ -18,25 +18,25 @@ namespace MediaBrowser.Controller.Entities.Audio /// </summary> public class MusicArtist : Folder, IItemByName, IHasMusicGenres, IHasDualAccess, IHasLookupInfo<ArtistInfo> { - [IgnoreDataMember] + [JsonIgnore] public bool IsAccessedByName => ParentId.Equals(Guid.Empty); - [IgnoreDataMember] + [JsonIgnore] public override bool IsFolder => !IsAccessedByName; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsCumulativeRunTimeTicks => true; - [IgnoreDataMember] + [JsonIgnore] public override bool IsDisplayedAsFolder => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsAddingToPlaylist => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => false; public override double GetDefaultPrimaryImageAspectRatio() @@ -60,7 +60,7 @@ namespace MediaBrowser.Controller.Entities.Audio return LibraryManager.GetItemList(query); } - [IgnoreDataMember] + [JsonIgnore] public override IEnumerable<BaseItem> Children { get @@ -117,7 +117,7 @@ namespace MediaBrowser.Controller.Entities.Audio /// If the item is a folder, it returns the folder itself /// </summary> /// <value>The containing folder path.</value> - [IgnoreDataMember] + [JsonIgnore] public override string ContainingFolderPath => Path; /// <summary> @@ -164,7 +164,7 @@ namespace MediaBrowser.Controller.Entities.Audio return info; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => false; public static string GetPath(string name) diff --git a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs index d26aaf2bb..537e9630b 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicGenre.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; using MediaBrowser.Controller.Extensions; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities.Audio @@ -23,13 +23,13 @@ namespace MediaBrowser.Controller.Entities.Audio return GetUserDataKeys()[0]; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsAddingToPlaylist => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsAncestors => false; - [IgnoreDataMember] + [JsonIgnore] public override bool IsDisplayedAsFolder => true; /// <summary> @@ -37,7 +37,7 @@ namespace MediaBrowser.Controller.Entities.Audio /// If the item is a folder, it returns the folder itself /// </summary> /// <value>The containing folder path.</value> - [IgnoreDataMember] + [JsonIgnore] public override string ContainingFolderPath => Path; public override double GetDefaultPrimaryImageAspectRatio() @@ -55,7 +55,7 @@ namespace MediaBrowser.Controller.Entities.Audio return true; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => false; public IList<BaseItem> GetTaggedItems(InternalItemsQuery query) diff --git a/MediaBrowser.Controller/Entities/AudioBook.cs b/MediaBrowser.Controller/Entities/AudioBook.cs index 65c8a5fdd..a13873bf9 100644 --- a/MediaBrowser.Controller/Entities/AudioBook.cs +++ b/MediaBrowser.Controller/Entities/AudioBook.cs @@ -1,23 +1,23 @@ using System; +using System.Text.Json.Serialization; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { public class AudioBook : Audio.Audio, IHasSeries, IHasLookupInfo<SongInfo> { - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPositionTicksResume => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => true; - [IgnoreDataMember] + [JsonIgnore] public string SeriesPresentationUniqueKey { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string SeriesName { get; set; } - [IgnoreDataMember] + [JsonIgnore] public Guid SeriesId { get; set; } public string FindSeriesSortName() diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 10a603e42..599d41bb2 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; @@ -25,7 +26,6 @@ using MediaBrowser.Model.Library; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Users; using Microsoft.Extensions.Logging; @@ -40,7 +40,7 @@ namespace MediaBrowser.Controller.Entities /// The supported image extensions /// </summary> public static readonly string[] SupportedImageExtensions - = new [] { ".png", ".jpg", ".jpeg", ".tbn", ".gif" }; + = new[] { ".png", ".jpg", ".jpeg", ".tbn", ".gif" }; private static readonly List<string> _supportedExtensions = new List<string>(SupportedImageExtensions) { @@ -98,62 +98,62 @@ namespace MediaBrowser.Controller.Entities SampleFolderName }; - [IgnoreDataMember] + [JsonIgnore] public Guid[] ThemeSongIds { get; set; } - [IgnoreDataMember] + [JsonIgnore] public Guid[] ThemeVideoIds { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string PreferredMetadataCountryCode { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string PreferredMetadataLanguage { get; set; } public long? Size { get; set; } public string Container { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string Tagline { get; set; } - [IgnoreDataMember] + [JsonIgnore] public virtual ItemImageInfo[] ImageInfos { get; set; } - [IgnoreDataMember] + [JsonIgnore] public bool IsVirtualItem { get; set; } /// <summary> /// Gets or sets the album. /// </summary> /// <value>The album.</value> - [IgnoreDataMember] + [JsonIgnore] public string Album { get; set; } /// <summary> /// Gets or sets the channel identifier. /// </summary> /// <value>The channel identifier.</value> - [IgnoreDataMember] + [JsonIgnore] public Guid ChannelId { get; set; } - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsAddingToPlaylist => false; - [IgnoreDataMember] + [JsonIgnore] public virtual bool AlwaysScanInternalMetadataPath => false; /// <summary> /// Gets a value indicating whether this instance is in mixed folder. /// </summary> /// <value><c>true</c> if this instance is in mixed folder; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsInMixedFolder { get; set; } - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsPlayedStatus => false; - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsPositionTicksResume => false; - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsRemoteImageDownloading => true; private string _name; @@ -161,7 +161,7 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the name. /// </summary> /// <value>The name.</value> - [IgnoreDataMember] + [JsonIgnore] public virtual string Name { get => _name; @@ -174,35 +174,35 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] public bool IsUnaired => PremiereDate.HasValue && PremiereDate.Value.ToLocalTime().Date >= DateTime.Now.Date; - [IgnoreDataMember] + [JsonIgnore] public int? TotalBitrate { get; set; } - [IgnoreDataMember] + [JsonIgnore] public ExtraType? ExtraType { get; set; } - [IgnoreDataMember] + [JsonIgnore] public bool IsThemeMedia => ExtraType.HasValue && (ExtraType.Value == Model.Entities.ExtraType.ThemeSong || ExtraType.Value == Model.Entities.ExtraType.ThemeVideo); - [IgnoreDataMember] + [JsonIgnore] public string OriginalTitle { get; set; } /// <summary> /// Gets or sets the id. /// </summary> /// <value>The id.</value> - [IgnoreDataMember] + [JsonIgnore] public Guid Id { get; set; } - [IgnoreDataMember] + [JsonIgnore] public Guid OwnerId { get; set; } /// <summary> /// Gets or sets the audio. /// </summary> /// <value>The audio.</value> - [IgnoreDataMember] + [JsonIgnore] public ProgramAudio? Audio { get; set; } /// <summary> @@ -210,7 +210,7 @@ namespace MediaBrowser.Controller.Entities /// Default is based on the type for everything except actual generic folders. /// </summary> /// <value>The display prefs id.</value> - [IgnoreDataMember] + [JsonIgnore] public virtual Guid DisplayPreferencesId { get @@ -224,10 +224,10 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the path. /// </summary> /// <value>The path.</value> - [IgnoreDataMember] + [JsonIgnore] public virtual string Path { get; set; } - [IgnoreDataMember] + [JsonIgnore] public virtual SourceType SourceType { get @@ -245,7 +245,7 @@ namespace MediaBrowser.Controller.Entities /// Returns the folder containing the item. /// If the item is a folder, it returns the folder itself /// </summary> - [IgnoreDataMember] + [JsonIgnore] public virtual string ContainingFolderPath { get @@ -263,26 +263,26 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the name of the service. /// </summary> /// <value>The name of the service.</value> - [IgnoreDataMember] + [JsonIgnore] public string ServiceName { get; set; } /// <summary> /// If this content came from an external service, the id of the content on that service /// </summary> - [IgnoreDataMember] + [JsonIgnore] public string ExternalId { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string ExternalSeriesId { get; set; } /// <summary> /// Gets or sets the etag. /// </summary> /// <value>The etag.</value> - [IgnoreDataMember] + [JsonIgnore] public string ExternalEtag { get; set; } - [IgnoreDataMember] + [JsonIgnore] public virtual bool IsHidden => false; public BaseItem GetOwner() @@ -295,7 +295,7 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the type of the location. /// </summary> /// <value>The type of the location.</value> - [IgnoreDataMember] + [JsonIgnore] public virtual LocationType LocationType { get @@ -320,7 +320,7 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] public MediaProtocol? PathProtocol { get @@ -343,13 +343,13 @@ namespace MediaBrowser.Controller.Entities return current.HasValue && current.Value == protocol; } - [IgnoreDataMember] + [JsonIgnore] public bool IsFileProtocol => IsPathProtocol(MediaProtocol.File); - [IgnoreDataMember] + [JsonIgnore] public bool HasPathProtocol => PathProtocol.HasValue; - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsLocalMetadata { get @@ -363,7 +363,7 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] public virtual string FileNameWithoutExtension { get @@ -377,7 +377,7 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] public virtual bool EnableAlphaNumericSorting => true; private List<Tuple<StringBuilder, bool>> GetSortChunks(string s1) @@ -418,7 +418,7 @@ namespace MediaBrowser.Controller.Entities /// This is just a helper for convenience /// </summary> /// <value>The primary image path.</value> - [IgnoreDataMember] + [JsonIgnore] public string PrimaryImagePath => this.GetImagePath(ImageType.Primary); public bool IsMetadataFetcherEnabled(LibraryOptions libraryOptions, string name) @@ -503,7 +503,7 @@ namespace MediaBrowser.Controller.Entities foreach (var folder in collectionFolders) { - if (allowed.Contains(folder.Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) + if (allowed.Contains(folder.Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase)) { return true; } @@ -544,20 +544,20 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the date created. /// </summary> /// <value>The date created.</value> - [IgnoreDataMember] + [JsonIgnore] public DateTime DateCreated { get; set; } /// <summary> /// Gets or sets the date modified. /// </summary> /// <value>The date modified.</value> - [IgnoreDataMember] + [JsonIgnore] public DateTime DateModified { get; set; } - [IgnoreDataMember] + [JsonIgnore] public DateTime DateLastSaved { get; set; } - [IgnoreDataMember] + [JsonIgnore] public DateTime DateLastRefreshed { get; set; } /// <summary> @@ -583,24 +583,24 @@ namespace MediaBrowser.Controller.Entities return Name; } - [IgnoreDataMember] + [JsonIgnore] public bool IsLocked { get; set; } /// <summary> /// Gets or sets the locked fields. /// </summary> /// <value>The locked fields.</value> - [IgnoreDataMember] + [JsonIgnore] public MetadataFields[] LockedFields { get; set; } /// <summary> /// Gets the type of the media. /// </summary> /// <value>The type of the media.</value> - [IgnoreDataMember] + [JsonIgnore] public virtual string MediaType => null; - [IgnoreDataMember] + [JsonIgnore] public virtual string[] PhysicalLocations { get @@ -619,7 +619,7 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the name of the forced sort. /// </summary> /// <value>The name of the forced sort.</value> - [IgnoreDataMember] + [JsonIgnore] public string ForcedSortName { get => _forcedSortName; @@ -631,7 +631,7 @@ namespace MediaBrowser.Controller.Entities /// Gets the name of the sort. /// </summary> /// <value>The name of the sort.</value> - [IgnoreDataMember] + [JsonIgnore] public string SortName { get @@ -664,10 +664,10 @@ namespace MediaBrowser.Controller.Entities { if (SourceType == SourceType.Channel) { - return System.IO.Path.Combine(basePath, "channels", ChannelId.ToString("N"), Id.ToString("N")); + return System.IO.Path.Combine(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), Id.ToString("N", CultureInfo.InvariantCulture)); } - var idString = Id.ToString("N"); + var idString = Id.ToString("N", CultureInfo.InvariantCulture); basePath = System.IO.Path.Combine(basePath, "library"); @@ -744,7 +744,7 @@ namespace MediaBrowser.Controller.Entities return builder.ToString().RemoveDiacritics(); } - [IgnoreDataMember] + [JsonIgnore] public bool EnableMediaSourceDisplay { get @@ -758,14 +758,14 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] public Guid ParentId { get; set; } /// <summary> /// Gets or sets the parent. /// </summary> /// <value>The parent.</value> - [IgnoreDataMember] + [JsonIgnore] public Folder Parent { get => GetParent() as Folder; @@ -822,7 +822,7 @@ namespace MediaBrowser.Controller.Entities return null; } - [IgnoreDataMember] + [JsonIgnore] public virtual Guid DisplayParentId { get @@ -832,7 +832,7 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] public BaseItem DisplayParent { get @@ -850,97 +850,97 @@ namespace MediaBrowser.Controller.Entities /// When the item first debuted. For movies this could be premiere date, episodes would be first aired /// </summary> /// <value>The premiere date.</value> - [IgnoreDataMember] + [JsonIgnore] public DateTime? PremiereDate { get; set; } /// <summary> /// Gets or sets the end date. /// </summary> /// <value>The end date.</value> - [IgnoreDataMember] + [JsonIgnore] public DateTime? EndDate { get; set; } /// <summary> /// Gets or sets the official rating. /// </summary> /// <value>The official rating.</value> - [IgnoreDataMember] + [JsonIgnore] public string OfficialRating { get; set; } - [IgnoreDataMember] + [JsonIgnore] public int InheritedParentalRatingValue { get; set; } /// <summary> /// Gets or sets the critic rating. /// </summary> /// <value>The critic rating.</value> - [IgnoreDataMember] + [JsonIgnore] public float? CriticRating { get; set; } /// <summary> /// Gets or sets the custom rating. /// </summary> /// <value>The custom rating.</value> - [IgnoreDataMember] + [JsonIgnore] public string CustomRating { get; set; } /// <summary> /// Gets or sets the overview. /// </summary> /// <value>The overview.</value> - [IgnoreDataMember] + [JsonIgnore] public string Overview { get; set; } /// <summary> /// Gets or sets the studios. /// </summary> /// <value>The studios.</value> - [IgnoreDataMember] + [JsonIgnore] public string[] Studios { get; set; } /// <summary> /// Gets or sets the genres. /// </summary> /// <value>The genres.</value> - [IgnoreDataMember] + [JsonIgnore] public string[] Genres { get; set; } /// <summary> /// Gets or sets the tags. /// </summary> /// <value>The tags.</value> - [IgnoreDataMember] + [JsonIgnore] public string[] Tags { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string[] ProductionLocations { get; set; } /// <summary> /// Gets or sets the home page URL. /// </summary> /// <value>The home page URL.</value> - [IgnoreDataMember] + [JsonIgnore] public string HomePageUrl { get; set; } /// <summary> /// Gets or sets the community rating. /// </summary> /// <value>The community rating.</value> - [IgnoreDataMember] + [JsonIgnore] public float? CommunityRating { get; set; } /// <summary> /// Gets or sets the run time ticks. /// </summary> /// <value>The run time ticks.</value> - [IgnoreDataMember] + [JsonIgnore] public long? RunTimeTicks { get; set; } /// <summary> /// Gets or sets the production year. /// </summary> /// <value>The production year.</value> - [IgnoreDataMember] + [JsonIgnore] public int? ProductionYear { get; set; } /// <summary> @@ -948,20 +948,20 @@ namespace MediaBrowser.Controller.Entities /// This could be episode number, album track number, etc. /// </summary> /// <value>The index number.</value> - [IgnoreDataMember] + [JsonIgnore] public int? IndexNumber { get; set; } /// <summary> /// For an episode this could be the season number, or for a song this could be the disc number. /// </summary> /// <value>The parent index number.</value> - [IgnoreDataMember] + [JsonIgnore] public int? ParentIndexNumber { get; set; } - [IgnoreDataMember] + [JsonIgnore] public virtual bool HasLocalAlternateVersions => false; - [IgnoreDataMember] + [JsonIgnore] public string OfficialRatingForComparison { get @@ -982,7 +982,7 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] public string CustomRatingForComparison { get @@ -1095,7 +1095,7 @@ namespace MediaBrowser.Controller.Entities var info = new MediaSourceInfo { - Id = item.Id.ToString("N"), + Id = item.Id.ToString("N", CultureInfo.InvariantCulture), Protocol = protocol ?? MediaProtocol.File, MediaStreams = MediaSourceManager.GetMediaStreams(item.Id), Name = GetMediaSourceName(item), @@ -1113,7 +1113,7 @@ namespace MediaBrowser.Controller.Entities if (info.Protocol == MediaProtocol.File) { - info.ETag = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N"); + info.ETag = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture).GetMD5().ToString("N", CultureInfo.InvariantCulture); } var video = item as Video; @@ -1407,13 +1407,13 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] protected virtual bool SupportsOwnedItems => !ParentId.Equals(Guid.Empty) && IsFileProtocol; - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsPeople => false; - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsThemeMedia => false; /// <summary> @@ -1613,10 +1613,10 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the provider ids. /// </summary> /// <value>The provider ids.</value> - [IgnoreDataMember] + [JsonIgnore] public Dictionary<string, string> ProviderIds { get; set; } - [IgnoreDataMember] + [JsonIgnore] public virtual Folder LatestItemsIndexContainer => null; public virtual double GetDefaultPrimaryImageAspectRatio() @@ -1626,10 +1626,10 @@ namespace MediaBrowser.Controller.Entities public virtual string CreatePresentationUniqueKey() { - return Id.ToString("N"); + return Id.ToString("N", CultureInfo.InvariantCulture); } - [IgnoreDataMember] + [JsonIgnore] public string PresentationUniqueKey { get; set; } public string GetPresentationUniqueKey() @@ -1934,7 +1934,7 @@ namespace MediaBrowser.Controller.Entities return IsVisibleStandaloneInternal(user, true); } - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsInheritedParentImages => false; protected bool IsVisibleStandaloneInternal(User user, bool checkFolders) @@ -1977,10 +1977,10 @@ namespace MediaBrowser.Controller.Entities /// Gets a value indicating whether this instance is folder. /// </summary> /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public virtual bool IsFolder => false; - [IgnoreDataMember] + [JsonIgnore] public virtual bool IsDisplayedAsFolder => false; public virtual string GetClientTypeName() @@ -2045,7 +2045,7 @@ namespace MediaBrowser.Controller.Entities if (itemByPath == null) { - //Logger.LogWarning("Unable to find linked item at path {0}", info.Path); + Logger.LogWarning("Unable to find linked item at path {0}", info.Path); } return itemByPath; @@ -2057,7 +2057,7 @@ namespace MediaBrowser.Controller.Entities if (item == null) { - //Logger.LogWarning("Unable to find linked item at path {0}", info.Path); + Logger.LogWarning("Unable to find linked item at path {0}", info.Path); } return item; @@ -2066,7 +2066,7 @@ namespace MediaBrowser.Controller.Entities return null; } - [IgnoreDataMember] + [JsonIgnore] public virtual bool EnableRememberingTrackSelections => true; /// <summary> @@ -2085,14 +2085,17 @@ namespace MediaBrowser.Controller.Entities if (!current.Contains(name, StringComparer.OrdinalIgnoreCase)) { - if (current.Length == 0) + int curLen = current.Length; + if (curLen == 0) { Studios = new[] { name }; } else { - var list = - Studios = current.Concat(new[] { name }).ToArray(); + var newArr = new string[curLen + 1]; + current.CopyTo(newArr, 0); + newArr[curLen] = name; + Studios = newArr; } } } @@ -2231,8 +2234,12 @@ namespace MediaBrowser.Controller.Entities else { - var currentCount = ImageInfos.Length; - ImageInfos = ImageInfos.Concat(new[] { image }).ToArray(); + var current = ImageInfos; + var currentCount = current.Length; + var newArr = new ItemImageInfo[currentCount + 1]; + current.CopyTo(newArr, 0); + newArr[currentCount] = image; + ImageInfos = newArr; } } @@ -2736,7 +2743,7 @@ namespace MediaBrowser.Controller.Entities { var list = GetEtagValues(user); - return string.Join("|", list.ToArray()).GetMD5().ToString("N"); + return string.Join("|", list.ToArray()).GetMD5().ToString("N", CultureInfo.InvariantCulture); } protected virtual List<string> GetEtagValues(User user) @@ -2769,7 +2776,7 @@ namespace MediaBrowser.Controller.Entities return null; } - [IgnoreDataMember] + [JsonIgnore] public virtual bool IsTopParent { get @@ -2797,10 +2804,10 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsAncestors => true; - [IgnoreDataMember] + [JsonIgnore] public virtual bool StopRefreshIfLocalMetadataFound => true; public virtual IEnumerable<Guid> GetIdsForAncestorQuery() @@ -2871,16 +2878,24 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the remote trailers. /// </summary> /// <value>The remote trailers.</value> - public MediaUrl[] RemoteTrailers { get; set; } + public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; } public IEnumerable<BaseItem> GetExtras() { return ExtraIds.Select(LibraryManager.GetItemById).Where(i => i != null).OrderBy(i => i.SortName); } - public IEnumerable<BaseItem> GetExtras(ExtraType[] extraTypes) + public IEnumerable<BaseItem> GetExtras(IReadOnlyCollection<ExtraType> extraTypes) { - return ExtraIds.Select(LibraryManager.GetItemById).Where(i => i != null && extraTypes.Contains(i.ExtraType.Value)).OrderBy(i => i.SortName); + return ExtraIds.Select(LibraryManager.GetItemById).Where(i => i != null && extraTypes.Contains(i.ExtraType.Value)); + } + + public IEnumerable<BaseItem> GetTrailers() + { + if (this is IHasTrailers) + return ((IHasTrailers)this).LocalTrailerIds.Select(LibraryManager.GetItemById).Where(i => i != null).OrderBy(i => i.SortName); + else + return Array.Empty<BaseItem>(); } public IEnumerable<BaseItem> GetDisplayExtras() @@ -2900,7 +2915,7 @@ namespace MediaBrowser.Controller.Entities } // Possible types of extra videos - public static 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 }; + 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 }; public virtual bool SupportsExternalTransfer => false; } diff --git a/MediaBrowser.Controller/Entities/BasePluginFolder.cs b/MediaBrowser.Controller/Entities/BasePluginFolder.cs index 8cdb9695c..62d172fcc 100644 --- a/MediaBrowser.Controller/Entities/BasePluginFolder.cs +++ b/MediaBrowser.Controller/Entities/BasePluginFolder.cs @@ -1,4 +1,4 @@ -using MediaBrowser.Model.Serialization; +using System.Text.Json.Serialization; namespace MediaBrowser.Controller.Entities { @@ -8,7 +8,7 @@ namespace MediaBrowser.Controller.Entities /// </summary> public abstract class BasePluginFolder : Folder, ICollectionFolder { - [IgnoreDataMember] + [JsonIgnore] public virtual string CollectionType => null; public override bool CanDelete() @@ -21,10 +21,10 @@ namespace MediaBrowser.Controller.Entities return true; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => false; //public override double? GetDefaultPrimaryImageAspectRatio() diff --git a/MediaBrowser.Controller/Entities/Book.cs b/MediaBrowser.Controller/Entities/Book.cs index 7a23d9a66..44c35374d 100644 --- a/MediaBrowser.Controller/Entities/Book.cs +++ b/MediaBrowser.Controller/Entities/Book.cs @@ -1,21 +1,21 @@ using System; using System.Linq; +using System.Text.Json.Serialization; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { public class Book : BaseItem, IHasLookupInfo<BookInfo>, IHasSeries { - [IgnoreDataMember] + [JsonIgnore] public override string MediaType => Model.Entities.MediaType.Book; - [IgnoreDataMember] + [JsonIgnore] public string SeriesPresentationUniqueKey { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string SeriesName { get; set; } - [IgnoreDataMember] + [JsonIgnore] public Guid SeriesId { get; set; } public string FindSeriesSortName() diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs index 275052d48..bc5e7467e 100644 --- a/MediaBrowser.Controller/Entities/CollectionFolder.cs +++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs @@ -2,14 +2,13 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; - using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Extensions; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; @@ -33,10 +32,10 @@ namespace MediaBrowser.Controller.Entities PhysicalFolderIds = Array.Empty<Guid>(); } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => false; public override bool CanDelete() @@ -144,10 +143,10 @@ namespace MediaBrowser.Controller.Entities /// Allow different display preferences for each collection folder /// </summary> /// <value>The display prefs id.</value> - [IgnoreDataMember] + [JsonIgnore] public override Guid DisplayPreferencesId => Id; - [IgnoreDataMember] + [JsonIgnore] public override string[] PhysicalLocations => PhysicalLocationsList; public override bool IsSaveLocalMetadataEnabled() @@ -311,7 +310,7 @@ namespace MediaBrowser.Controller.Entities /// Our children are actually just references to the ones in the physical root... /// </summary> /// <value>The actual children.</value> - [IgnoreDataMember] + [JsonIgnore] public override IEnumerable<BaseItem> Children => GetActualChildren(); public IEnumerable<BaseItem> GetActualChildren() @@ -361,7 +360,7 @@ namespace MediaBrowser.Controller.Entities return result; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => false; } } diff --git a/MediaBrowser.Controller/Entities/Extensions.cs b/MediaBrowser.Controller/Entities/Extensions.cs index f3bddd29c..d2ca11740 100644 --- a/MediaBrowser.Controller/Entities/Extensions.cs +++ b/MediaBrowser.Controller/Entities/Extensions.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using MediaBrowser.Common.Extensions; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Entities @@ -28,13 +29,17 @@ namespace MediaBrowser.Controller.Entities Url = url }; - if (item.RemoteTrailers.Length == 0) + if (item.RemoteTrailers.Count == 0) { item.RemoteTrailers = new[] { mediaUrl }; } else { - item.RemoteTrailers = item.RemoteTrailers.Concat(new[] { mediaUrl }).ToArray(); + var oldIds = item.RemoteTrailers; + var newIds = new MediaUrl[oldIds.Count + 1]; + oldIds.CopyTo(newIds); + newIds[oldIds.Count] = mediaUrl; + item.RemoteTrailers = newIds; } } } diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index c056bc0b4..61cc208d7 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Progress; @@ -17,7 +19,6 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dto; using MediaBrowser.Model.IO; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities @@ -38,7 +39,7 @@ namespace MediaBrowser.Controller.Entities public LinkedChild[] LinkedChildren { get; set; } - [IgnoreDataMember] + [JsonIgnore] public DateTime? DateLastMediaAdded { get; set; } public Folder() @@ -46,35 +47,35 @@ namespace MediaBrowser.Controller.Entities LinkedChildren = Array.Empty<LinkedChild>(); } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsThemeMedia => true; - [IgnoreDataMember] + [JsonIgnore] public virtual bool IsPreSorted => false; - [IgnoreDataMember] + [JsonIgnore] public virtual bool IsPhysicalRoot => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => true; /// <summary> /// Gets a value indicating whether this instance is folder. /// </summary> /// <value><c>true</c> if this instance is folder; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public override bool IsFolder => true; - [IgnoreDataMember] + [JsonIgnore] public override bool IsDisplayedAsFolder => true; - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsCumulativeRunTimeTicks => false; - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsDateLastMediaAdded => false; public override bool CanDelete() @@ -99,7 +100,7 @@ namespace MediaBrowser.Controller.Entities return baseResult; } - [IgnoreDataMember] + [JsonIgnore] public override string FileNameWithoutExtension { get @@ -126,7 +127,7 @@ namespace MediaBrowser.Controller.Entities return true; } - [IgnoreDataMember] + [JsonIgnore] protected virtual bool SupportsShortcutChildren => false; /// <summary> @@ -161,14 +162,14 @@ namespace MediaBrowser.Controller.Entities /// Gets the actual children. /// </summary> /// <value>The actual children.</value> - [IgnoreDataMember] + [JsonIgnore] public virtual IEnumerable<BaseItem> Children => LoadChildren(); /// <summary> /// thread-safe access to all recursive children of this folder - without regard to user /// </summary> /// <value>The recursive children.</value> - [IgnoreDataMember] + [JsonIgnore] public IEnumerable<BaseItem> RecursiveChildren => GetRecursiveChildren(); public override bool IsVisible(User user) @@ -177,7 +178,7 @@ namespace MediaBrowser.Controller.Entities { if (user.Policy.BlockedMediaFolders != null) { - if (user.Policy.BlockedMediaFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase) || + if (user.Policy.BlockedMediaFolders.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase) || // Backwards compatibility user.Policy.BlockedMediaFolders.Contains(Name, StringComparer.OrdinalIgnoreCase)) @@ -187,7 +188,7 @@ namespace MediaBrowser.Controller.Entities } else { - if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Contains(Id.ToString("N"), StringComparer.OrdinalIgnoreCase)) + if (!user.Policy.EnableAllFolders && !user.Policy.EnabledFolders.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase)) { return false; } @@ -665,36 +666,36 @@ namespace MediaBrowser.Controller.Entities query.StartIndex = null; query.Limit = null; - var itemsList = LibraryManager.GetItemList(query); + IEnumerable<BaseItem> itemsList = LibraryManager.GetItemList(query); var user = query.User; if (user != null) { // needed for boxsets - itemsList = itemsList.Where(i => i.IsVisibleStandalone(query.User)).ToList(); + itemsList = itemsList.Where(i => i.IsVisibleStandalone(query.User)); } - BaseItem[] returnItems; + IEnumerable<BaseItem> returnItems; int totalCount = 0; if (query.EnableTotalRecordCount) { - var itemsArray = itemsList.ToArray(); - totalCount = itemsArray.Length; - returnItems = itemsArray; + var itemArray = itemsList.ToArray(); + totalCount = itemArray.Length; + returnItems = itemArray; } else { - returnItems = itemsList.ToArray(); + returnItems = itemsList; } if (limit.HasValue) { - returnItems = returnItems.Skip(startIndex ?? 0).Take(limit.Value).ToArray(); + returnItems = returnItems.Skip(startIndex ?? 0).Take(limit.Value); } else if (startIndex.HasValue) { - returnItems = returnItems.Skip(startIndex.Value).ToArray(); + returnItems = returnItems.Skip(startIndex.Value); } return new QueryResult<BaseItem> @@ -1427,7 +1428,7 @@ namespace MediaBrowser.Controller.Entities .Where(i => i.Item2 != null); } - [IgnoreDataMember] + [JsonIgnore] protected override bool SupportsOwnedItems => base.SupportsOwnedItems || SupportsShortcutChildren; protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) @@ -1594,7 +1595,7 @@ namespace MediaBrowser.Controller.Entities return !IsPlayed(user); } - [IgnoreDataMember] + [JsonIgnore] public virtual bool SupportsUserDataFromChildren { get diff --git a/MediaBrowser.Controller/Entities/Genre.cs b/MediaBrowser.Controller/Entities/Genre.cs index 44cb62d22..773c7df34 100644 --- a/MediaBrowser.Controller/Entities/Genre.cs +++ b/MediaBrowser.Controller/Entities/Genre.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Extensions; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities @@ -34,13 +34,13 @@ namespace MediaBrowser.Controller.Entities /// If the item is a folder, it returns the folder itself /// </summary> /// <value>The containing folder path.</value> - [IgnoreDataMember] + [JsonIgnore] public override string ContainingFolderPath => Path; - [IgnoreDataMember] + [JsonIgnore] public override bool IsDisplayedAsFolder => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsAncestors => false; public override bool IsSaveLocalMetadataEnabled() @@ -61,7 +61,7 @@ namespace MediaBrowser.Controller.Entities return LibraryManager.GetItemList(query); } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => false; public static string GetPath(string name) diff --git a/MediaBrowser.Controller/Entities/IHasTrailers.cs b/MediaBrowser.Controller/Entities/IHasTrailers.cs index 3bdb9b64a..dd8e3c45f 100644 --- a/MediaBrowser.Controller/Entities/IHasTrailers.cs +++ b/MediaBrowser.Controller/Entities/IHasTrailers.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using MediaBrowser.Model.Entities; namespace MediaBrowser.Controller.Entities @@ -11,29 +10,82 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the remote trailers. /// </summary> /// <value>The remote trailers.</value> - MediaUrl[] RemoteTrailers { get; set; } + IReadOnlyList<MediaUrl> RemoteTrailers { get; set; } /// <summary> /// Gets or sets the local trailer ids. /// </summary> /// <value>The local trailer ids.</value> - Guid[] LocalTrailerIds { get; set; } - Guid[] RemoteTrailerIds { get; set; } + IReadOnlyList<Guid> LocalTrailerIds { get; set; } + + /// <summary> + /// Gets or sets the remote trailer ids. + /// </summary> + /// <value>The remote trailer ids.</value> + IReadOnlyList<Guid> RemoteTrailerIds { get; set; } + Guid Id { get; set; } } + /// <summary> + /// Class providing extension methods for working with <see cref="IHasTrailers" />. + /// </summary> public static class HasTrailerExtensions { /// <summary> + /// Gets the trailer count. + /// </summary> + /// <returns><see cref="IReadOnlyList{Guid}" />.</returns> + public static int GetTrailerCount(this IHasTrailers item) + => item.LocalTrailerIds.Count + item.RemoteTrailerIds.Count; + + /// <summary> /// Gets the trailer ids. /// </summary> - /// <returns>List<Guid>.</returns> - public static List<Guid> GetTrailerIds(this IHasTrailers item) + /// <returns><see cref="IReadOnlyList{Guid}" />.</returns> + public static IReadOnlyList<Guid> GetTrailerIds(this IHasTrailers item) { - var list = item.LocalTrailerIds.ToList(); - list.AddRange(item.RemoteTrailerIds); - return list; + var localIds = item.LocalTrailerIds; + var remoteIds = item.RemoteTrailerIds; + + var all = new Guid[localIds.Count + remoteIds.Count]; + var index = 0; + foreach (var id in localIds) + { + all[index++] = id; + } + + foreach (var id in remoteIds) + { + all[index++] = id; + } + + return all; } + /// <summary> + /// Gets the trailers. + /// </summary> + /// <returns><see cref="IReadOnlyList{BaseItem}" />.</returns> + public static IReadOnlyList<BaseItem> GetTrailers(this IHasTrailers item) + { + var localIds = item.LocalTrailerIds; + var remoteIds = item.RemoteTrailerIds; + var libraryManager = BaseItem.LibraryManager; + + var all = new BaseItem[localIds.Count + remoteIds.Count]; + var index = 0; + foreach (var id in localIds) + { + all[index++] = libraryManager.GetItemById(id); + } + + foreach (var id in remoteIds) + { + all[index++] = libraryManager.GetItemById(id); + } + + return all; + } } } diff --git a/MediaBrowser.Controller/Entities/ItemImageInfo.cs b/MediaBrowser.Controller/Entities/ItemImageInfo.cs index 848493864..fc46dec2e 100644 --- a/MediaBrowser.Controller/Entities/ItemImageInfo.cs +++ b/MediaBrowser.Controller/Entities/ItemImageInfo.cs @@ -1,6 +1,6 @@ using System; +using System.Text.Json.Serialization; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { @@ -28,7 +28,7 @@ namespace MediaBrowser.Controller.Entities public int Height { get; set; } - [IgnoreDataMember] + [JsonIgnore] public bool IsLocalFile => Path == null || !Path.StartsWith("http", StringComparison.OrdinalIgnoreCase); } } diff --git a/MediaBrowser.Controller/Entities/LinkedChild.cs b/MediaBrowser.Controller/Entities/LinkedChild.cs index bb2d03246..d88c31007 100644 --- a/MediaBrowser.Controller/Entities/LinkedChild.cs +++ b/MediaBrowser.Controller/Entities/LinkedChild.cs @@ -1,7 +1,8 @@ using System; using System.Collections.Generic; +using System.Globalization; +using System.Text.Json.Serialization; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { @@ -11,7 +12,7 @@ namespace MediaBrowser.Controller.Entities public LinkedChildType Type { get; set; } public string LibraryItemId { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string Id { get; set; } /// <summary> @@ -29,7 +30,7 @@ namespace MediaBrowser.Controller.Entities if (string.IsNullOrEmpty(child.Path)) { - child.LibraryItemId = item.Id.ToString("N"); + child.LibraryItemId = item.Id.ToString("N", CultureInfo.InvariantCulture); } return child; @@ -37,7 +38,7 @@ namespace MediaBrowser.Controller.Entities public LinkedChild() { - Id = Guid.NewGuid().ToString("N"); + Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); } } diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs index a532b5ee9..feaf8c45a 100644 --- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs +++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Users; namespace MediaBrowser.Controller.Entities.Movies @@ -24,17 +24,20 @@ namespace MediaBrowser.Controller.Entities.Movies DisplayOrder = ItemSortBy.PremiereDate; } - [IgnoreDataMember] + [JsonIgnore] protected override bool FilterLinkedChildrenPerUser => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => true; - public Guid[] LocalTrailerIds { get; set; } - public Guid[] RemoteTrailerIds { get; set; } + /// <inheritdoc /> + public IReadOnlyList<Guid> LocalTrailerIds { get; set; } + + /// <inheritdoc /> + public IReadOnlyList<Guid> RemoteTrailerIds { get; set; } /// <summary> /// Gets or sets the display order. @@ -61,7 +64,8 @@ namespace MediaBrowser.Controller.Entities.Movies { return base.GetNonCachedChildren(directoryService); } - return new List<BaseItem>(); + + return Enumerable.Empty<BaseItem>(); } protected override List<BaseItem> LoadChildren() @@ -75,7 +79,7 @@ namespace MediaBrowser.Controller.Entities.Movies return new List<BaseItem>(); } - [IgnoreDataMember] + [JsonIgnore] private bool IsLegacyBoxSet { get @@ -94,7 +98,7 @@ namespace MediaBrowser.Controller.Entities.Movies } } - [IgnoreDataMember] + [JsonIgnore] public override bool IsPreSorted => true; public override bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders) diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs index 20c5b3521..11dc472b6 100644 --- a/MediaBrowser.Controller/Entities/Movies/Movie.cs +++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Providers; @@ -8,7 +9,6 @@ using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities.Movies { @@ -27,8 +27,11 @@ namespace MediaBrowser.Controller.Entities.Movies RemoteTrailerIds = Array.Empty<Guid>(); } - public Guid[] LocalTrailerIds { get; set; } - public Guid[] RemoteTrailerIds { get; set; } + /// <inheritdoc /> + public IReadOnlyList<Guid> LocalTrailerIds { get; set; } + + /// <inheritdoc /> + public IReadOnlyList<Guid> RemoteTrailerIds { get; set; } /// <summary> /// Gets or sets the name of the TMDB collection. @@ -36,7 +39,7 @@ namespace MediaBrowser.Controller.Entities.Movies /// <value>The name of the TMDB collection.</value> public string TmdbCollectionName { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string CollectionName { get => TmdbCollectionName; @@ -183,7 +186,7 @@ namespace MediaBrowser.Controller.Entities.Movies return list; } - [IgnoreDataMember] + [JsonIgnore] public override bool StopRefreshIfLocalMetadataFound => false; } } diff --git a/MediaBrowser.Controller/Entities/MusicVideo.cs b/MediaBrowser.Controller/Entities/MusicVideo.cs index 5bf082b7e..603242063 100644 --- a/MediaBrowser.Controller/Entities/MusicVideo.cs +++ b/MediaBrowser.Controller/Entities/MusicVideo.cs @@ -1,24 +1,23 @@ using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { public class MusicVideo : Video, IHasArtist, IHasMusicGenres, IHasLookupInfo<MusicVideoInfo> { - [IgnoreDataMember] - public string[] Artists { get; set; } + /// <inheritdoc /> + [JsonIgnore] + public IReadOnlyList<string> Artists { get; set; } public MusicVideo() { Artists = Array.Empty<string>(); } - [IgnoreDataMember] - public string[] AllArtists => Artists; - public override UnratedItem GetBlockUnratedType() { return UnratedItem.Music; diff --git a/MediaBrowser.Controller/Entities/Person.cs b/MediaBrowser.Controller/Entities/Person.cs index dd0183489..d9b4b2206 100644 --- a/MediaBrowser.Controller/Entities/Person.cs +++ b/MediaBrowser.Controller/Entities/Person.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities @@ -50,7 +50,7 @@ namespace MediaBrowser.Controller.Entities /// If the item is a folder, it returns the folder itself /// </summary> /// <value>The containing folder path.</value> - [IgnoreDataMember] + [JsonIgnore] public override string ContainingFolderPath => Path; public override bool CanDelete() @@ -63,13 +63,13 @@ namespace MediaBrowser.Controller.Entities return true; } - [IgnoreDataMember] + [JsonIgnore] public override bool EnableAlphaNumericSorting => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsAncestors => false; public static string GetPath(string name) diff --git a/MediaBrowser.Controller/Entities/Photo.cs b/MediaBrowser.Controller/Entities/Photo.cs index 60c832189..86d62add9 100644 --- a/MediaBrowser.Controller/Entities/Photo.cs +++ b/MediaBrowser.Controller/Entities/Photo.cs @@ -1,21 +1,21 @@ +using System.Text.Json.Serialization; using MediaBrowser.Model.Drawing; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { public class Photo : BaseItem { - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsLocalMetadata => false; - [IgnoreDataMember] + [JsonIgnore] public override string MediaType => Model.Entities.MediaType.Photo; - [IgnoreDataMember] + [JsonIgnore] public override Folder LatestItemsIndexContainer => AlbumEntity; - [IgnoreDataMember] + [JsonIgnore] public PhotoAlbum AlbumEntity { get diff --git a/MediaBrowser.Controller/Entities/PhotoAlbum.cs b/MediaBrowser.Controller/Entities/PhotoAlbum.cs index 4cd0c8b66..b86f1ac2a 100644 --- a/MediaBrowser.Controller/Entities/PhotoAlbum.cs +++ b/MediaBrowser.Controller/Entities/PhotoAlbum.cs @@ -1,16 +1,16 @@ -using MediaBrowser.Model.Serialization; +using System.Text.Json.Serialization; namespace MediaBrowser.Controller.Entities { public class PhotoAlbum : Folder { - [IgnoreDataMember] + [JsonIgnore] public override bool AlwaysScanInternalMetadataPath => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => false; } } diff --git a/MediaBrowser.Controller/Entities/Studio.cs b/MediaBrowser.Controller/Entities/Studio.cs index d6da0d48c..068032317 100644 --- a/MediaBrowser.Controller/Entities/Studio.cs +++ b/MediaBrowser.Controller/Entities/Studio.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; using MediaBrowser.Controller.Extensions; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities @@ -28,13 +28,13 @@ namespace MediaBrowser.Controller.Entities /// If the item is a folder, it returns the folder itself /// </summary> /// <value>The containing folder path.</value> - [IgnoreDataMember] + [JsonIgnore] public override string ContainingFolderPath => Path; - [IgnoreDataMember] + [JsonIgnore] public override bool IsDisplayedAsFolder => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsAncestors => false; public override double GetDefaultPrimaryImageAspectRatio() @@ -62,7 +62,7 @@ namespace MediaBrowser.Controller.Entities return LibraryManager.GetItemList(query); } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => false; public static string GetPath(string name) diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index fb29c07b0..49229fa4b 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -2,11 +2,11 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities.TV @@ -23,8 +23,11 @@ namespace MediaBrowser.Controller.Entities.TV RemoteTrailerIds = Array.Empty<Guid>(); } - public Guid[] LocalTrailerIds { get; set; } - public Guid[] RemoteTrailerIds { get; set; } + /// <inheritdoc /> + public IReadOnlyList<Guid> LocalTrailerIds { get; set; } + + /// <inheritdoc /> + public IReadOnlyList<Guid> RemoteTrailerIds { get; set; } /// <summary> /// Gets the season in which it aired. @@ -46,25 +49,25 @@ namespace MediaBrowser.Controller.Entities.TV return series == null ? SeriesName : series.SortName; } - [IgnoreDataMember] + [JsonIgnore] protected override bool SupportsOwnedItems => IsStacked || MediaSourceCount > 1; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => true; - [IgnoreDataMember] + [JsonIgnore] public int? AiredSeasonNumber => AirsAfterSeasonNumber ?? AirsBeforeSeasonNumber ?? ParentIndexNumber; - [IgnoreDataMember] + [JsonIgnore] public override Folder LatestItemsIndexContainer => Series; - [IgnoreDataMember] + [JsonIgnore] public override Guid DisplayParentId => SeasonId; - [IgnoreDataMember] + [JsonIgnore] protected override bool EnableDefaultVideoUserDataKeys => false; public override double GetDefaultPrimaryImageAspectRatio() @@ -101,7 +104,7 @@ namespace MediaBrowser.Controller.Entities.TV /// This Episode's Series Instance /// </summary> /// <value>The series.</value> - [IgnoreDataMember] + [JsonIgnore] public Series Series { get @@ -115,7 +118,7 @@ namespace MediaBrowser.Controller.Entities.TV } } - [IgnoreDataMember] + [JsonIgnore] public Season Season { get @@ -129,16 +132,16 @@ namespace MediaBrowser.Controller.Entities.TV } } - [IgnoreDataMember] + [JsonIgnore] public bool IsInSeasonFolder => FindParent<Season>() != null; - [IgnoreDataMember] + [JsonIgnore] public string SeriesPresentationUniqueKey { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string SeriesName { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string SeasonName { get; set; } public string FindSeriesPresentationUniqueKey() @@ -221,7 +224,7 @@ namespace MediaBrowser.Controller.Entities.TV return false; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsRemoteImageDownloading { get @@ -235,12 +238,12 @@ namespace MediaBrowser.Controller.Entities.TV } } - [IgnoreDataMember] + [JsonIgnore] public bool IsMissingEpisode => LocationType == LocationType.Virtual; - [IgnoreDataMember] + [JsonIgnore] public Guid SeasonId { get; set; } - [IgnoreDataMember] + [JsonIgnore] public Guid SeriesId { get; set; } public Guid FindSeriesId() diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 5d7c260d1..9c8a469e2 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Users; namespace MediaBrowser.Controller.Entities.TV @@ -15,22 +15,22 @@ namespace MediaBrowser.Controller.Entities.TV /// </summary> public class Season : Folder, IHasSeries, IHasLookupInfo<SeasonInfo> { - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsAddingToPlaylist => true; - [IgnoreDataMember] + [JsonIgnore] public override bool IsPreSorted => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsDateLastMediaAdded => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => true; - [IgnoreDataMember] + [JsonIgnore] public override Guid DisplayParentId => SeriesId; public override double GetDefaultPrimaryImageAspectRatio() @@ -71,7 +71,7 @@ namespace MediaBrowser.Controller.Entities.TV /// This Episode's Series Instance /// </summary> /// <value>The series.</value> - [IgnoreDataMember] + [JsonIgnore] public Series Series { get @@ -85,7 +85,7 @@ namespace MediaBrowser.Controller.Entities.TV } } - [IgnoreDataMember] + [JsonIgnore] public string SeriesPath { get @@ -179,13 +179,13 @@ namespace MediaBrowser.Controller.Entities.TV return UnratedItem.Series; } - [IgnoreDataMember] + [JsonIgnore] public string SeriesPresentationUniqueKey { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string SeriesName { get; set; } - [IgnoreDataMember] + [JsonIgnore] public Guid SeriesId { get; set; } public string FindSeriesPresentationUniqueKey() diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs index eae834f6f..76cb9a651 100644 --- a/MediaBrowser.Controller/Entities/TV/Series.cs +++ b/MediaBrowser.Controller/Entities/TV/Series.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Dto; @@ -9,7 +11,6 @@ using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Users; namespace MediaBrowser.Controller.Entities.TV @@ -30,23 +31,26 @@ namespace MediaBrowser.Controller.Entities.TV public DayOfWeek[] AirDays { get; set; } public string AirTime { get; set; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsAddingToPlaylist => true; - [IgnoreDataMember] + [JsonIgnore] public override bool IsPreSorted => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsDateLastMediaAdded => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => true; - public Guid[] LocalTrailerIds { get; set; } - public Guid[] RemoteTrailerIds { get; set; } + /// <inheritdoc /> + public IReadOnlyList<Guid> LocalTrailerIds { get; set; } + + /// <inheritdoc /> + public IReadOnlyList<Guid> RemoteTrailerIds { get; set; } /// <summary> /// airdate, dvd or absolute @@ -91,7 +95,7 @@ namespace MediaBrowser.Controller.Entities.TV } var folders = LibraryManager.GetCollectionFolders(this) - .Select(i => i.Id.ToString("N")) + .Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture)) .ToArray(); if (folders.Length == 0) @@ -500,7 +504,7 @@ namespace MediaBrowser.Controller.Entities.TV return list; } - [IgnoreDataMember] + [JsonIgnore] public override bool StopRefreshIfLocalMetadataFound => false; } } diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs index 5bf22d7bc..0b8be90cd 100644 --- a/MediaBrowser.Controller/Entities/Trailer.cs +++ b/MediaBrowser.Controller/Entities/Trailer.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { @@ -93,7 +93,7 @@ namespace MediaBrowser.Controller.Entities return list; } - [IgnoreDataMember] + [JsonIgnore] public override bool StopRefreshIfLocalMetadataFound => false; } } diff --git a/MediaBrowser.Controller/Entities/User.cs b/MediaBrowser.Controller/Entities/User.cs index 9952ba418..c70ecccf1 100644 --- a/MediaBrowser.Controller/Entities/User.cs +++ b/MediaBrowser.Controller/Entities/User.cs @@ -1,11 +1,12 @@ using System; +using System.Globalization; using System.IO; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Users; namespace MediaBrowser.Controller.Entities @@ -16,13 +17,6 @@ namespace MediaBrowser.Controller.Entities public class User : BaseItem { public static IUserManager UserManager { get; set; } - public static IXmlSerializer XmlSerializer { get; set; } - - /// <summary> - /// From now on all user paths will be Id-based. - /// This is for backwards compatibility. - /// </summary> - public bool UsesIdForConfigurationPath { get; set; } /// <summary> /// Gets or sets the password. @@ -30,9 +24,8 @@ namespace MediaBrowser.Controller.Entities /// <value>The password.</value> public string Password { get; set; } public string EasyPassword { get; set; } - public string Salt { get; set; } - // Strictly to remove IgnoreDataMember + // Strictly to remove JsonIgnore public override ItemImageInfo[] ImageInfos { get => base.ImageInfos; @@ -43,7 +36,7 @@ namespace MediaBrowser.Controller.Entities /// Gets or sets the path. /// </summary> /// <value>The path.</value> - [IgnoreDataMember] + [JsonIgnore] public override string Path { get => ConfigurationDirectoryPath; @@ -72,14 +65,14 @@ namespace MediaBrowser.Controller.Entities /// If the item is a folder, it returns the folder itself /// </summary> /// <value>The containing folder path.</value> - [IgnoreDataMember] + [JsonIgnore] public override string ContainingFolderPath => Path; /// <summary> /// Gets the root folder. /// </summary> /// <value>The root folder.</value> - [IgnoreDataMember] + [JsonIgnore] public Folder RootFolder => LibraryManager.GetUserRootFolder(); /// <summary> @@ -95,7 +88,7 @@ namespace MediaBrowser.Controller.Entities private volatile UserConfiguration _config; private readonly object _configSyncLock = new object(); - [IgnoreDataMember] + [JsonIgnore] public UserConfiguration Configuration { get @@ -118,7 +111,7 @@ namespace MediaBrowser.Controller.Entities private volatile UserPolicy _policy; private readonly object _policySyncLock = new object(); - [IgnoreDataMember] + [JsonIgnore] public UserPolicy Policy { get @@ -147,46 +140,23 @@ namespace MediaBrowser.Controller.Entities /// <exception cref="ArgumentNullException"></exception> public Task Rename(string newName) { - if (string.IsNullOrEmpty(newName)) - { - throw new ArgumentNullException(nameof(newName)); - } - - // If only the casing is changing, leave the file system alone - if (!UsesIdForConfigurationPath && !string.Equals(newName, Name, StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrWhiteSpace(newName)) { - UsesIdForConfigurationPath = true; - - // Move configuration - var newConfigDirectory = GetConfigurationDirectoryPath(newName); - var oldConfigurationDirectory = ConfigurationDirectoryPath; - - // Exceptions will be thrown if these paths already exist - if (Directory.Exists(newConfigDirectory)) - { - Directory.Delete(newConfigDirectory, true); - } - - if (Directory.Exists(oldConfigurationDirectory)) - { - Directory.Move(oldConfigurationDirectory, newConfigDirectory); - } - else - { - Directory.CreateDirectory(newConfigDirectory); - } + throw new ArgumentException("Username can't be empty", nameof(newName)); } Name = newName; - return RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)) - { - ReplaceAllMetadata = true, - ImageRefreshMode = MetadataRefreshMode.FullRefresh, - MetadataRefreshMode = MetadataRefreshMode.FullRefresh, - ForceSave = true + return RefreshMetadata( + new MetadataRefreshOptions(new DirectoryService(Logger, FileSystem)) + { + ReplaceAllMetadata = true, + ImageRefreshMode = MetadataRefreshMode.FullRefresh, + MetadataRefreshMode = MetadataRefreshMode.FullRefresh, + ForceSave = true - }, CancellationToken.None); + }, + CancellationToken.None); } public override void UpdateToRepository(ItemUpdateType updateReason, CancellationToken cancellationToken) @@ -198,7 +168,7 @@ namespace MediaBrowser.Controller.Entities /// Gets the path to the user's configuration directory /// </summary> /// <value>The configuration directory path.</value> - [IgnoreDataMember] + [JsonIgnore] public string ConfigurationDirectoryPath => GetConfigurationDirectoryPath(Name); public override double GetDefaultPrimaryImageAspectRatio() @@ -215,22 +185,9 @@ namespace MediaBrowser.Controller.Entities { var parentPath = ConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath; - // Legacy - if (!UsesIdForConfigurationPath) - { - if (string.IsNullOrEmpty(username)) - { - throw new ArgumentNullException(nameof(username)); - } - - var safeFolderName = FileSystem.GetValidFilename(username); - - return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, safeFolderName); - } - // TODO: Remove idPath and just use usernamePath for future releases var usernamePath = System.IO.Path.Combine(parentPath, username); - var idPath = System.IO.Path.Combine(parentPath, Id.ToString("N")); + var idPath = System.IO.Path.Combine(parentPath, Id.ToString("N", CultureInfo.InvariantCulture)); if (!Directory.Exists(usernamePath) && Directory.Exists(idPath)) { Directory.Move(idPath, usernamePath); @@ -295,7 +252,7 @@ namespace MediaBrowser.Controller.Entities return false; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => false; public long InternalId { get; set; } diff --git a/MediaBrowser.Controller/Entities/UserItemData.cs b/MediaBrowser.Controller/Entities/UserItemData.cs index f7136bdf2..ab425ee0f 100644 --- a/MediaBrowser.Controller/Entities/UserItemData.cs +++ b/MediaBrowser.Controller/Entities/UserItemData.cs @@ -1,5 +1,5 @@ using System; -using MediaBrowser.Model.Serialization; +using System.Text.Json.Serialization; namespace MediaBrowser.Controller.Entities { @@ -93,7 +93,7 @@ namespace MediaBrowser.Controller.Entities /// This should never be serialized. /// </summary> /// <value><c>null</c> if [likes] contains no value, <c>true</c> if [likes]; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool? Likes { get diff --git a/MediaBrowser.Controller/Entities/UserRootFolder.cs b/MediaBrowser.Controller/Entities/UserRootFolder.cs index 7fe8df8af..7fcf48a48 100644 --- a/MediaBrowser.Controller/Entities/UserRootFolder.cs +++ b/MediaBrowser.Controller/Entities/UserRootFolder.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { @@ -33,10 +33,10 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => false; private void ClearCache() @@ -75,10 +75,10 @@ namespace MediaBrowser.Controller.Entities return GetChildren(user, true).Count; } - [IgnoreDataMember] + [JsonIgnore] protected override bool SupportsShortcutChildren => true; - [IgnoreDataMember] + [JsonIgnore] public override bool IsPreSorted => true; protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs index 4a6d32dce..fd045f0dd 100644 --- a/MediaBrowser.Controller/Entities/UserView.cs +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using System.Threading.Tasks; using MediaBrowser.Controller.TV; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { @@ -17,7 +17,7 @@ namespace MediaBrowser.Controller.Entities public static ITVSeriesManager TVSeriesManager; - [IgnoreDataMember] + [JsonIgnore] public string CollectionType => ViewType; public override IEnumerable<Guid> GetIdsForAncestorQuery() @@ -40,10 +40,10 @@ namespace MediaBrowser.Controller.Entities return list; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => false; public override int GetChildCount(User user) @@ -167,7 +167,7 @@ namespace MediaBrowser.Controller.Entities return Task.CompletedTask; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => false; } } diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index e483c8f34..454bdc4ae 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.Movies; @@ -987,7 +988,7 @@ namespace MediaBrowser.Controller.Entities private UserView GetUserViewWithName(string name, string type, string sortName, BaseItem parent) { - return _userViewManager.GetUserSubView(parent.Id, parent.Id.ToString("N"), type, sortName); + return _userViewManager.GetUserSubView(parent.Id, parent.Id.ToString("N", CultureInfo.InvariantCulture), type, sortName); } private UserView GetUserView(string type, string localizationKey, string sortName, BaseItem parent) diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index 8379dcc09..60906bdb0 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Library; @@ -13,7 +14,6 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Entities { @@ -25,23 +25,23 @@ namespace MediaBrowser.Controller.Entities ISupportsPlaceHolders, IHasMediaSources { - [IgnoreDataMember] + [JsonIgnore] public string PrimaryVersionId { get; set; } public string[] AdditionalParts { get; set; } public string[] LocalAlternateVersions { get; set; } public LinkedChild[] LinkedAlternateVersions { get; set; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPositionTicksResume { get @@ -90,7 +90,7 @@ namespace MediaBrowser.Controller.Entities return base.CreatePresentationUniqueKey(); } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsThemeMedia => true; /// <summary> @@ -180,10 +180,10 @@ namespace MediaBrowser.Controller.Entities return IsFileProtocol; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsAddingToPlaylist => true; - [IgnoreDataMember] + [JsonIgnore] public int MediaSourceCount { get @@ -200,10 +200,10 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] public bool IsStacked => AdditionalParts.Length > 0; - [IgnoreDataMember] + [JsonIgnore] public override bool HasLocalAlternateVersions => LocalAlternateVersions.Length > 0; public IEnumerable<Guid> GetAdditionalPartIds() @@ -218,7 +218,7 @@ namespace MediaBrowser.Controller.Entities public static ILiveTvManager LiveTvManager { get; set; } - [IgnoreDataMember] + [JsonIgnore] public override SourceType SourceType { get @@ -247,7 +247,7 @@ namespace MediaBrowser.Controller.Entities return base.CanDelete(); } - [IgnoreDataMember] + [JsonIgnore] public bool IsCompleteMedia { get @@ -261,7 +261,7 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] protected virtual bool EnableDefaultVideoUserDataKeys => true; public override List<string> GetUserDataKeys() @@ -338,7 +338,7 @@ namespace MediaBrowser.Controller.Entities .OrderBy(i => i.SortName); } - [IgnoreDataMember] + [JsonIgnore] public override string ContainingFolderPath { get @@ -360,7 +360,7 @@ namespace MediaBrowser.Controller.Entities } } - [IgnoreDataMember] + [JsonIgnore] public override string FileNameWithoutExtension { get @@ -432,14 +432,14 @@ namespace MediaBrowser.Controller.Entities /// Gets a value indicating whether [is3 D]. /// </summary> /// <value><c>true</c> if [is3 D]; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool Is3D => Video3DFormat.HasValue; /// <summary> /// Gets the type of the media. /// </summary> /// <value>The type of the media.</value> - [IgnoreDataMember] + [JsonIgnore] public override string MediaType => Model.Entities.MediaType.Video; protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, List<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken) diff --git a/MediaBrowser.Controller/Entities/Year.cs b/MediaBrowser.Controller/Entities/Year.cs index 13e82fada..a01ef5c31 100644 --- a/MediaBrowser.Controller/Entities/Year.cs +++ b/MediaBrowser.Controller/Entities/Year.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; -using MediaBrowser.Model.Serialization; +using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; namespace MediaBrowser.Controller.Entities @@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.Entities /// If the item is a folder, it returns the folder itself /// </summary> /// <value>The containing folder path.</value> - [IgnoreDataMember] + [JsonIgnore] public override string ContainingFolderPath => Path; public override double GetDefaultPrimaryImageAspectRatio() @@ -35,7 +35,7 @@ namespace MediaBrowser.Controller.Entities return value; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsAncestors => false; public override bool CanDelete() @@ -72,7 +72,7 @@ namespace MediaBrowser.Controller.Entities return null; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople => false; public static string GetPath(string name) diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 3f8cc0b83..61b2c15ae 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -83,7 +83,7 @@ namespace MediaBrowser.Controller void EnableLoopback(string appName); - WakeOnLanInfo[] GetWakeOnLanInfo(); + IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo(); string ExpandVirtualPath(string path); string ReverseVirtualPath(string path); diff --git a/MediaBrowser.Controller/Library/IUserManager.cs b/MediaBrowser.Controller/Library/IUserManager.cs index 7f7370893..bbedc0442 100644 --- a/MediaBrowser.Controller/Library/IUserManager.cs +++ b/MediaBrowser.Controller/Library/IUserManager.cs @@ -23,6 +23,12 @@ namespace MediaBrowser.Controller.Library IEnumerable<User> Users { get; } /// <summary> + /// Gets the user ids. + /// </summary> + /// <value>The users ids.</value> + IEnumerable<Guid> UsersIds { get; } + + /// <summary> /// Occurs when [user updated]. /// </summary> event EventHandler<GenericEventArgs<User>> UserUpdated; @@ -92,7 +98,7 @@ namespace MediaBrowser.Controller.Library /// <returns>User.</returns> /// <exception cref="ArgumentNullException">name</exception> /// <exception cref="ArgumentException"></exception> - Task<User> CreateUser(string name); + User CreateUser(string name); /// <summary> /// Deletes the user. @@ -101,7 +107,7 @@ namespace MediaBrowser.Controller.Library /// <returns>Task.</returns> /// <exception cref="ArgumentNullException">user</exception> /// <exception cref="ArgumentException"></exception> - Task DeleteUser(User user); + void DeleteUser(User user); /// <summary> /// Resets the password. diff --git a/MediaBrowser.Controller/LiveTv/IListingsProvider.cs b/MediaBrowser.Controller/LiveTv/IListingsProvider.cs index c719ad81c..2ea0a748e 100644 --- a/MediaBrowser.Controller/LiveTv/IListingsProvider.cs +++ b/MediaBrowser.Controller/LiveTv/IListingsProvider.cs @@ -10,10 +10,15 @@ namespace MediaBrowser.Controller.LiveTv public interface IListingsProvider { string Name { get; } + string Type { get; } + Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken); + Task Validate(ListingsProviderInfo info, bool validateLogin, bool validateListings); + Task<List<NameIdPair>> GetLineups(ListingsProviderInfo info, string country, string location); + Task<List<ChannelInfo>> GetChannels(ListingsProviderInfo info, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index f99df6c7c..e02c387e4 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -221,7 +221,7 @@ namespace MediaBrowser.Controller.LiveTv /// <param name="fields">The fields.</param> /// <param name="user">The user.</param> /// <returns>Task.</returns> - Task AddInfoToProgramDto(List<Tuple<BaseItem, BaseItemDto>> programs, ItemFields[] fields, User user = null); + Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem, BaseItemDto)> programs, ItemFields[] fields, User user = null); /// <summary> /// Saves the tuner host. @@ -258,7 +258,7 @@ namespace MediaBrowser.Controller.LiveTv /// <param name="items">The items.</param> /// <param name="options">The options.</param> /// <param name="user">The user.</param> - void AddChannelInfo(List<Tuple<BaseItemDto, LiveTvChannel>> items, DtoOptions options, User user); + void AddChannelInfo(IReadOnlyCollection<(BaseItemDto, LiveTvChannel)> items, DtoOptions options, User user); Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken); Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken); diff --git a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs index 55f47aae9..60391bb83 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvChannel.cs @@ -2,13 +2,13 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.LiveTv { @@ -31,13 +31,13 @@ namespace MediaBrowser.Controller.LiveTv return UnratedItem.LiveTvChannel; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPositionTicksResume => false; - [IgnoreDataMember] + [JsonIgnore] public override SourceType SourceType => SourceType.LiveTV; - [IgnoreDataMember] + [JsonIgnore] public override bool EnableRememberingTrackSelections => false; /// <summary> @@ -52,7 +52,7 @@ namespace MediaBrowser.Controller.LiveTv /// <value>The type of the channel.</value> public ChannelType ChannelType { get; set; } - [IgnoreDataMember] + [JsonIgnore] public override LocationType LocationType => LocationType.Remote; protected override string CreateSortName() @@ -70,7 +70,7 @@ namespace MediaBrowser.Controller.LiveTv return (Number ?? string.Empty) + "-" + (Name ?? string.Empty); } - [IgnoreDataMember] + [JsonIgnore] public override string MediaType => ChannelType == ChannelType.Radio ? Model.Entities.MediaType.Audio : Model.Entities.MediaType.Video; public override string GetClientTypeName() @@ -89,7 +89,7 @@ namespace MediaBrowser.Controller.LiveTv var info = new MediaSourceInfo { - Id = Id.ToString("N"), + Id = Id.ToString("N", CultureInfo.InvariantCulture), Protocol = PathProtocol ?? MediaProtocol.File, MediaStreams = new List<MediaStream>(), Name = Name, @@ -111,7 +111,7 @@ namespace MediaBrowser.Controller.LiveTv protected override string GetInternalMetadataPath(string basePath) { - return System.IO.Path.Combine(basePath, "livetv", Id.ToString("N"), "metadata"); + return System.IO.Path.Combine(basePath, "livetv", Id.ToString("N", CultureInfo.InvariantCulture), "metadata"); } public override bool CanDelete() @@ -119,45 +119,45 @@ namespace MediaBrowser.Controller.LiveTv return false; } - [IgnoreDataMember] + [JsonIgnore] public bool IsMovie { get; set; } /// <summary> /// Gets or sets a value indicating whether this instance is sports. /// </summary> /// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsSports { get; set; } /// <summary> /// Gets or sets a value indicating whether this instance is series. /// </summary> /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsSeries { get; set; } /// <summary> /// Gets or sets a value indicating whether this instance is news. /// </summary> /// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsNews { get; set; } /// <summary> /// Gets or sets a value indicating whether this instance is kids. /// </summary> /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsKids => Tags.Contains("Kids", StringComparer.OrdinalIgnoreCase); - [IgnoreDataMember] + [JsonIgnore] public bool IsRepeat { get; set; } /// <summary> /// Gets or sets the episode title. /// </summary> /// <value>The episode title.</value> - [IgnoreDataMember] + [JsonIgnore] public string EpisodeTitle { get; set; } } } diff --git a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs index 8bde6a5da..13df85aed 100644 --- a/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs +++ b/MediaBrowser.Controller/LiveTv/LiveTvProgram.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; @@ -8,7 +10,6 @@ using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.LiveTv { @@ -62,79 +63,79 @@ namespace MediaBrowser.Controller.LiveTv } } - [IgnoreDataMember] + [JsonIgnore] public override SourceType SourceType => SourceType.LiveTV; /// <summary> /// The start date of the program, in UTC. /// </summary> - [IgnoreDataMember] + [JsonIgnore] public DateTime StartDate { get; set; } /// <summary> /// Gets or sets a value indicating whether this instance is repeat. /// </summary> /// <value><c>true</c> if this instance is repeat; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsRepeat { get; set; } /// <summary> /// Gets or sets the episode title. /// </summary> /// <value>The episode title.</value> - [IgnoreDataMember] + [JsonIgnore] public string EpisodeTitle { get; set; } - [IgnoreDataMember] + [JsonIgnore] public string ShowId { get; set; } /// <summary> /// Gets or sets a value indicating whether this instance is movie. /// </summary> /// <value><c>true</c> if this instance is movie; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsMovie { get; set; } /// <summary> /// Gets or sets a value indicating whether this instance is sports. /// </summary> /// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsSports => Tags.Contains("Sports", StringComparer.OrdinalIgnoreCase); /// <summary> /// Gets or sets a value indicating whether this instance is series. /// </summary> /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsSeries { get; set; } /// <summary> /// Gets or sets a value indicating whether this instance is live. /// </summary> /// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsLive => Tags.Contains("Live", StringComparer.OrdinalIgnoreCase); /// <summary> /// Gets or sets a value indicating whether this instance is news. /// </summary> /// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsNews => Tags.Contains("News", StringComparer.OrdinalIgnoreCase); /// <summary> /// Gets or sets a value indicating whether this instance is kids. /// </summary> /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsKids => Tags.Contains("Kids", StringComparer.OrdinalIgnoreCase); /// <summary> /// Gets or sets a value indicating whether this instance is premiere. /// </summary> /// <value><c>true</c> if this instance is premiere; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsPremiere => Tags.Contains("Premiere", StringComparer.OrdinalIgnoreCase); /// <summary> @@ -142,10 +143,10 @@ namespace MediaBrowser.Controller.LiveTv /// If the item is a folder, it returns the folder itself /// </summary> /// <value>The containing folder path.</value> - [IgnoreDataMember] + [JsonIgnore] public override string ContainingFolderPath => Path; - //[IgnoreDataMember] + //[JsonIgnore] //public override string MediaType //{ // get @@ -154,7 +155,7 @@ namespace MediaBrowser.Controller.LiveTv // } //} - [IgnoreDataMember] + [JsonIgnore] public bool IsAiring { get @@ -165,7 +166,7 @@ namespace MediaBrowser.Controller.LiveTv } } - [IgnoreDataMember] + [JsonIgnore] public bool HasAired { get @@ -188,7 +189,7 @@ namespace MediaBrowser.Controller.LiveTv protected override string GetInternalMetadataPath(string basePath) { - return System.IO.Path.Combine(basePath, "livetv", Id.ToString("N")); + return System.IO.Path.Combine(basePath, "livetv", Id.ToString("N", CultureInfo.InvariantCulture)); } public override bool CanDelete() @@ -196,7 +197,7 @@ namespace MediaBrowser.Controller.LiveTv return false; } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPeople { get @@ -211,7 +212,7 @@ namespace MediaBrowser.Controller.LiveTv } } - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsAncestors => false; private LiveTvOptions GetConfiguration() diff --git a/MediaBrowser.Controller/LiveTv/TimerInfo.cs b/MediaBrowser.Controller/LiveTv/TimerInfo.cs index 2fea6a8cb..46774b2b7 100644 --- a/MediaBrowser.Controller/LiveTv/TimerInfo.cs +++ b/MediaBrowser.Controller/LiveTv/TimerInfo.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using MediaBrowser.Model.LiveTv; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.LiveTv { @@ -129,10 +129,10 @@ namespace MediaBrowser.Controller.LiveTv /// Gets or sets a value indicating whether this instance is live. /// </summary> /// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool IsLive => Tags.Contains("Live", StringComparer.OrdinalIgnoreCase); - [IgnoreDataMember] + [JsonIgnore] public bool IsPremiere => Tags.Contains("Premiere", StringComparer.OrdinalIgnoreCase); public int? ProductionYear { get; set; } diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 01893f1b5..c6bca2518 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -19,6 +19,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> </Project> diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index b9ccc93ef..d896a7aef 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Text; using System.Threading; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Configuration; @@ -22,8 +23,17 @@ namespace MediaBrowser.Controller.MediaEncoding private readonly IMediaEncoder _mediaEncoder; private readonly IFileSystem _fileSystem; private readonly ISubtitleEncoder _subtitleEncoder; - // private readonly IApplicationPaths _appPaths; - // private readonly IAssemblyInfo _assemblyInfo; + + private static readonly string[] _videoProfiles = new[] + { + "ConstrainedBaseline", + "Baseline", + "Extended", + "Main", + "High", + "ProgressiveHigh", + "ConstrainedHigh" + }; public EncodingHelper(IMediaEncoder mediaEncoder, IFileSystem fileSystem, ISubtitleEncoder subtitleEncoder) { @@ -33,9 +43,13 @@ namespace MediaBrowser.Controller.MediaEncoding } public string GetH264Encoder(EncodingJobInfo state, EncodingOptions encodingOptions) - { - var defaultEncoder = "libx264"; + => GetH264OrH265Encoder("libx264", "h264", state, encodingOptions); + + public string GetH265Encoder(EncodingJobInfo state, EncodingOptions encodingOptions) + => GetH264OrH265Encoder("libx265", "hevc", state, encodingOptions); + private string GetH264OrH265Encoder(string defaultEncoder, string hwEncoder, EncodingJobInfo state, EncodingOptions encodingOptions) + { // Only use alternative encoders for video files. // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully // Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this. @@ -45,35 +59,30 @@ namespace MediaBrowser.Controller.MediaEncoding var codecMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { - {"qsv", "h264_qsv"}, - {"h264_qsv", "h264_qsv"}, - {"nvenc", "h264_nvenc"}, - {"amf", "h264_amf"}, - {"omx", "h264_omx"}, - {"h264_v4l2m2m", "h264_v4l2m2m"}, - {"mediacodec", "h264_mediacodec"}, - {"vaapi", "h264_vaapi"} + {"qsv", hwEncoder + "_qsv"}, + {hwEncoder + "_qsv", hwEncoder + "_qsv"}, + {"nvenc", hwEncoder + "_nvenc"}, + {"amf", hwEncoder + "_amf"}, + {"omx", hwEncoder + "_omx"}, + {hwEncoder + "_v4l2m2m", hwEncoder + "_v4l2m2m"}, + {"mediacodec", hwEncoder + "_mediacodec"}, + {"vaapi", hwEncoder + "_vaapi"} }; if (!string.IsNullOrEmpty(hwType) - && encodingOptions.EnableHardwareEncoding && codecMap.ContainsKey(hwType)) + && encodingOptions.EnableHardwareEncoding + && codecMap.ContainsKey(hwType) + && CheckVaapi(state, hwType, encodingOptions)) { - if (CheckVaapi(state, hwType, encodingOptions)) - { - var preferredEncoder = codecMap[hwType]; + var preferredEncoder = codecMap[hwType]; - if (_mediaEncoder.SupportsEncoder(preferredEncoder)) - { - return preferredEncoder; - } + if (_mediaEncoder.SupportsEncoder(preferredEncoder)) + { + return preferredEncoder; } } - } - // Avoid performing a second attempt when the first one - // hasn't tried hardware encoding anyway. - encodingOptions.EnableHardwareEncoding = false; return defaultEncoder; } @@ -98,15 +107,13 @@ namespace MediaBrowser.Controller.MediaEncoding { var videoStream = state.VideoStream; - if (videoStream != null) + // vaapi will throw an error with this input + // [vaapi @ 0x7faed8000960] No VAAPI support for codec mpeg4 profile -99. + if (string.Equals(videoStream?.Codec, "mpeg4", StringComparison.OrdinalIgnoreCase)) { - // vaapi will throw an error with this input - // [vaapi @ 0x7faed8000960] No VAAPI support for codec mpeg4 profile -99. - if (string.Equals(videoStream.Codec, "mpeg4", StringComparison.OrdinalIgnoreCase)) - { - return false; - } + return false; } + return true; } @@ -119,18 +126,27 @@ namespace MediaBrowser.Controller.MediaEncoding if (!string.IsNullOrEmpty(codec)) { + if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) + { + return GetH265Encoder(state, encodingOptions); + } + if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase)) { return GetH264Encoder(state, encodingOptions); } + if (string.Equals(codec, "vpx", StringComparison.OrdinalIgnoreCase)) { return "libvpx"; } + if (string.Equals(codec, "wmv", StringComparison.OrdinalIgnoreCase)) { return "wmv2"; } + if (string.Equals(codec, "theora", StringComparison.OrdinalIgnoreCase)) { return "libtheora"; @@ -149,11 +165,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// <returns>System.String.</returns> public string GetUserAgentParam(EncodingJobInfo state) { - string useragent = null; - - state.RemoteHttpHeaders.TryGetValue("User-Agent", out useragent); - - if (!string.IsNullOrEmpty(useragent)) + if (state.RemoteHttpHeaders.TryGetValue("User-Agent", out string useragent)) { return "-user_agent \"" + useragent + "\""; } @@ -180,50 +192,62 @@ namespace MediaBrowser.Controller.MediaEncoding { return null; } + if (string.Equals(container, "wmv", StringComparison.OrdinalIgnoreCase)) { return null; } + if (string.Equals(container, "mts", StringComparison.OrdinalIgnoreCase)) { return null; } + if (string.Equals(container, "vob", StringComparison.OrdinalIgnoreCase)) { return null; } + if (string.Equals(container, "mpg", StringComparison.OrdinalIgnoreCase)) { return null; } + if (string.Equals(container, "mpeg", StringComparison.OrdinalIgnoreCase)) { return null; } + if (string.Equals(container, "rec", StringComparison.OrdinalIgnoreCase)) { return null; } + if (string.Equals(container, "dvr-ms", StringComparison.OrdinalIgnoreCase)) { return null; } + if (string.Equals(container, "ogm", StringComparison.OrdinalIgnoreCase)) { return null; } + if (string.Equals(container, "divx", StringComparison.OrdinalIgnoreCase)) { return null; } + if (string.Equals(container, "tp", StringComparison.OrdinalIgnoreCase)) { return null; } + if (string.Equals(container, "rmvb", StringComparison.OrdinalIgnoreCase)) { return null; } + if (string.Equals(container, "rtp", StringComparison.OrdinalIgnoreCase)) { return null; @@ -251,10 +275,12 @@ namespace MediaBrowser.Controller.MediaEncoding { return null; } + if (string.Equals(codec, "aac_latm", StringComparison.OrdinalIgnoreCase)) { return null; } + if (string.Equals(codec, "eac3", StringComparison.OrdinalIgnoreCase)) { return null; @@ -279,30 +305,37 @@ namespace MediaBrowser.Controller.MediaEncoding { return "mp3"; } + if (string.Equals(ext, ".aac", StringComparison.OrdinalIgnoreCase)) { return "aac"; } + if (string.Equals(ext, ".wma", StringComparison.OrdinalIgnoreCase)) { return "wma"; } + if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase)) { return "vorbis"; } + if (string.Equals(ext, ".oga", StringComparison.OrdinalIgnoreCase)) { return "vorbis"; } + if (string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase)) { return "vorbis"; } + if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase)) { return "vorbis"; } + if (string.Equals(ext, ".webma", StringComparison.OrdinalIgnoreCase)) { return "vorbis"; @@ -324,14 +357,17 @@ namespace MediaBrowser.Controller.MediaEncoding { return "wmv"; } + if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase)) { return "vpx"; } + if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase)) { return "theora"; } + if (string.Equals(ext, ".m3u8", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ts", StringComparison.OrdinalIgnoreCase)) { return "h264"; @@ -342,19 +378,9 @@ namespace MediaBrowser.Controller.MediaEncoding public int GetVideoProfileScore(string profile) { - var list = new[] - { - "ConstrainedBaseline", - "Baseline", - "Extended", - "Main", - "High", - "ProgressiveHigh", - "ConstrainedHigh" - }; - // strip spaces because they may be stripped out on the query string - return Array.FindIndex(list, t => string.Equals(t, profile.Replace(" ", ""), StringComparison.OrdinalIgnoreCase)); + profile = profile.Replace(" ", ""); + return Array.FindIndex(_videoProfiles, x => string.Equals(x, profile, StringComparison.OrdinalIgnoreCase)); } public string GetInputPathArgument(EncodingJobInfo state) @@ -362,14 +388,19 @@ namespace MediaBrowser.Controller.MediaEncoding var protocol = state.InputProtocol; var mediaPath = state.MediaPath ?? string.Empty; - var inputPath = new[] { mediaPath }; - - if (state.IsInputVideo) + string[] inputPath; + if (state.IsInputVideo + && !(state.VideoType == VideoType.Iso && state.IsoMount == null)) { - if (!(state.VideoType == VideoType.Iso && state.IsoMount == null)) - { - inputPath = MediaEncoderHelpers.GetInputArgument(_fileSystem, mediaPath, state.InputProtocol, state.IsoMount, state.PlayableStreamFileNames); - } + inputPath = MediaEncoderHelpers.GetInputArgument( + _fileSystem, + mediaPath, + state.IsoMount, + state.PlayableStreamFileNames); + } + else + { + inputPath = new[] { mediaPath }; } return _mediaEncoder.GetInputArgument(inputPath, protocol); @@ -388,18 +419,22 @@ namespace MediaBrowser.Controller.MediaEncoding { return "aac -strict experimental"; } + if (string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase)) { return "libmp3lame"; } + if (string.Equals(codec, "vorbis", StringComparison.OrdinalIgnoreCase)) { return "libvorbis"; } + if (string.Equals(codec, "wma", StringComparison.OrdinalIgnoreCase)) { return "wmav2"; } + if (string.Equals(codec, "opus", StringComparison.OrdinalIgnoreCase)) { return "libopus"; @@ -413,54 +448,59 @@ namespace MediaBrowser.Controller.MediaEncoding /// </summary> public string GetInputArgument(EncodingJobInfo state, EncodingOptions encodingOptions) { - var request = state.BaseRequest; + var arg = new StringBuilder(); - var arg = string.Format("-i {0}", GetInputPathArgument(state)); - - if (state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) + if (state.IsVideoRequest + && string.Equals(encodingOptions.HardwareAccelerationType, "vaapi", StringComparison.OrdinalIgnoreCase)) { - if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream) + var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hwOutputFormat = "vaapi"; + + if (hasGraphicalSubs) { - 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((double)state.VideoStream.Width.Value / 16.0 * 9.0); + hwOutputFormat = "yuv420p"; + } - arg += string.Format(" -canvas_size {0}:{1}", state.VideoStream.Width.Value.ToString(CultureInfo.InvariantCulture), height.ToString(CultureInfo.InvariantCulture)); - } + arg.Append("-hwaccel vaapi -hwaccel_output_format ") + .Append(hwOutputFormat) + .Append(" -vaapi_device ") + .Append(encodingOptions.VaapiDevice) + .Append(' '); + } - var subtitlePath = state.SubtitleStream.Path; + arg.Append("-i ") + .Append(GetInputPathArgument(state)); - if (string.Equals(Path.GetExtension(subtitlePath), ".sub", StringComparison.OrdinalIgnoreCase)) - { - var idxFile = Path.ChangeExtension(subtitlePath, ".idx"); - if (File.Exists(idxFile)) - { - subtitlePath = idxFile; - } - } + if (state.SubtitleStream != null + && 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 += " -i \"" + subtitlePath + "\""; + arg.Append(" -canvas_size ") + .Append(state.VideoStream.Width.Value.ToString(CultureInfo.InvariantCulture)) + .Append(':') + .Append(height.ToString(CultureInfo.InvariantCulture)); } - } - if (state.IsVideoRequest) - { - if (GetVideoEncoder(state, encodingOptions).IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1) - { - var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; - var hwOutputFormat = "vaapi"; + var subtitlePath = state.SubtitleStream.Path; - if (hasGraphicalSubs) + if (string.Equals(Path.GetExtension(subtitlePath), ".sub", StringComparison.OrdinalIgnoreCase)) + { + var idxFile = Path.ChangeExtension(subtitlePath, ".idx"); + if (File.Exists(idxFile)) { - hwOutputFormat = "yuv420p"; + subtitlePath = idxFile; } - - arg = "-hwaccel vaapi -hwaccel_output_format " + hwOutputFormat + " -vaapi_device " + encodingOptions.VaapiDevice + " " + arg; } + + arg.Append(" -i \"").Append(subtitlePath).Append('\"'); } - return arg.Trim(); + return arg.ToString(); } /// <summary> @@ -472,8 +512,32 @@ namespace MediaBrowser.Controller.MediaEncoding { var codec = stream.Codec ?? string.Empty; - return codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1 || - codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1; + return codec.IndexOf("264", StringComparison.OrdinalIgnoreCase) != -1 + || codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1; + } + + public bool IsH265(MediaStream stream) + { + var codec = stream.Codec ?? string.Empty; + + return codec.IndexOf("265", StringComparison.OrdinalIgnoreCase) != -1 + || codec.IndexOf("hevc", StringComparison.OrdinalIgnoreCase) != -1; + } + + public string GetBitStreamArgs(MediaStream stream) + { + if (IsH264(stream)) + { + return "-bsf:v h264_mp4toannexb"; + } + else if (IsH265(stream)) + { + return "-bsf:v hevc_mp4toannexb"; + } + else + { + return null; + } } public string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec) @@ -486,26 +550,38 @@ namespace MediaBrowser.Controller.MediaEncoding { // With vpx when crf is used, b:v becomes a max rate // https://trac.ffmpeg.org/wiki/vpxEncodingGuide. - return string.Format(" -maxrate:v {0} -bufsize:v {1} -b:v {0}", bitrate.Value.ToString(_usCulture), (bitrate.Value * 2).ToString(_usCulture)); + return string.Format( + CultureInfo.InvariantCulture, + " -maxrate:v {0} -bufsize:v {1} -b:v {0}", + bitrate.Value, + bitrate.Value * 2); } if (string.Equals(videoCodec, "msmpeg4", StringComparison.OrdinalIgnoreCase)) { - return string.Format(" -b:v {0}", bitrate.Value.ToString(_usCulture)); + return string.Format( + CultureInfo.InvariantCulture, + " -b:v {0}", + bitrate.Value); } - if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(videoCodec, "libx264", StringComparison.OrdinalIgnoreCase) || + string.Equals(videoCodec, "libx265", StringComparison.OrdinalIgnoreCase)) { // h264 - return string.Format(" -maxrate {0} -bufsize {1}", - bitrate.Value.ToString(_usCulture), - (bitrate.Value * 2).ToString(_usCulture)); + return string.Format( + CultureInfo.InvariantCulture, + " -maxrate {0} -bufsize {1}", + bitrate.Value, + bitrate.Value * 2); } // h264 - return string.Format(" -b:v {0} -maxrate {0} -bufsize {1}", - bitrate.Value.ToString(_usCulture), - (bitrate.Value * 2).ToString(_usCulture)); + return string.Format( + CultureInfo.InvariantCulture, + " -b:v {0} -maxrate {0} -bufsize {1}", + bitrate.Value, + bitrate.Value * 2); } return string.Empty; @@ -515,8 +591,10 @@ namespace MediaBrowser.Controller.MediaEncoding { // Clients may direct play higher than level 41, but there's no reason to transcode higher if (double.TryParse(level, NumberStyles.Any, _usCulture, out double requestLevel) - && string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) - && requestLevel > 41) + && requestLevel > 41 + && (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase))) { return "41"; } @@ -536,7 +614,7 @@ namespace MediaBrowser.Controller.MediaEncoding // hls always copies timestamps var setPtsParam = state.CopyTimestamps || state.TranscodingType != TranscodingJobType.Progressive ? string.Empty - : string.Format(",setpts=PTS -{0}/TB", seconds.ToString(_usCulture)); + : string.Format(CultureInfo.InvariantCulture, ",setpts=PTS -{0}/TB", seconds); // TODO // var fallbackFontPath = Path.Combine(_appPaths.ProgramDataPath, "fonts", "DroidSansFallback.ttf"); @@ -620,49 +698,53 @@ namespace MediaBrowser.Controller.MediaEncoding /// <summary> /// Gets the video bitrate to specify on the command line /// </summary> - public string GetVideoQualityParam(EncodingJobInfo state, string videoEncoder, EncodingOptions encodingOptions, string defaultH264Preset) + public string GetVideoQualityParam(EncodingJobInfo state, string videoEncoder, EncodingOptions encodingOptions, string defaultPreset) { var param = string.Empty; var isVc1 = state.VideoStream != null && string.Equals(state.VideoStream.Codec, "vc1", StringComparison.OrdinalIgnoreCase); + var isLibX265 = string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase); - if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase) || isLibX265) { - if (!string.IsNullOrEmpty(encodingOptions.H264Preset)) + if (!string.IsNullOrEmpty(encodingOptions.EncoderPreset)) { - param += "-preset " + encodingOptions.H264Preset; + param += "-preset " + encodingOptions.EncoderPreset; } else { - param += "-preset " + defaultH264Preset; + param += "-preset " + defaultPreset; } - if (encodingOptions.H264Crf >= 0 && encodingOptions.H264Crf <= 51) + int encodeCrf = encodingOptions.H264Crf; + if (isLibX265) { - param += " -crf " + encodingOptions.H264Crf.ToString(CultureInfo.InvariantCulture); + encodeCrf = encodingOptions.H265Crf; } - else + + if (encodeCrf >= 0 && encodeCrf <= 51) { - param += " -crf 23"; + param += " -crf " + encodeCrf.ToString(CultureInfo.InvariantCulture); } - } - - else if (string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase)) - { - param += "-preset fast"; + else + { + string defaultCrf = "23"; + if (isLibX265) + { + defaultCrf = "28"; + } - param += " -crf 28"; + param += " -crf " + defaultCrf; + } } - - // h264 (h264_qsv) - else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)) // h264 (h264_qsv) { string[] valid_h264_qsv = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" }; - if (valid_h264_qsv.Contains(encodingOptions.H264Preset, StringComparer.OrdinalIgnoreCase)) + if (valid_h264_qsv.Contains(encodingOptions.EncoderPreset, StringComparer.OrdinalIgnoreCase)) { - param += "-preset " + encodingOptions.H264Preset; + param += "-preset " + encodingOptions.EncoderPreset; } else { @@ -672,11 +754,10 @@ namespace MediaBrowser.Controller.MediaEncoding param += " -look_ahead 0"; } - - // h264 (h264_nvenc) - else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc) + || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) { - switch (encodingOptions.H264Preset) + switch (encodingOptions.EncoderPreset) { case "veryslow": @@ -705,9 +786,7 @@ namespace MediaBrowser.Controller.MediaEncoding break; } } - - // webm - else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // webm { // Values 0-3, 0 being highest quality but slower var profileScore = 0; @@ -733,18 +812,14 @@ namespace MediaBrowser.Controller.MediaEncoding qmin, qmax); } - else if (string.Equals(videoEncoder, "mpeg4", StringComparison.OrdinalIgnoreCase)) { param += "-mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2"; } - - // asf/wmv - else if (string.Equals(videoEncoder, "wmv2", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(videoEncoder, "wmv2", StringComparison.OrdinalIgnoreCase)) // asf/wmv { param += "-qmin 2"; } - else if (string.Equals(videoEncoder, "msmpeg4", StringComparison.OrdinalIgnoreCase)) { param += "-mbd 2"; @@ -760,21 +835,21 @@ namespace MediaBrowser.Controller.MediaEncoding var targetVideoCodec = state.ActualOutputVideoCodec; - var request = state.BaseRequest; var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault(); // vaapi does not support Baseline profile, force Constrained Baseline in this case, // which is compatible (and ugly) - if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) && - profile != null && profile.ToLowerInvariant().Contains("baseline")) + if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) + && profile != null + && profile.IndexOf("baseline", StringComparison.OrdinalIgnoreCase) != -1) { - profile = "constrained_baseline"; + profile = "constrained_baseline"; } if (!string.IsNullOrEmpty(profile)) { - if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) && - !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) { // not supported by h264_omx param += " -profile:v " + profile; @@ -789,8 +864,9 @@ namespace MediaBrowser.Controller.MediaEncoding // h264_qsv and h264_nvenc expect levels to be expressed as a decimal. libx264 supports decimal and non-decimal format // also needed for libx264 due to https://trac.ffmpeg.org/ticket/3307 - if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) || - string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase)) { switch (level) { @@ -826,10 +902,11 @@ namespace MediaBrowser.Controller.MediaEncoding break; } } - // nvenc doesn't decode with param -level set ?! - else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)) + else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) + || string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) { - //param += ""; + // nvenc doesn't decode with param -level set ?! + // TODO: } else if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)) { @@ -842,10 +919,15 @@ namespace MediaBrowser.Controller.MediaEncoding param += " -x264opts:0 subme=0:me_range=4:rc_lookahead=10:me=dia:no_chroma_me:8x8dct=0:partitions=none"; } - if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) && - !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) && - !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) && - !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase)) + { + // todo + } + + if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) + && !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) { param = "-pix_fmt yuv420p " + param; } @@ -867,12 +949,10 @@ namespace MediaBrowser.Controller.MediaEncoding return false; } - if (videoStream.IsInterlaced) + if (videoStream.IsInterlaced + && state.DeInterlace(videoStream.Codec, false)) { - if (state.DeInterlace(videoStream.Codec, false)) - { - return false; - } + return false; } if (videoStream.IsAnamorphic ?? false) @@ -884,24 +964,23 @@ namespace MediaBrowser.Controller.MediaEncoding } // Can't stream copy if we're burning in subtitles - if (request.SubtitleStreamIndex.HasValue) + if (request.SubtitleStreamIndex.HasValue + && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) { - if (state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) - { - return false; - } + return false; } - if (string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase)) + if (string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase) + && videoStream.IsAVC.HasValue + && !videoStream.IsAVC.Value + && request.RequireAvc) { - if (videoStream.IsAVC.HasValue && !videoStream.IsAVC.Value && request.RequireAvc) - { - return false; - } + return false; } // Source and target codecs must match - if (string.IsNullOrEmpty(videoStream.Codec) || !state.SupportedVideoCodecs.Contains(videoStream.Codec, StringComparer.OrdinalIgnoreCase)) + if (string.IsNullOrEmpty(videoStream.Codec) + || !state.SupportedVideoCodecs.Contains(videoStream.Codec, StringComparer.OrdinalIgnoreCase)) { return false; } @@ -931,21 +1010,17 @@ namespace MediaBrowser.Controller.MediaEncoding } // Video width must fall within requested value - if (request.MaxWidth.HasValue) + if (request.MaxWidth.HasValue + && (!videoStream.Width.HasValue || videoStream.Width.Value > request.MaxWidth.Value)) { - if (!videoStream.Width.HasValue || videoStream.Width.Value > request.MaxWidth.Value) - { - return false; - } + return false; } // Video height must fall within requested value - if (request.MaxHeight.HasValue) + if (request.MaxHeight.HasValue + && (!videoStream.Height.HasValue || videoStream.Height.Value > request.MaxHeight.Value)) { - if (!videoStream.Height.HasValue || videoStream.Height.Value > request.MaxHeight.Value) - { - return false; - } + return false; } // Video framerate must fall within requested value @@ -961,12 +1036,10 @@ namespace MediaBrowser.Controller.MediaEncoding } // Video bitrate must fall within requested value - if (request.VideoBitRate.HasValue) + if (request.VideoBitRate.HasValue + && (!videoStream.BitRate.HasValue || videoStream.BitRate.Value > request.VideoBitRate.Value)) { - if (!videoStream.BitRate.HasValue || videoStream.BitRate.Value > request.VideoBitRate.Value) - { - return false; - } + return false; } var maxBitDepth = state.GetRequestedVideoBitDepth(videoStream.Codec); @@ -979,35 +1052,31 @@ namespace MediaBrowser.Controller.MediaEncoding } var maxRefFrames = state.GetRequestedMaxRefFrames(videoStream.Codec); - if (maxRefFrames.HasValue) + if (maxRefFrames.HasValue + && videoStream.RefFrames.HasValue && videoStream.RefFrames.Value > maxRefFrames.Value) { - if (videoStream.RefFrames.HasValue && videoStream.RefFrames.Value > maxRefFrames.Value) - { - return false; - } + return false; } // If a specific level was requested, the source must match or be less than var level = state.GetRequestedLevel(videoStream.Codec); - if (!string.IsNullOrEmpty(level)) + if (!string.IsNullOrEmpty(level) + && double.TryParse(level, NumberStyles.Any, _usCulture, out var requestLevel)) { - if (double.TryParse(level, NumberStyles.Any, _usCulture, out var requestLevel)) + if (!videoStream.Level.HasValue) { - if (!videoStream.Level.HasValue) - { - //return false; - } + //return false; + } - if (videoStream.Level.HasValue && videoStream.Level.Value > requestLevel) - { - return false; - } + if (videoStream.Level.HasValue && videoStream.Level.Value > requestLevel) + { + return false; } } - if (string.Equals(state.InputContainer, "avi", StringComparison.OrdinalIgnoreCase) && - string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase) && - !(videoStream.IsAVC ?? false)) + if (string.Equals(state.InputContainer, "avi", StringComparison.OrdinalIgnoreCase) + && string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase) + && !(videoStream.IsAVC ?? false)) { // see Coach S01E01 - Kelly and the Professor(0).avi return false; @@ -1016,7 +1085,7 @@ namespace MediaBrowser.Controller.MediaEncoding return request.EnableAutoStreamCopy; } - public bool CanStreamCopyAudio(EncodingJobInfo state, MediaStream audioStream, string[] supportedAudioCodecs) + public bool CanStreamCopyAudio(EncodingJobInfo state, MediaStream audioStream, IEnumerable<string> supportedAudioCodecs) { var request = state.BaseRequest; @@ -1026,16 +1095,16 @@ namespace MediaBrowser.Controller.MediaEncoding } var maxBitDepth = state.GetRequestedAudioBitDepth(audioStream.Codec); - if (maxBitDepth.HasValue) + if (maxBitDepth.HasValue + && audioStream.BitDepth.HasValue + && audioStream.BitDepth.Value > maxBitDepth.Value) { - if (audioStream.BitDepth.HasValue && audioStream.BitDepth.Value > maxBitDepth.Value) - { - return false; - } + return false; } // Source and target codecs must match - if (string.IsNullOrEmpty(audioStream.Codec) || !supportedAudioCodecs.Contains(audioStream.Codec, StringComparer.OrdinalIgnoreCase)) + if (string.IsNullOrEmpty(audioStream.Codec) + || !supportedAudioCodecs.Contains(audioStream.Codec, StringComparer.OrdinalIgnoreCase)) { return false; } @@ -1048,6 +1117,7 @@ namespace MediaBrowser.Controller.MediaEncoding { return false; } + if (audioStream.Channels.Value > channels.Value) { return false; @@ -1061,6 +1131,7 @@ namespace MediaBrowser.Controller.MediaEncoding { return false; } + if (audioStream.SampleRate.Value > request.AudioSampleRate.Value) { return false; @@ -1074,6 +1145,7 @@ namespace MediaBrowser.Controller.MediaEncoding { return false; } + if (audioStream.BitRate.Value > request.AudioBitRate.Value) { return false; @@ -1089,29 +1161,29 @@ namespace MediaBrowser.Controller.MediaEncoding if (videoStream != null) { - var isUpscaling = request.Height.HasValue && videoStream.Height.HasValue && - request.Height.Value > videoStream.Height.Value && request.Width.HasValue && videoStream.Width.HasValue && - request.Width.Value > videoStream.Width.Value; + var isUpscaling = request.Height.HasValue + && videoStream.Height.HasValue + && request.Height.Value > videoStream.Height.Value + && request.Width.HasValue + && videoStream.Width.HasValue + && request.Width.Value > videoStream.Width.Value; // Don't allow bitrate increases unless upscaling - if (!isUpscaling) + if (!isUpscaling && bitrate.HasValue && videoStream.BitRate.HasValue) { - if (bitrate.HasValue && videoStream.BitRate.HasValue) - { - bitrate = GetMinBitrate(videoStream.BitRate.Value, bitrate.Value); - } + bitrate = GetMinBitrate(videoStream.BitRate.Value, bitrate.Value); } - } - - if (bitrate.HasValue) - { - var inputVideoCodec = videoStream.Codec; - bitrate = ScaleBitrate(bitrate.Value, inputVideoCodec, outputVideoCodec); - // If a max bitrate was requested, don't let the scaled bitrate exceed it - if (request.VideoBitRate.HasValue) + if (bitrate.HasValue) { - bitrate = Math.Min(bitrate.Value, request.VideoBitRate.Value); + var inputVideoCodec = videoStream.Codec; + bitrate = ScaleBitrate(bitrate.Value, inputVideoCodec, outputVideoCodec); + + // If a max bitrate was requested, don't let the scaled bitrate exceed it + if (request.VideoBitRate.HasValue) + { + bitrate = Math.Min(bitrate.Value, request.VideoBitRate.Value); + } } } @@ -1127,7 +1199,7 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (sourceBitrate <= 3000000) { - sourceBitrate = Convert.ToInt32(sourceBitrate * 2); + sourceBitrate *= 2; } var bitrate = Math.Min(sourceBitrate, requestedBitrate); @@ -1137,12 +1209,13 @@ namespace MediaBrowser.Controller.MediaEncoding private static double GetVideoBitrateScaleFactor(string codec) { - if (StringHelper.EqualsIgnoreCase(codec, "h265") || - StringHelper.EqualsIgnoreCase(codec, "hevc") || - StringHelper.EqualsIgnoreCase(codec, "vp9")) + if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) + || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase)) { return .5; } + return 1; } @@ -1169,21 +1242,15 @@ namespace MediaBrowser.Controller.MediaEncoding scaleFactor = Math.Max(scaleFactor, 2); } - var newBitrate = scaleFactor * bitrate; - - return Convert.ToInt32(newBitrate); + return Convert.ToInt32(scaleFactor * bitrate); } public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream) { if (request.AudioBitRate.HasValue) { - // Make sure we don't request a bitrate higher than the source - var currentBitrate = audioStream == null ? request.AudioBitRate.Value : audioStream.BitRate ?? request.AudioBitRate.Value; - // Don't encode any higher than this return Math.Min(384000, request.AudioBitRate.Value); - //return Math.Min(currentBitrate, request.AudioBitRate.Value); } return null; @@ -1196,12 +1263,14 @@ namespace MediaBrowser.Controller.MediaEncoding var filters = new List<string>(); // Boost volume to 200% when downsampling from 6ch to 2ch - if (channels.HasValue && channels.Value <= 2) + if (channels.HasValue + && channels.Value <= 2 + && state.AudioStream != null + && state.AudioStream.Channels.HasValue + && state.AudioStream.Channels.Value > 5 + && !encodingOptions.DownMixAudioBoost.Equals(1)) { - if (state.AudioStream != null && state.AudioStream.Channels.HasValue && state.AudioStream.Channels.Value > 5 && !encodingOptions.DownMixAudioBoost.Equals(1)) - { - filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(_usCulture)); - } + filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(_usCulture)); } var isCopyingTimestamps = state.CopyTimestamps || state.TranscodingType != TranscodingJobType.Progressive; @@ -1209,12 +1278,16 @@ namespace MediaBrowser.Controller.MediaEncoding { var seconds = TimeSpan.FromTicks(state.StartTimeTicks ?? 0).TotalSeconds; - filters.Add(string.Format("asetpts=PTS-{0}/TB", Math.Round(seconds).ToString(_usCulture))); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "asetpts=PTS-{0}/TB", + Math.Round(seconds))); } if (filters.Count > 0) { - return "-af \"" + string.Join(",", filters.ToArray()) + "\""; + return "-af \"" + string.Join(",", filters) + "\""; } return string.Empty; @@ -1231,18 +1304,17 @@ namespace MediaBrowser.Controller.MediaEncoding { var request = state.BaseRequest; - var inputChannels = audioStream == null - ? null - : audioStream.Channels; + var inputChannels = audioStream?.Channels; if (inputChannels <= 0) { inputChannels = null; } - int? transcoderChannelLimit = null; var codec = outputAudioCodec ?? string.Empty; + + int? transcoderChannelLimit; if (codec.IndexOf("wma", StringComparison.OrdinalIgnoreCase) != -1) { // wmav2 currently only supports two channel output @@ -1291,6 +1363,7 @@ namespace MediaBrowser.Controller.MediaEncoding { return val2; } + if (!val2.HasValue) { return val1; @@ -1364,7 +1437,10 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.VideoStream != null) { - args += string.Format("-map 0:{0}", state.VideoStream.Index); + args += string.Format( + CultureInfo.InvariantCulture, + "-map 0:{0}", + state.VideoStream.Index); } else { @@ -1374,7 +1450,10 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.AudioStream != null) { - args += string.Format(" -map 0:{0}", state.AudioStream.Index); + args += string.Format( + CultureInfo.InvariantCulture, + " -map 0:{0}", + state.AudioStream.Index); } else @@ -1389,7 +1468,10 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (subtitleMethod == SubtitleDeliveryMethod.Embed) { - args += string.Format(" -map 0:{0}", state.SubtitleStream.Index); + args += string.Format( + CultureInfo.InvariantCulture, + " -map 0:{0}", + state.SubtitleStream.Index); } else if (state.SubtitleStream.IsExternal && !state.SubtitleStream.IsTextSubtitleStream) { @@ -1441,7 +1523,10 @@ namespace MediaBrowser.Controller.MediaEncoding var request = state.BaseRequest; // Add resolution params, if specified - if (request.Width.HasValue || request.Height.HasValue || request.MaxHeight.HasValue || request.MaxWidth.HasValue) + if (request.Width.HasValue + || request.Height.HasValue + || request.MaxHeight.HasValue + || request.MaxWidth.HasValue) { outputSizeParam = GetOutputSizeParam(state, options, outputVideoCodec).TrimEnd('"'); @@ -1463,12 +1548,15 @@ namespace MediaBrowser.Controller.MediaEncoding } } - if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase) && outputSizeParam.Length == 0) + 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) { + if (state.SubtitleStream != null + && state.SubtitleStream.IsTextSubtitleStream + && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode) { outputSizeParam += ",hwmap=mode=read+write+direct"; } } @@ -1477,7 +1565,11 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.VideoStream != null && state.VideoStream.Width.HasValue && state.VideoStream.Height.HasValue) { - videoSizeParam = string.Format("scale={0}:{1}", state.VideoStream.Width.Value.ToString(_usCulture), state.VideoStream.Height.Value.ToString(_usCulture)); + videoSizeParam = string.Format( + CultureInfo.InvariantCulture, + "scale={0}:{1}", + state.VideoStream.Width.Value, + state.VideoStream.Height.Value); videoSizeParam += ":force_original_aspect_ratio=decrease"; } @@ -1490,15 +1582,18 @@ namespace MediaBrowser.Controller.MediaEncoding ? 0 : state.SubtitleStream.Index; - return string.Format(" -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay{3}\"", - mapPrefix.ToString(_usCulture), - subtitleStreamIndex.ToString(_usCulture), - state.VideoStream.Index.ToString(_usCulture), + return string.Format( + CultureInfo.InvariantCulture, + " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay{3}\"", + mapPrefix, + subtitleStreamIndex, + state.VideoStream.Index, outputSizeParam, videoSizeParam); } - private ValueTuple<int?, int?> GetFixedOutputSize(int? videoWidth, + private (int? width, int? height) GetFixedOutputSize( + int? videoWidth, int? videoHeight, int? requestedWidth, int? requestedHeight, @@ -1507,11 +1602,11 @@ namespace MediaBrowser.Controller.MediaEncoding { if (!videoWidth.HasValue && !requestedWidth.HasValue) { - return new ValueTuple<int?, int?>(null, null); + return (null, null); } if (!videoHeight.HasValue && !requestedHeight.HasValue) { - return new ValueTuple<int?, int?>(null, null); + return (null, null); } decimal inputWidth = Convert.ToDecimal(videoWidth ?? requestedWidth); @@ -1531,7 +1626,7 @@ namespace MediaBrowser.Controller.MediaEncoding outputWidth = 2 * Math.Truncate(outputWidth / 2); outputHeight = 2 * Math.Truncate(outputHeight / 2); - return new ValueTuple<int?, int?>(Convert.ToInt32(outputWidth), Convert.ToInt32(outputHeight)); + return (Convert.ToInt32(outputWidth), Convert.ToInt32(outputHeight)); } public List<string> GetScalingFilters(int? videoWidth, @@ -1545,9 +1640,17 @@ namespace MediaBrowser.Controller.MediaEncoding int? requestedMaxHeight) { var filters = new List<string>(); - var fixedOutputSize = GetFixedOutputSize(videoWidth, videoHeight, requestedWidth, requestedHeight, requestedMaxWidth, requestedMaxHeight); - - if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) && fixedOutputSize.Item1.HasValue && fixedOutputSize.Item2.HasValue) + var (width, height) = GetFixedOutputSize( + videoWidth, + videoHeight, + requestedWidth, + requestedHeight, + requestedMaxWidth, + requestedMaxHeight); + + if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) + && width.HasValue + && height.HasValue) { // Work around vaapi's reduced scaling features var scaler = "scale_vaapi"; @@ -1555,15 +1658,26 @@ namespace MediaBrowser.Controller.MediaEncoding // Given the input dimensions (inputWidth, inputHeight), determine the output dimensions // (outputWidth, outputHeight). The user may request precise output dimensions or maximum // output dimensions. Output dimensions are guaranteed to be even. - var outputWidth = fixedOutputSize.Item1.Value; - var outputHeight = fixedOutputSize.Item2.Value; + var outputWidth = width.Value; + var outputHeight = height.Value; - if (!videoWidth.HasValue || outputWidth != videoWidth.Value || !videoHeight.HasValue || outputHeight != videoHeight.Value) + if (!videoWidth.HasValue + || outputWidth != videoWidth.Value + || !videoHeight.HasValue + || outputHeight != videoHeight.Value) { - filters.Add(string.Format("{0}=w={1}:h={2}", scaler, outputWidth.ToString(_usCulture), outputHeight.ToString(_usCulture))); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "{0}=w={1}:h={2}", + scaler, + outputWidth, + outputHeight)); } } - else if ((videoDecoder ?? string.Empty).IndexOf("_cuvid", StringComparison.OrdinalIgnoreCase) != -1 && fixedOutputSize.Item1.HasValue && fixedOutputSize.Item2.HasValue) + else if ((videoDecoder ?? string.Empty).IndexOf("_cuvid", StringComparison.OrdinalIgnoreCase) != -1 + && width.HasValue + && height.HasValue) { // Nothing to do, it's handled as an input resize filter } @@ -1579,7 +1693,12 @@ namespace MediaBrowser.Controller.MediaEncoding var widthParam = requestedWidth.Value.ToString(_usCulture); var heightParam = requestedHeight.Value.ToString(_usCulture); - filters.Add(string.Format("scale=trunc({0}/64)*64:trunc({1}/2)*2", widthParam, heightParam)); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "scale=trunc({0}/64)*64:trunc({1}/2)*2", + widthParam, + heightParam)); } else { @@ -1595,11 +1714,21 @@ namespace MediaBrowser.Controller.MediaEncoding if (isExynosV4L2) { - filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,min({0}\\,{1}*dar))/64)*64:trunc(min(max(iw/dar\\,ih)\\,min({0}/dar\\,{1}))/2)*2", maxWidthParam, maxHeightParam)); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "scale=trunc(min(max(iw\\,ih*dar)\\,min({0}\\,{1}*dar))/64)*64:trunc(min(max(iw/dar\\,ih)\\,min({0}/dar\\,{1}))/2)*2", + maxWidthParam, + maxHeightParam)); } else { - filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,min({0}\\,{1}*dar))/2)*2:trunc(min(max(iw/dar\\,ih)\\,min({0}/dar\\,{1}))/2)*2", maxWidthParam, maxHeightParam)); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "scale=trunc(min(max(iw\\,ih*dar)\\,min({0}\\,{1}*dar))/2)*2:trunc(min(max(iw/dar\\,ih)\\,min({0}/dar\\,{1}))/2)*2", + maxWidthParam, + maxHeightParam)); } } @@ -1615,7 +1744,11 @@ namespace MediaBrowser.Controller.MediaEncoding { var widthParam = requestedWidth.Value.ToString(_usCulture); - filters.Add(string.Format("scale={0}:trunc(ow/a/2)*2", widthParam)); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "scale={0}:trunc(ow/a/2)*2", + widthParam)); } } @@ -1626,11 +1759,19 @@ namespace MediaBrowser.Controller.MediaEncoding if (isExynosV4L2) { - filters.Add(string.Format("scale=trunc(oh*a/64)*64:{0}", heightParam)); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "scale=trunc(oh*a/64)*64:{0}", + heightParam)); } else { - filters.Add(string.Format("scale=trunc(oh*a/2)*2:{0}", heightParam)); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "scale=trunc(oh*a/2)*2:{0}", + heightParam)); } } @@ -1641,11 +1782,19 @@ namespace MediaBrowser.Controller.MediaEncoding if (isExynosV4L2) { - filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,{0})/64)*64:trunc(ow/dar/2)*2", maxWidthParam)); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "scale=trunc(min(max(iw\\,ih*dar)\\,{0})/64)*64:trunc(ow/dar/2)*2", + maxWidthParam)); } else { - filters.Add(string.Format("scale=trunc(min(max(iw\\,ih*dar)\\,{0})/2)*2:trunc(ow/dar/2)*2", maxWidthParam)); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "scale=trunc(min(max(iw\\,ih*dar)\\,{0})/2)*2:trunc(ow/dar/2)*2", + maxWidthParam)); } } @@ -1656,11 +1805,19 @@ namespace MediaBrowser.Controller.MediaEncoding if (isExynosV4L2) { - filters.Add(string.Format("scale=trunc(oh*a/64)*64:min(max(iw/dar\\,ih)\\,{0})", maxHeightParam)); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "scale=trunc(oh*a/64)*64:min(max(iw/dar\\,ih)\\,{0})", + maxHeightParam)); } else { - filters.Add(string.Format("scale=trunc(oh*a/2)*2:min(max(iw/dar\\,ih)\\,{0})", maxHeightParam)); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "scale=trunc(oh*a/2)*2:min(max(iw/dar\\,ih)\\,{0})", + maxHeightParam)); } } } @@ -1670,8 +1827,8 @@ namespace MediaBrowser.Controller.MediaEncoding private string GetFixedSizeScalingFilter(Video3DFormat? threedFormat, int requestedWidth, int requestedHeight) { - var widthParam = requestedWidth.ToString(_usCulture); - var heightParam = requestedHeight.ToString(_usCulture); + var widthParam = requestedWidth.ToString(CultureInfo.InvariantCulture); + var heightParam = requestedHeight.ToString(CultureInfo.InvariantCulture); string filter = null; @@ -1713,13 +1870,14 @@ namespace MediaBrowser.Controller.MediaEncoding } } - return string.Format(filter, widthParam, heightParam); + return string.Format(CultureInfo.InvariantCulture, filter, widthParam, heightParam); } /// <summary> /// If we're going to put a fixed size on the command line, this will calculate it /// </summary> - public string GetOutputSizeParam(EncodingJobInfo state, + public string GetOutputSizeParam( + EncodingJobInfo state, EncodingOptions options, string outputVideoCodec, bool allowTimeStampCopy = true) @@ -1728,24 +1886,45 @@ namespace MediaBrowser.Controller.MediaEncoding var request = state.BaseRequest; + 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"); + + // 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"); + } + } + if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) { filters.Add("format=nv12|vaapi"); filters.Add("hwupload"); } - if (state.DeInterlace("h264", true) && string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + if (state.DeInterlace("h264", true) + && string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) { - filters.Add(string.Format("deinterlace_vaapi")); + filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_vaapi")); } - var videoStream = state.VideoStream; - - if (state.DeInterlace("h264", true) && !string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) + if ((state.DeInterlace("h264", true) || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true)) + && !string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) { - var inputFramerate = videoStream == null ? null : videoStream.RealFrameRate; + 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) @@ -1758,11 +1937,11 @@ namespace MediaBrowser.Controller.MediaEncoding } } - var inputWidth = videoStream == null ? null : videoStream.Width; - var inputHeight = videoStream == null ? null : videoStream.Height; + var inputWidth = videoStream?.Width; + var inputHeight = videoStream?.Height; var threeDFormat = state.MediaSource.Video3DFormat; - var videoDecoder = this.GetHardwareAcceleratedVideoDecoder(state, options); + var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, options); filters.AddRange(GetScalingFilters(inputWidth, inputHeight, threeDFormat, videoDecoder, outputVideoCodec, request.Width, request.Height, request.MaxWidth, request.MaxHeight)); @@ -1780,6 +1959,7 @@ namespace MediaBrowser.Controller.MediaEncoding { filters.Add("hwmap"); } + if (allowTimeStampCopy) { output += " -copyts"; @@ -1788,7 +1968,10 @@ namespace MediaBrowser.Controller.MediaEncoding if (filters.Count > 0) { - output += string.Format(" -vf \"{0}\"", string.Join(",", filters.ToArray())); + output += string.Format( + CultureInfo.InvariantCulture, + " -vf \"{0}\"", + string.Join(",", filters)); } return output; @@ -1836,7 +2019,8 @@ namespace MediaBrowser.Controller.MediaEncoding } } - if (state.AudioStream != null && CanStreamCopyAudio(state, state.AudioStream, state.SupportedAudioCodecs)) + if (state.AudioStream != null + && CanStreamCopyAudio(state, state.AudioStream, state.SupportedAudioCodecs)) { state.OutputAudioCodec = "copy"; } @@ -1853,14 +2037,10 @@ namespace MediaBrowser.Controller.MediaEncoding } public static string GetProbeSizeArgument(int numInputFiles) - { - return numInputFiles > 1 ? "-probesize 1G" : ""; - } + => numInputFiles > 1 ? "-probesize 1G" : ""; public static string GetAnalyzeDurationArgument(int numInputFiles) - { - return numInputFiles > 1 ? "-analyzeduration 200M" : ""; - } + => numInputFiles > 1 ? "-analyzeduration 200M" : ""; public string GetInputModifier(EncodingJobInfo state, EncodingOptions encodingOptions) { @@ -1928,18 +2108,22 @@ namespace MediaBrowser.Controller.MediaEncoding { flags.Add("+igndts"); } + if (state.IgnoreInputIndex) { flags.Add("+ignidx"); } + if (state.GenPtsInput || string.Equals(state.OutputVideoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { flags.Add("+genpts"); } + if (state.DiscardCorruptFramesInput) { flags.Add("+discardcorrupt"); } + if (state.EnableFastSeekInput) { flags.Add("+fastseek"); @@ -1947,24 +2131,33 @@ namespace MediaBrowser.Controller.MediaEncoding if (flags.Count > 0) { - inputModifier += " -fflags " + string.Join("", flags.ToArray()); + inputModifier += " -fflags " + string.Join(string.Empty, flags); } - var videoDecoder = this.GetHardwareAcceleratedVideoDecoder(state, encodingOptions); + var videoDecoder = GetHardwareAcceleratedVideoDecoder(state, encodingOptions); if (!string.IsNullOrEmpty(videoDecoder)) { inputModifier += " " + videoDecoder; - var videoStream = state.VideoStream; - var inputWidth = videoStream == null ? null : videoStream.Width; - var inputHeight = videoStream == null ? null : videoStream.Height; - var request = state.BaseRequest; + if ((videoDecoder ?? string.Empty).IndexOf("_cuvid", StringComparison.OrdinalIgnoreCase) != -1) + { + var videoStream = state.VideoStream; + var inputWidth = videoStream?.Width; + var inputHeight = videoStream?.Height; + var request = state.BaseRequest; - var fixedOutputSize = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight); + var (width, height) = GetFixedOutputSize(inputWidth, inputHeight, request.Width, request.Height, request.MaxWidth, request.MaxHeight); - if ((videoDecoder ?? string.Empty).IndexOf("_cuvid", StringComparison.OrdinalIgnoreCase) != -1 && fixedOutputSize.Item1.HasValue && fixedOutputSize.Item2.HasValue) - { - inputModifier += string.Format(" -resize {0}x{1}", fixedOutputSize.Item1.Value.ToString(_usCulture), fixedOutputSize.Item2.Value.ToString(_usCulture)); + if ((videoDecoder ?? string.Empty).IndexOf("_cuvid", StringComparison.OrdinalIgnoreCase) != -1 + && width.HasValue + && height.HasValue) + { + inputModifier += string.Format( + CultureInfo.InvariantCulture, + " -resize {0}x{1}", + width.Value, + height.Value); + } } } @@ -1973,9 +2166,9 @@ namespace MediaBrowser.Controller.MediaEncoding var outputVideoCodec = GetVideoEncoder(state, encodingOptions); // Important: If this is ever re-enabled, make sure not to use it with wtv because it breaks seeking - if (!string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase) && - state.TranscodingType != TranscodingJobType.Progressive && - state.EnableBreakOnNonKeyFrames(outputVideoCodec)) + if (!string.Equals(state.InputContainer, "wtv", StringComparison.OrdinalIgnoreCase) + && state.TranscodingType != TranscodingJobType.Progressive + && state.EnableBreakOnNonKeyFrames(outputVideoCodec)) { inputModifier += " -noaccurate_seek"; } @@ -1999,14 +2192,16 @@ namespace MediaBrowser.Controller.MediaEncoding } - public void AttachMediaSourceInfo(EncodingJobInfo state, - MediaSourceInfo mediaSource, - string requestedUrl) + public void AttachMediaSourceInfo( + EncodingJobInfo state, + MediaSourceInfo mediaSource, + string requestedUrl) { if (state == null) { throw new ArgumentNullException(nameof(state)); } + if (mediaSource == null) { throw new ArgumentNullException(nameof(mediaSource)); @@ -2064,15 +2259,16 @@ namespace MediaBrowser.Controller.MediaEncoding state.RemoteHttpHeaders = mediaSource.RequiredHttpHeaders; state.ReadInputAtNativeFramerate = mediaSource.ReadAtNativeFramerate; - if (state.ReadInputAtNativeFramerate || - mediaSource.Protocol == MediaProtocol.File && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase)) + if (state.ReadInputAtNativeFramerate + || mediaSource.Protocol == MediaProtocol.File + && string.Equals(mediaSource.Container, "wtv", StringComparison.OrdinalIgnoreCase)) { state.InputVideoSync = "-1"; state.InputAudioSync = "1"; } - if (string.Equals(mediaSource.Container, "wma", StringComparison.OrdinalIgnoreCase) || - string.Equals(mediaSource.Container, "asf", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(mediaSource.Container, "wma", StringComparison.OrdinalIgnoreCase) + || string.Equals(mediaSource.Container, "asf", StringComparison.OrdinalIgnoreCase)) { // Seeing some stuttering when transcoding wma to audio-only HLS state.InputAudioSync = "1"; @@ -2184,7 +2380,7 @@ namespace MediaBrowser.Controller.MediaEncoding return null; } - return this.GetHardwareAcceleratedVideoDecoder(state.MediaSource.VideoType ?? VideoType.VideoFile, state.VideoStream, encodingOptions); + return GetHardwareAcceleratedVideoDecoder(state.MediaSource.VideoType ?? VideoType.VideoFile, state.VideoStream, encodingOptions); } public string GetHardwareAcceleratedVideoDecoder(VideoType videoType, MediaStream videoStream, EncodingOptions encodingOptions) @@ -2197,9 +2393,9 @@ namespace MediaBrowser.Controller.MediaEncoding return null; } - if (videoStream != null && - !string.IsNullOrEmpty(videoStream.Codec) && - !string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType)) + if (videoStream != null + && !string.IsNullOrEmpty(videoStream.Codec) + && !string.IsNullOrEmpty(encodingOptions.HardwareAccelerationType)) { if (string.Equals(encodingOptions.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase)) { @@ -2349,7 +2545,10 @@ namespace MediaBrowser.Controller.MediaEncoding { if (Environment.OSVersion.Platform == PlatformID.Win32NT) { - return "-hwaccel dxva2"; + if(Environment.OSVersion.Version.Major > 6 || (Environment.OSVersion.Version.Major == 6 && Environment.OSVersion.Version.Minor > 1)) + return "-hwaccel d3d11va"; + else + return "-hwaccel dxva2"; } switch (videoStream.Codec.ToLowerInvariant()) @@ -2397,14 +2596,10 @@ namespace MediaBrowser.Controller.MediaEncoding codec = format; } - var args = " -codec:s:0 " + codec; - - args += " -disposition:s:0 default"; - - return args; + return " -codec:s:0 " + codec + " -disposition:s:0 default"; } - public string GetProgressiveVideoFullCommandLine(EncodingJobInfo state, EncodingOptions encodingOptions, string outputPath, string defaultH264Preset) + public string GetProgressiveVideoFullCommandLine(EncodingJobInfo state, EncodingOptions encodingOptions, string outputPath, string defaultPreset) { // Get the output codec name var videoCodec = GetVideoEncoder(state, encodingOptions); @@ -2412,8 +2607,8 @@ namespace MediaBrowser.Controller.MediaEncoding var format = string.Empty; var keyFrame = string.Empty; - if (string.Equals(Path.GetExtension(outputPath), ".mp4", StringComparison.OrdinalIgnoreCase) && - state.BaseRequest.Context == EncodingContext.Streaming) + if (string.Equals(Path.GetExtension(outputPath), ".mp4", StringComparison.OrdinalIgnoreCase) + && state.BaseRequest.Context == EncodingContext.Streaming) { // Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js format = " -f mp4 -movflags frag_keyframe+empty_moov"; @@ -2423,18 +2618,19 @@ namespace MediaBrowser.Controller.MediaEncoding var inputModifier = GetInputModifier(state, encodingOptions); - return string.Format("{0} {1}{2} {3} {4} -map_metadata -1 -map_chapters -1 -threads {5} {6}{7}{8} -y \"{9}\"", + return string.Format( + CultureInfo.InvariantCulture, + "{0} {1}{2} {3} {4} -map_metadata -1 -map_chapters -1 -threads {5} {6}{7}{8} -y \"{9}\"", inputModifier, GetInputArgument(state, encodingOptions), keyFrame, GetMapArgs(state), - GetProgressiveVideoArguments(state, encodingOptions, videoCodec, defaultH264Preset), + GetProgressiveVideoArguments(state, encodingOptions, videoCodec, defaultPreset), threads, GetProgressiveVideoAudioArguments(state, encodingOptions), GetSubtitleEmbedArguments(state), format, - outputPath - ).Trim(); + outputPath).Trim(); } public string GetOutputFFlags(EncodingJobInfo state) @@ -2447,13 +2643,13 @@ namespace MediaBrowser.Controller.MediaEncoding if (flags.Count > 0) { - return " -fflags " + string.Join("", flags.ToArray()); + return " -fflags " + string.Join("", flags); } return string.Empty; } - public string GetProgressiveVideoArguments(EncodingJobInfo state, EncodingOptions encodingOptions, string videoCodec, string defaultH264Preset) + public string GetProgressiveVideoArguments(EncodingJobInfo state, EncodingOptions encodingOptions, string videoCodec, string defaultPreset) { var args = "-codec:v:0 " + videoCodec; @@ -2464,11 +2660,15 @@ namespace MediaBrowser.Controller.MediaEncoding if (string.Equals(videoCodec, "copy", StringComparison.OrdinalIgnoreCase)) { - if (state.VideoStream != null && IsH264(state.VideoStream) && - string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) && - !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) + if (state.VideoStream != null + && string.Equals(state.OutputContainer, "ts", StringComparison.OrdinalIgnoreCase) + && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) { - args += " -bsf:v h264_mp4toannexb"; + string bitStreamArgs = GetBitStreamArgs(state.VideoStream); + if (!string.IsNullOrEmpty(bitStreamArgs)) + { + args += " " + bitStreamArgs; + } } if (state.RunTimeTicks.HasValue && state.BaseRequest.CopyTimestamps) @@ -2483,8 +2683,10 @@ namespace MediaBrowser.Controller.MediaEncoding } else { - var keyFrameArg = string.Format(" -force_key_frames \"expr:gte(t,n_forced*{0})\"", - 5.ToString(_usCulture)); + var keyFrameArg = string.Format( + CultureInfo.InvariantCulture, + " -force_key_frames \"expr:gte(t,n_forced*{0})\"", + 5); args += keyFrameArg; @@ -2505,6 +2707,7 @@ namespace MediaBrowser.Controller.MediaEncoding { args += " -copyts"; } + args += " -avoid_negative_ts disabled -start_at_zero"; } @@ -2514,7 +2717,7 @@ namespace MediaBrowser.Controller.MediaEncoding args += GetGraphicalSubtitleParam(state, encodingOptions, videoCodec); } - var qualityParam = GetVideoQualityParam(state, videoCodec, encodingOptions, defaultH264Preset); + var qualityParam = GetVideoQualityParam(state, videoCodec, encodingOptions, defaultPreset); if (!string.IsNullOrEmpty(qualityParam)) { @@ -2605,39 +2808,22 @@ namespace MediaBrowser.Controller.MediaEncoding } } - var albumCoverInput = string.Empty; - var mapArgs = string.Empty; - var metadata = string.Empty; - var vn = string.Empty; - - var hasArt = !string.IsNullOrEmpty(state.AlbumCoverPath); - hasArt = false; - - if (hasArt) - { - albumCoverInput = " -i \"" + state.AlbumCoverPath + "\""; - mapArgs = " -map 0:a -map 1:v -c:1:v copy"; - metadata = " -metadata:s:v title=\"Album cover\" -metadata:s:v comment=\"Cover(Front)\""; - } - else - { - vn = " -vn"; - } - var threads = GetNumberOfThreads(state, encodingOptions, null); var inputModifier = GetInputModifier(state, encodingOptions); - return string.Format("{0} {1}{7}{8} -threads {2}{3} {4} -id3v2_version 3 -write_id3v1 1{6} -y \"{5}\"", + return string.Format( + CultureInfo.InvariantCulture, + "{0} {1}{7}{8} -threads {2}{3} {4} -id3v2_version 3 -write_id3v1 1{6} -y \"{5}\"", inputModifier, GetInputArgument(state, encodingOptions), threads, - vn, - string.Join(" ", audioTranscodeParams.ToArray()), + " -vn", + string.Join(" ", audioTranscodeParams), outputPath, - metadata, - albumCoverInput, - mapArgs).Trim(); + string.Empty, + string.Empty, + string.Empty).Trim(); } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs index be97c75a2..d64feb2f7 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs @@ -118,14 +118,14 @@ namespace MediaBrowser.Controller.MediaEncoding /// Gets or sets the profile. /// </summary> /// <value>The profile.</value> - [ApiMember(Name = "Profile", Description = "Optional. Specify a specific h264 profile, e.g. main, baseline, high.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + [ApiMember(Name = "Profile", Description = "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string Profile { get; set; } /// <summary> /// Gets or sets the level. /// </summary> /// <value>The level.</value> - [ApiMember(Name = "Level", Description = "Optional. Specify a level for the h264 profile, e.g. 3, 3.1.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + [ApiMember(Name = "Level", Description = "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string Level { get; set; } /// <summary> @@ -212,7 +212,7 @@ namespace MediaBrowser.Controller.MediaEncoding /// Gets or sets the video codec. /// </summary> /// <value>The video codec.</value> - [ApiMember(Name = "VideoCodec", Description = "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h264, mpeg4, theora, vpx, wmv.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] + [ApiMember(Name = "VideoCodec", Description = "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string VideoCodec { get; set; } public string SubtitleCodec { get; set; } diff --git a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs index 7f842c1d0..5cedc3d57 100644 --- a/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs +++ b/MediaBrowser.Controller/MediaEncoding/MediaEncoderHelpers.cs @@ -1,8 +1,8 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using MediaBrowser.Model.IO; -using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.Controller.MediaEncoding { @@ -16,31 +16,26 @@ namespace MediaBrowser.Controller.MediaEncoding /// </summary> /// <param name="fileSystem">The file system.</param> /// <param name="videoPath">The video path.</param> - /// <param name="protocol">The protocol.</param> /// <param name="isoMount">The iso mount.</param> /// <param name="playableStreamFileNames">The playable stream file names.</param> - /// <returns>System.String[][].</returns> - public static string[] GetInputArgument(IFileSystem fileSystem, string videoPath, MediaProtocol protocol, IIsoMount isoMount, string[] playableStreamFileNames) + /// <returns>string[].</returns> + public static string[] GetInputArgument(IFileSystem fileSystem, string videoPath, IIsoMount isoMount, IReadOnlyCollection<string> playableStreamFileNames) { - if (playableStreamFileNames.Length > 0) + if (playableStreamFileNames.Count > 0) { if (isoMount == null) { return GetPlayableStreamFiles(fileSystem, videoPath, playableStreamFileNames); } + return GetPlayableStreamFiles(fileSystem, isoMount.MountedPath, playableStreamFileNames); } return new[] { videoPath }; } - private static string[] GetPlayableStreamFiles(IFileSystem fileSystem, string rootPath, string[] filenames) + private static string[] GetPlayableStreamFiles(IFileSystem fileSystem, string rootPath, IEnumerable<string> filenames) { - if (filenames.Length == 0) - { - return new string[] { }; - } - var allFiles = fileSystem .GetFilePaths(rootPath, true) .ToArray(); diff --git a/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs b/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs index 64c2294e3..29fb81e32 100644 --- a/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs +++ b/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs @@ -1,5 +1,6 @@ using System; using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net { @@ -33,7 +34,7 @@ namespace MediaBrowser.Controller.Net /// <param name="request">The http request wrapper</param> /// <param name="response">The http response wrapper</param> /// <param name="requestDto">The request DTO</param> - public void RequestFilter(IRequest request, IResponse response, object requestDto) + public void RequestFilter(IRequest request, HttpResponse response, object requestDto) { AuthService.Authenticate(request, this); } diff --git a/MediaBrowser.Controller/Playlists/Playlist.cs b/MediaBrowser.Controller/Playlists/Playlist.cs index e83260725..b45043f92 100644 --- a/MediaBrowser.Controller/Playlists/Playlist.cs +++ b/MediaBrowser.Controller/Playlists/Playlist.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Dto; @@ -9,7 +11,6 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; namespace MediaBrowser.Controller.Playlists { @@ -33,7 +34,7 @@ namespace MediaBrowser.Controller.Playlists Shares = Array.Empty<Share>(); } - [IgnoreDataMember] + [JsonIgnore] public bool IsFile => IsPlaylistFile(Path); public static bool IsPlaylistFile(string path) @@ -41,7 +42,7 @@ namespace MediaBrowser.Controller.Playlists return System.IO.Path.HasExtension(path); } - [IgnoreDataMember] + [JsonIgnore] public override string ContainingFolderPath { get @@ -57,19 +58,19 @@ namespace MediaBrowser.Controller.Playlists } } - [IgnoreDataMember] + [JsonIgnore] protected override bool FilterLinkedChildrenPerUser => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsInheritedParentImages => false; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsPlayedStatus => string.Equals(MediaType, "Video", StringComparison.OrdinalIgnoreCase); - [IgnoreDataMember] + [JsonIgnore] public override bool AlwaysScanInternalMetadataPath => true; - [IgnoreDataMember] + [JsonIgnore] public override bool SupportsCumulativeRunTimeTicks => true; public override double GetDefaultPrimaryImageAspectRatio() @@ -192,12 +193,12 @@ namespace MediaBrowser.Controller.Playlists return new[] { item }; } - [IgnoreDataMember] + [JsonIgnore] public override bool IsPreSorted => true; public string PlaylistMediaType { get; set; } - [IgnoreDataMember] + [JsonIgnore] public override string MediaType => PlaylistMediaType; public void SetMediaType(string value) @@ -205,7 +206,7 @@ namespace MediaBrowser.Controller.Playlists PlaylistMediaType = value; } - [IgnoreDataMember] + [JsonIgnore] private bool IsSharedItem { get @@ -239,7 +240,7 @@ namespace MediaBrowser.Controller.Playlists return base.IsVisible(user); } - var userId = user.Id.ToString("N"); + var userId = user.Id.ToString("N", CultureInfo.InvariantCulture); foreach (var share in shares) { if (string.Equals(share.UserId, userId, StringComparison.OrdinalIgnoreCase)) diff --git a/MediaBrowser.Controller/Providers/AlbumInfo.cs b/MediaBrowser.Controller/Providers/AlbumInfo.cs index b0b443fc0..ac6b86c1d 100644 --- a/MediaBrowser.Controller/Providers/AlbumInfo.cs +++ b/MediaBrowser.Controller/Providers/AlbumInfo.cs @@ -9,7 +9,7 @@ namespace MediaBrowser.Controller.Providers /// Gets or sets the album artist. /// </summary> /// <value>The album artist.</value> - public string[] AlbumArtists { get; set; } + public IReadOnlyList<string> AlbumArtists { get; set; } /// <summary> /// Gets or sets the artist provider ids. diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs index f4b915c06..ebff81b7f 100644 --- a/MediaBrowser.Controller/Providers/MetadataResult.cs +++ b/MediaBrowser.Controller/Providers/MetadataResult.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using MediaBrowser.Controller.Entities; namespace MediaBrowser.Controller.Providers @@ -55,7 +56,7 @@ namespace MediaBrowser.Controller.Providers foreach (var i in UserDataList) { - if (string.Equals(userId, i.UserId.ToString("N"), StringComparison.OrdinalIgnoreCase)) + if (string.Equals(userId, i.UserId.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase)) { userData = i; } diff --git a/MediaBrowser.Controller/Providers/MusicVideoInfo.cs b/MediaBrowser.Controller/Providers/MusicVideoInfo.cs index 194b26484..9835351fc 100644 --- a/MediaBrowser.Controller/Providers/MusicVideoInfo.cs +++ b/MediaBrowser.Controller/Providers/MusicVideoInfo.cs @@ -1,7 +1,9 @@ +using System.Collections.Generic; + namespace MediaBrowser.Controller.Providers { public class MusicVideoInfo : ItemLookupInfo { - public string[] Artists { get; set; } + public IReadOnlyList<string> Artists { get; set; } } } diff --git a/MediaBrowser.Controller/Providers/SongInfo.cs b/MediaBrowser.Controller/Providers/SongInfo.cs index 61e950130..50615b0bd 100644 --- a/MediaBrowser.Controller/Providers/SongInfo.cs +++ b/MediaBrowser.Controller/Providers/SongInfo.cs @@ -1,12 +1,15 @@ using System; +using System.Collections.Generic; namespace MediaBrowser.Controller.Providers { public class SongInfo : ItemLookupInfo { - public string[] AlbumArtists { get; set; } + public IReadOnlyList<string> AlbumArtists { get; set; } + public string Album { get; set; } - public string[] Artists { get; set; } + + public IReadOnlyList<string> Artists { get; set; } public SongInfo() { diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index f0e81e8e7..acda6a416 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -1,9 +1,9 @@ using System; using System.Linq; +using System.Text.Json.Serialization; using System.Threading; using MediaBrowser.Controller.Entities; using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; @@ -123,7 +123,7 @@ namespace MediaBrowser.Controller.Session /// Gets or sets the session controller. /// </summary> /// <value>The session controller.</value> - [IgnoreDataMember] + [JsonIgnore] public ISessionController[] SessionControllers { get; set; } /// <summary> diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj index 867b82ede..a8f8da9b8 100644 --- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj +++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj @@ -12,6 +12,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> </Project> diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs index 30a33b729..bd727bcdf 100644 --- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs @@ -158,8 +158,6 @@ namespace MediaBrowser.LocalMetadata.Savers /// <returns>Task.</returns> public static void AddCommonNodes(BaseItem item, XmlWriter writer, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataRepo, IFileSystem fileSystem, IServerConfigurationManager config) { - var writtenProviderIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase); - if (!string.IsNullOrEmpty(item.OfficialRating)) { writer.WriteElementString("ContentRating", item.OfficialRating); @@ -228,7 +226,7 @@ namespace MediaBrowser.LocalMetadata.Savers } } - if (item.RemoteTrailers.Length > 0) + if (item.RemoteTrailers.Count > 0) { writer.WriteStartElement("Trailers"); diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index b00350875..3620abfee 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -1,111 +1,157 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; +using System.Diagnostics; using System.Linq; +using System.Text; using System.Text.RegularExpressions; -using MediaBrowser.Model.Diagnostics; using Microsoft.Extensions.Logging; namespace MediaBrowser.MediaEncoding.Encoder { public class EncoderValidator { - private readonly ILogger _logger; - private readonly IProcessFactory _processFactory; + private const string DefaultEncoderPath = "ffmpeg"; - public EncoderValidator(ILogger logger, IProcessFactory processFactory) + private static readonly string[] requiredDecoders = new[] { - _logger = logger; - _processFactory = processFactory; - } + "mpeg2video", + "h264_qsv", + "hevc_qsv", + "mpeg2_qsv", + "vc1_qsv", + "h264_cuvid", + "hevc_cuvid", + "dts", + "ac3", + "aac", + "mp3", + "h264", + "hevc" + }; - public (IEnumerable<string> decoders, IEnumerable<string> encoders) GetAvailableCoders(string encoderPath) + private static readonly string[] requiredEncoders = new[] + { + "libx264", + "libx265", + "mpeg4", + "msmpeg4", + "libvpx", + "libvpx-vp9", + "aac", + "libmp3lame", + "libopus", + "libvorbis", + "srt", + "h264_nvenc", + "hevc_nvenc", + "h264_qsv", + "hevc_qsv", + "h264_omx", + "hevc_omx", + "h264_vaapi", + "hevc_vaapi", + "h264_v4l2m2m", + "ac3" + }; + + // Try and use the individual library versions to determine a FFmpeg version + // This lookup table is to be maintained with the following command line: + // $ ffmpeg -version | perl -ne ' print "$1=$2.$3," if /^(lib\w+)\s+(\d+)\.\s*(\d+)/' + private static readonly IReadOnlyDictionary<string, Version> _ffmpegVersionMap = new Dictionary<string, Version> { - _logger.LogInformation("Validating media encoder at {EncoderPath}", encoderPath); + { "libavutil=56.31,libavcodec=58.54,libavformat=58.29,libavdevice=58.8,libavfilter=7.57,libswscale=5.5,libswresample=3.5,libpostproc=55.5,", new Version(4, 2) }, + { "libavutil=56.22,libavcodec=58.35,libavformat=58.20,libavdevice=58.5,libavfilter=7.40,libswscale=5.3,libswresample=3.3,libpostproc=55.3,", new Version(4, 1) }, + { "libavutil=56.14,libavcodec=58.18,libavformat=58.12,libavdevice=58.3,libavfilter=7.16,libswscale=5.1,libswresample=3.1,libpostproc=55.1,", new Version(4, 0) }, + { "libavutil=55.78,libavcodec=57.107,libavformat=57.83,libavdevice=57.10,libavfilter=6.107,libswscale=4.8,libswresample=2.9,libpostproc=54.7,", new Version(3, 4) }, + { "libavutil=55.58,libavcodec=57.89,libavformat=57.71,libavdevice=57.6,libavfilter=6.82,libswscale=4.6,libswresample=2.7,libpostproc=54.5,", new Version(3, 3) }, + { "libavutil=55.34,libavcodec=57.64,libavformat=57.56,libavdevice=57.1,libavfilter=6.65,libswscale=4.2,libswresample=2.3,libpostproc=54.1,", new Version(3, 2) }, + { "libavutil=54.31,libavcodec=56.60,libavformat=56.40,libavdevice=56.4,libavfilter=5.40,libswscale=3.1,libswresample=1.2,libpostproc=53.3,", new Version(2, 8) } + }; - var decoders = GetCodecs(encoderPath, Codec.Decoder); - var encoders = GetCodecs(encoderPath, Codec.Encoder); + private readonly ILogger _logger; - _logger.LogInformation("Encoder validation complete"); + private readonly string _encoderPath; - return (decoders, encoders); + public EncoderValidator(ILogger logger, string encoderPath = DefaultEncoderPath) + { + _logger = logger; + _encoderPath = encoderPath; } - public bool ValidateVersion(string encoderAppPath, bool logOutput) + public static Version MinVersion { get; } = new Version(4, 0); + + public static Version MaxVersion { get; } = null; + + public bool ValidateVersion() { string output = null; try { - output = GetProcessOutput(encoderAppPath, "-version"); + output = GetProcessOutput(_encoderPath, "-version"); } catch (Exception ex) { - if (logOutput) - { - _logger.LogError(ex, "Error validating encoder"); - } + _logger.LogError(ex, "Error validating encoder"); } if (string.IsNullOrWhiteSpace(output)) { - if (logOutput) - { - _logger.LogError("FFmpeg validation: The process returned no result"); - } + _logger.LogError("FFmpeg validation: The process returned no result"); return false; } _logger.LogDebug("ffmpeg output: {Output}", output); - if (output.IndexOf("Libav developers", StringComparison.OrdinalIgnoreCase) != -1) + return ValidateVersionInternal(output); + } + + internal bool ValidateVersionInternal(string versionOutput) + { + if (versionOutput.IndexOf("Libav developers", StringComparison.OrdinalIgnoreCase) != -1) { - if (logOutput) - { - _logger.LogError("FFmpeg validation: avconv instead of ffmpeg is not supported"); - } + _logger.LogError("FFmpeg validation: avconv instead of ffmpeg is not supported"); return false; } - // The min and max FFmpeg versions required to run jellyfin successfully - var minRequired = new Version(4, 0); - var maxRequired = new Version(4, 0); - // Work out what the version under test is - var underTest = GetFFmpegVersion(output); + var version = GetFFmpegVersion(versionOutput); - if (logOutput) - { - _logger.LogInformation("FFmpeg validation: Found ffmpeg version {0}", underTest != null ? underTest.ToString() : "unknown"); + _logger.LogInformation("Found ffmpeg version {0}", version != null ? version.ToString() : "unknown"); - if (underTest == null) // Version is unknown + if (version == null) + { + if (MinVersion != null && MaxVersion != null) // Version is unknown { - if (minRequired.Equals(maxRequired)) + if (MinVersion == MaxVersion) { - _logger.LogWarning("FFmpeg validation: We recommend ffmpeg version {0}", minRequired.ToString()); + _logger.LogWarning("FFmpeg validation: We recommend ffmpeg version {0}", MinVersion); } else { - _logger.LogWarning("FFmpeg validation: We recommend a minimum of {0} and maximum of {1}", minRequired.ToString(), maxRequired.ToString()); + _logger.LogWarning("FFmpeg validation: We recommend a minimum of {0} and maximum of {1}", MinVersion, MaxVersion); } } - else if (underTest.CompareTo(minRequired) < 0) // Version is below what we recommend - { - _logger.LogWarning("FFmpeg validation: The minimum recommended ffmpeg version is {0}", minRequired.ToString()); - } - else if (underTest.CompareTo(maxRequired) > 0) // Version is above what we recommend - { - _logger.LogWarning("FFmpeg validation: The maximum recommended ffmpeg version is {0}", maxRequired.ToString()); - } - else // Version is ok - { - _logger.LogInformation("FFmpeg validation: Found suitable ffmpeg version"); - } + + return false; + } + else if (MinVersion != null && version < MinVersion) // Version is below what we recommend + { + _logger.LogWarning("FFmpeg validation: The minimum recommended ffmpeg version is {0}", MinVersion); + return false; + } + else if (MaxVersion != null && version > MaxVersion) // Version is above what we recommend + { + _logger.LogWarning("FFmpeg validation: The maximum recommended ffmpeg version is {0}", MaxVersion); + return false; } - // underTest shall be null if versions is unknown - return (underTest == null) ? false : (underTest.CompareTo(minRequired) >= 0 && underTest.CompareTo(maxRequired) <= 0); + return true; } + public IEnumerable<string> GetDecoders() => GetCodecs(Codec.Decoder); + + public IEnumerable<string> GetEncoders() => GetCodecs(Codec.Encoder); + /// <summary> /// Using the output from "ffmpeg -version" work out the FFmpeg version. /// For pre-built binaries the first line should contain a string like "ffmpeg version x.y", which is easy @@ -115,10 +161,10 @@ namespace MediaBrowser.MediaEncoding.Encoder /// </summary> /// <param name="output"></param> /// <returns></returns> - static private Version GetFFmpegVersion(string output) + internal static Version GetFFmpegVersion(string output) { // For pre-built binaries the FFmpeg version should be mentioned at the very start of the output - var match = Regex.Match(output, @"ffmpeg version (\d+\.\d+)"); + var match = Regex.Match(output, @"^ffmpeg version n?((?:\d+\.?)+)"); if (match.Success) { @@ -126,25 +172,11 @@ namespace MediaBrowser.MediaEncoding.Encoder } else { - // Try and use the individual library versions to determine a FFmpeg version - // This lookup table is to be maintained with the following command line: - // $ ./ffmpeg.exe -version | perl -ne ' print "$1=$2.$3," if /^(lib\w+)\s+(\d+)\.\s*(\d+)/' - var lut = new ReadOnlyDictionary<Version, string> - (new Dictionary<Version, string> - { - { new Version("4.1"), "libavutil=56.22,libavcodec=58.35,libavformat=58.20,libavdevice=58.5,libavfilter=7.40,libswscale=5.3,libswresample=3.3,libpostproc=55.3," }, - { new Version("4.0"), "libavutil=56.14,libavcodec=58.18,libavformat=58.12,libavdevice=58.3,libavfilter=7.16,libswscale=5.1,libswresample=3.1,libpostproc=55.1," }, - { new Version("3.4"), "libavutil=55.78,libavcodec=57.107,libavformat=57.83,libavdevice=57.10,libavfilter=6.107,libswscale=4.8,libswresample=2.9,libpostproc=54.7," }, - { new Version("3.3"), "libavutil=55.58,libavcodec=57.89,libavformat=57.71,libavdevice=57.6,libavfilter=6.82,libswscale=4.6,libswresample=2.7,libpostproc=54.5," }, - { new Version("3.2"), "libavutil=55.34,libavcodec=57.64,libavformat=57.56,libavdevice=57.1,libavfilter=6.65,libswscale=4.2,libswresample=2.3,libpostproc=54.1," }, - { new Version("2.8"), "libavutil=54.31,libavcodec=56.60,libavformat=56.40,libavdevice=56.4,libavfilter=5.40,libswscale=3.1,libswresample=1.2,libpostproc=53.3," } - }); - // Create a reduced version string and lookup key from dictionary - var reducedVersion = GetVersionString(output); + var reducedVersion = GetLibrariesVersionString(output); // Try to lookup the string and return Key, otherwise if not found returns null - return lut.FirstOrDefault(x => x.Value == reducedVersion).Key; + return _ffmpegVersionMap.TryGetValue(reducedVersion, out Version version) ? version : null; } } @@ -154,76 +186,38 @@ namespace MediaBrowser.MediaEncoding.Encoder /// </summary> /// <param name="output"></param> /// <returns></returns> - static private string GetVersionString(string output) + private static string GetLibrariesVersionString(string output) { - string pattern = @"((?<name>lib\w+)\s+(?<major>\d+)\.\s*(?<minor>\d+))"; - RegexOptions options = RegexOptions.Multiline; - - string rc = null; - - foreach (Match m in Regex.Matches(output, pattern, options)) + var rc = new StringBuilder(144); + foreach (Match m in Regex.Matches( + output, + @"((?<name>lib\w+)\s+(?<major>\d+)\.\s*(?<minor>\d+))", + RegexOptions.Multiline)) { - rc += string.Concat(m.Groups["name"], '=', m.Groups["major"], '.', m.Groups["minor"], ','); + rc.Append(m.Groups["name"]) + .Append('=') + .Append(m.Groups["major"]) + .Append('.') + .Append(m.Groups["minor"]) + .Append(','); } - return rc; + return rc.Length == 0 ? null : rc.ToString(); } - private static readonly string[] requiredDecoders = new[] - { - "mpeg2video", - "h264_qsv", - "hevc_qsv", - "mpeg2_qsv", - "vc1_qsv", - "h264_cuvid", - "hevc_cuvid", - "dts", - "ac3", - "aac", - "mp3", - "h264", - "hevc" - }; - - private static readonly string[] requiredEncoders = new[] - { - "libx264", - "libx265", - "mpeg4", - "msmpeg4", - "libvpx", - "libvpx-vp9", - "aac", - "libmp3lame", - "libopus", - "libvorbis", - "srt", - "h264_nvenc", - "hevc_nvenc", - "h264_qsv", - "hevc_qsv", - "h264_omx", - "hevc_omx", - "h264_vaapi", - "hevc_vaapi", - "h264_v4l2m2m", - "ac3" - }; - private enum Codec { Encoder, Decoder } - private IEnumerable<string> GetCodecs(string encoderAppPath, Codec codec) + private IEnumerable<string> GetCodecs(Codec codec) { string codecstr = codec == Codec.Encoder ? "encoders" : "decoders"; string output = null; try { - output = GetProcessOutput(encoderAppPath, "-" + codecstr); + output = GetProcessOutput(_encoderPath, "-" + codecstr); } catch (Exception ex) { @@ -250,45 +244,25 @@ namespace MediaBrowser.MediaEncoding.Encoder private string GetProcessOutput(string path, string arguments) { - IProcess process = _processFactory.Create(new ProcessOptions + using (var process = new Process() { - CreateNoWindow = true, - UseShellExecute = false, - FileName = path, - Arguments = arguments, - IsHidden = true, - ErrorDialog = false, - RedirectStandardOutput = true, - // ffmpeg uses stderr to log info, don't show this - RedirectStandardError = true - }); - - _logger.LogDebug("Running {Path} {Arguments}", path, arguments); - - using (process) - { - process.Start(); - - try + StartInfo = new ProcessStartInfo(path, arguments) { - return process.StandardOutput.ReadToEnd(); + CreateNoWindow = true, + UseShellExecute = false, + WindowStyle = ProcessWindowStyle.Hidden, + ErrorDialog = false, + RedirectStandardOutput = true, + // ffmpeg uses stderr to log info, don't show this + RedirectStandardError = true } - catch - { - _logger.LogWarning("Killing process {Path} {Arguments}", path, arguments); + }) + { + _logger.LogDebug("Running {Path} {Arguments}", path, arguments); - // Hate having to do this - try - { - process.Kill(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error killing process"); - } + process.Start(); - throw; - } + return process.StandardOutput.ReadToEnd(); } } } diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index a8874b6d0..04ff66991 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -114,13 +114,13 @@ namespace MediaBrowser.MediaEncoding.Encoder FFprobePath = Regex.Replace(FFmpegPath, @"[^\/\\]+?(\.[^\/\\\n.]+)?$", @"ffprobe$1"); // Interrogate to understand what coders are supported - var result = new EncoderValidator(_logger, _processFactory).GetAvailableCoders(FFmpegPath); + var validator = new EncoderValidator(_logger, FFmpegPath); - SetAvailableDecoders(result.decoders); - SetAvailableEncoders(result.encoders); + SetAvailableDecoders(validator.GetDecoders()); + SetAvailableEncoders(validator.GetEncoders()); } - _logger.LogInformation("FFmpeg: {0}: {1}", EncoderLocation.ToString(), FFmpegPath ?? string.Empty); + _logger.LogInformation("FFmpeg: {0}: {1}", EncoderLocation, FFmpegPath ?? string.Empty); } /// <summary> @@ -183,11 +183,11 @@ namespace MediaBrowser.MediaEncoding.Encoder { if (File.Exists(path)) { - rc = new EncoderValidator(_logger, _processFactory).ValidateVersion(path, true); + rc = new EncoderValidator(_logger, path).ValidateVersion(); if (!rc) { - _logger.LogWarning("FFmpeg: {0}: Failed version check: {1}", location.ToString(), path); + _logger.LogWarning("FFmpeg: {0}: Failed version check: {1}", location, path); } // ToDo - Enable the ffmpeg validator. At the moment any version can be used. @@ -198,7 +198,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } else { - _logger.LogWarning("FFmpeg: {0}: File not found: {1}", location.ToString(), path); + _logger.LogWarning("FFmpeg: {0}: File not found: {1}", location, path); } } @@ -228,9 +228,9 @@ namespace MediaBrowser.MediaEncoding.Encoder /// </summary> /// <param name="fileName"></param> /// <returns></returns> - private string ExistsOnSystemPath(string filename) + private string ExistsOnSystemPath(string fileName) { - string inJellyfinPath = GetEncoderPathFromDirectory(System.AppContext.BaseDirectory, filename); + string inJellyfinPath = GetEncoderPathFromDirectory(System.AppContext.BaseDirectory, fileName); if (!string.IsNullOrEmpty(inJellyfinPath)) { return inJellyfinPath; @@ -239,13 +239,14 @@ namespace MediaBrowser.MediaEncoding.Encoder foreach (var path in values.Split(Path.PathSeparator)) { - var candidatePath = GetEncoderPathFromDirectory(path, filename); + var candidatePath = GetEncoderPathFromDirectory(path, fileName); if (!string.IsNullOrEmpty(candidatePath)) { return candidatePath; } } + return null; } @@ -303,7 +304,7 @@ namespace MediaBrowser.MediaEncoding.Encoder { var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters; - var inputFiles = MediaEncoderHelpers.GetInputArgument(FileSystem, request.MediaSource.Path, request.MediaSource.Protocol, request.MountedIso, request.PlayableStreamFileNames); + var inputFiles = MediaEncoderHelpers.GetInputArgument(FileSystem, request.MediaSource.Path, request.MountedIso, request.PlayableStreamFileNames); var probeSize = EncodingHelper.GetProbeSizeArgument(inputFiles.Length); string analyzeDuration; diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index c0f92ac4a..264f31f3c 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -3,6 +3,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> <ItemGroup> @@ -18,7 +19,7 @@ <ItemGroup> <PackageReference Include="System.Text.Encoding.CodePages" Version="4.5.1" /> - <PackageReference Include="UTF.Unknown" Version="1.0.0" /> + <PackageReference Include="UTF.Unknown" Version="2.1.0" /> </ItemGroup> </Project> diff --git a/MediaBrowser.MediaEncoding/Properties/AssemblyInfo.cs b/MediaBrowser.MediaEncoding/Properties/AssemblyInfo.cs index a9491374b..7b74cfc89 100644 --- a/MediaBrowser.MediaEncoding/Properties/AssemblyInfo.cs +++ b/MediaBrowser.MediaEncoding/Properties/AssemblyInfo.cs @@ -1,5 +1,6 @@ using System.Reflection; using System.Resources; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -14,6 +15,7 @@ using System.Runtime.InteropServices; [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] +[assembly: InternalsVisibleTo("Jellyfin.MediaEncoding.Tests")] // Setting ComVisible to false makes the types in this assembly not visible // to COM components. If you need to access a type in this assembly from diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index 9ddfb9b01..d5fa76c3a 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -506,12 +506,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles if (failed) { - var msg = string.Format("ffmpeg subtitle conversion failed for {Path}", inputPath); + _logger.LogError("ffmpeg subtitle conversion failed for {Path}", inputPath); - _logger.LogError(msg); - - throw new Exception(msg); + throw new Exception( + string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle conversion failed for {0}", inputPath)); } + await SetAssFont(outputPath).ConfigureAwait(false); _logger.LogInformation("ffmpeg subtitle conversion succeeded for {Path}", inputPath); diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index 285ff4ba5..9ae10d980 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -18,7 +18,8 @@ namespace MediaBrowser.Model.Configuration public string EncoderAppPathDisplay { get; set; } public string VaapiDevice { get; set; } public int H264Crf { get; set; } - public string H264Preset { get; set; } + public int H265Crf { get; set; } + public string EncoderPreset { get; set; } public string DeinterlaceMethod { get; set; } public bool EnableHardwareEncoding { get; set; } public bool EnableSubtitleExtraction { get; set; } @@ -34,6 +35,7 @@ namespace MediaBrowser.Model.Configuration // This is a DRM device that is almost guaranteed to be there on every intel platform, plus it's the default one in ffmpeg if you don't specify anything VaapiDevice = "/dev/dri/renderD128"; H264Crf = 23; + H265Crf = 28; EnableHardwareEncoding = true; EnableSubtitleExtraction = true; HardwareDecodingCodecs = new string[] { "h264", "vc1" }; diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index 2673597ca..b8abe49e3 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -10,6 +10,7 @@ namespace MediaBrowser.Model.Configuration { public const int DefaultHttpPort = 8096; public const int DefaultHttpsPort = 8920; + private string _baseUrl; /// <summary> /// Gets or sets a value indicating whether [enable u pn p]. @@ -162,7 +163,33 @@ namespace MediaBrowser.Model.Configuration public bool SkipDeserializationForBasicTypes { get; set; } public string ServerName { get; set; } - public string WanDdns { get; set; } + public string BaseUrl + { + get => _baseUrl; + set + { + // Normalize the start of the string + if (string.IsNullOrWhiteSpace(value)) + { + // If baseUrl is empty, set an empty prefix string + value = string.Empty; + } + else if (!value.StartsWith("/")) + { + // If baseUrl was not configured with a leading slash, append one for consistency + value = "/" + value; + } + + // Normalize the end of the string + if (value.EndsWith("/")) + { + // If baseUrl was configured with a trailing slash, remove it for consistency + value = value.Remove(value.Length - 1); + } + + _baseUrl = value; + } + } public string UICulture { get; set; } @@ -243,6 +270,7 @@ namespace MediaBrowser.Model.Configuration SortRemoveCharacters = new[] { ",", "&", "-", "{", "}", "'" }; SortRemoveWords = new[] { "the", "a", "an" }; + BaseUrl = string.Empty; UICulture = "en-US"; MetadataOptions = new[] diff --git a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs index 5988112c2..ce6493232 100644 --- a/MediaBrowser.Model/Cryptography/ICryptoProvider.cs +++ b/MediaBrowser.Model/Cryptography/ICryptoProvider.cs @@ -1,22 +1,23 @@ -using System; -using System.IO; using System.Collections.Generic; namespace MediaBrowser.Model.Cryptography { public interface ICryptoProvider { - Guid GetMD5(string str); - byte[] ComputeMD5(Stream str); - byte[] ComputeMD5(byte[] bytes); - byte[] ComputeSHA1(byte[] bytes); + string DefaultHashMethod { get; } + IEnumerable<string> GetSupportedHashMethods(); + byte[] ComputeHash(string HashMethod, byte[] bytes); + byte[] ComputeHashWithDefaultMethod(byte[] bytes); + byte[] ComputeHash(string HashMethod, byte[] bytes, byte[] salt); + byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt); - byte[] ComputeHash(PasswordHash hash); + byte[] GenerateSalt(); - string DefaultHashMethod { get; } + + byte[] GenerateSalt(int length); } } diff --git a/MediaBrowser.Model/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs deleted file mode 100644 index f15b27d32..000000000 --- a/MediaBrowser.Model/Cryptography/PasswordHash.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace MediaBrowser.Model.Cryptography -{ - public class PasswordHash - { - // Defined from this hash storage spec - // https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md - // $<id>[$<param>=<value>(,<param>=<value>)*][$<salt>[$<hash>]] - // with one slight amendment to ease the transition, we're writing out the bytes in hex - // rather than making them a BASE64 string with stripped padding - - private string _id; - - private Dictionary<string, string> _parameters = new Dictionary<string, string>(); - - private string _salt; - - private byte[] _saltBytes; - - private string _hash; - - private byte[] _hashBytes; - - public string Id { get => _id; set => _id = value; } - - public Dictionary<string, string> Parameters { get => _parameters; set => _parameters = value; } - - public string Salt { get => _salt; set => _salt = value; } - - public byte[] SaltBytes { get => _saltBytes; set => _saltBytes = value; } - - public string Hash { get => _hash; set => _hash = value; } - - public byte[] HashBytes { get => _hashBytes; set => _hashBytes = value; } - - public PasswordHash(string storageString) - { - string[] splitted = storageString.Split('$'); - _id = splitted[1]; - if (splitted[2].Contains("=")) - { - foreach (string paramset in (splitted[2].Split(','))) - { - if (!string.IsNullOrEmpty(paramset)) - { - string[] fields = paramset.Split('='); - if (fields.Length == 2) - { - _parameters.Add(fields[0], fields[1]); - } - else - { - throw new Exception($"Malformed parameter in password hash string {paramset}"); - } - } - } - if (splitted.Length == 5) - { - _salt = splitted[3]; - _saltBytes = ConvertFromByteString(_salt); - _hash = splitted[4]; - _hashBytes = ConvertFromByteString(_hash); - } - else - { - _salt = string.Empty; - _hash = splitted[3]; - _hashBytes = ConvertFromByteString(_hash); - } - } - else - { - if (splitted.Length == 4) - { - _salt = splitted[2]; - _saltBytes = ConvertFromByteString(_salt); - _hash = splitted[3]; - _hashBytes = ConvertFromByteString(_hash); - } - else - { - _salt = string.Empty; - _hash = splitted[2]; - _hashBytes = ConvertFromByteString(_hash); - } - - } - - } - - public PasswordHash(ICryptoProvider cryptoProvider) - { - _id = cryptoProvider.DefaultHashMethod; - _saltBytes = cryptoProvider.GenerateSalt(); - _salt = ConvertToByteString(SaltBytes); - } - - public static byte[] ConvertFromByteString(string byteString) - { - byte[] bytes = new byte[byteString.Length / 2]; - for (int i = 0; i < byteString.Length; i += 2) - { - // TODO: NetStandard2.1 switch this to use a span instead of a substring. - bytes[i / 2] = Convert.ToByte(byteString.Substring(i, 2), 16); - } - - return bytes; - } - - public static string ConvertToByteString(byte[] bytes) - { - return BitConverter.ToString(bytes).Replace("-", ""); - } - - private string SerializeParameters() - { - string returnString = string.Empty; - foreach (var KVP in _parameters) - { - returnString += $",{KVP.Key}={KVP.Value}"; - } - - if ((!string.IsNullOrEmpty(returnString)) && returnString[0] == ',') - { - returnString = returnString.Remove(0, 1); - } - - return returnString; - } - - public override string ToString() - { - string outString = "$" + _id; - string paramstring = SerializeParameters(); - if (!string.IsNullOrEmpty(paramstring)) - { - outString += $"${paramstring}"; - } - - if (!string.IsNullOrEmpty(_salt)) - { - outString += $"${_salt}"; - } - - outString += $"${_hash}"; - return outString; - } - } - -} diff --git a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs index e52951dd0..2333fa7a0 100644 --- a/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs +++ b/MediaBrowser.Model/Dlna/ContentFeatureBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.Model.Dlna @@ -81,17 +82,20 @@ namespace MediaBrowser.Model.Dlna // flagValue = flagValue | DlnaFlags.TimeBasedSeek; //} - string dlnaflags = string.Format(";DLNA.ORG_FLAGS={0}", - DlnaMaps.FlagsToString(flagValue)); + string dlnaflags = string.Format( + CultureInfo.InvariantCulture, + ";DLNA.ORG_FLAGS={0}", + DlnaMaps.FlagsToString(flagValue)); - ResponseProfile mediaProfile = _profile.GetAudioMediaProfile(container, + ResponseProfile mediaProfile = _profile.GetAudioMediaProfile( + container, audioCodec, audioChannels, audioBitrate, audioSampleRate, audioBitDepth); - string orgPn = mediaProfile == null ? null : mediaProfile.OrgPn; + string orgPn = mediaProfile?.OrgPn; if (string.IsNullOrEmpty(orgPn)) { diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index b382d9d4a..4da5508b4 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -234,7 +234,7 @@ namespace MediaBrowser.Model.Dto /// Gets or sets the trailer urls. /// </summary> /// <value>The trailer urls.</value> - public MediaUrl[] RemoteTrailers { get; set; } + public IReadOnlyCollection<MediaUrl> RemoteTrailers { get; set; } /// <summary> /// Gets or sets the provider ids. @@ -386,7 +386,7 @@ namespace MediaBrowser.Model.Dto /// Gets or sets the artists. /// </summary> /// <value>The artists.</value> - public string[] Artists { get; set; } + public IReadOnlyList<string> Artists { get; set; } /// <summary> /// Gets or sets the artist items. diff --git a/MediaBrowser.Model/Dto/BaseItemPerson.cs b/MediaBrowser.Model/Dto/BaseItemPerson.cs index 7011ff8ea..270a4683a 100644 --- a/MediaBrowser.Model/Dto/BaseItemPerson.cs +++ b/MediaBrowser.Model/Dto/BaseItemPerson.cs @@ -1,4 +1,4 @@ -using MediaBrowser.Model.Serialization; +using System.Text.Json.Serialization; namespace MediaBrowser.Model.Dto { @@ -41,7 +41,7 @@ namespace MediaBrowser.Model.Dto /// Gets a value indicating whether this instance has primary image. /// </summary> /// <value><c>true</c> if this instance has primary image; otherwise, <c>false</c>.</value> - [IgnoreDataMember] + [JsonIgnore] public bool HasPrimaryImage => PrimaryImageTag != null; } } diff --git a/MediaBrowser.Model/Dto/MediaSourceInfo.cs b/MediaBrowser.Model/Dto/MediaSourceInfo.cs index 92e40fb01..5bdc4809a 100644 --- a/MediaBrowser.Model/Dto/MediaSourceInfo.cs +++ b/MediaBrowser.Model/Dto/MediaSourceInfo.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; -using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Session; namespace MediaBrowser.Model.Dto @@ -108,7 +108,7 @@ namespace MediaBrowser.Model.Dto } } - [IgnoreDataMember] + [JsonIgnore] public TranscodeReason[] TranscodeReasons { get; set; } public int? DefaultAudioStreamIndex { get; set; } @@ -148,7 +148,7 @@ namespace MediaBrowser.Model.Dto return null; } - [IgnoreDataMember] + [JsonIgnore] public MediaStream VideoStream { get diff --git a/MediaBrowser.Model/Dto/RecommendationDto.cs b/MediaBrowser.Model/Dto/RecommendationDto.cs index 0a890573b..acfb85e9b 100644 --- a/MediaBrowser.Model/Dto/RecommendationDto.cs +++ b/MediaBrowser.Model/Dto/RecommendationDto.cs @@ -1,10 +1,11 @@ using System; +using System.Collections.Generic; namespace MediaBrowser.Model.Dto { public class RecommendationDto { - public BaseItemDto[] Items { get; set; } + public IReadOnlyCollection<BaseItemDto> Items { get; set; } public RecommendationType RecommendationType { get; set; } diff --git a/MediaBrowser.Model/Globalization/CultureDto.cs b/MediaBrowser.Model/Globalization/CultureDto.cs index f229f2055..a213d4147 100644 --- a/MediaBrowser.Model/Globalization/CultureDto.cs +++ b/MediaBrowser.Model/Globalization/CultureDto.cs @@ -38,6 +38,7 @@ namespace MediaBrowser.Model.Globalization { return vals[0]; } + return null; } } diff --git a/MediaBrowser.Model/Globalization/ILocalizationManager.cs b/MediaBrowser.Model/Globalization/ILocalizationManager.cs index a9ce60a2a..91d946db8 100644 --- a/MediaBrowser.Model/Globalization/ILocalizationManager.cs +++ b/MediaBrowser.Model/Globalization/ILocalizationManager.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Globalization; -using System.Threading.Tasks; using MediaBrowser.Model.Entities; namespace MediaBrowser.Model.Globalization @@ -13,23 +12,26 @@ namespace MediaBrowser.Model.Globalization /// <summary> /// Gets the cultures. /// </summary> - /// <returns>IEnumerable{CultureDto}.</returns> - CultureDto[] GetCultures(); + /// <returns><see cref="IEnumerable{CultureDto}" />.</returns> + IEnumerable<CultureDto> GetCultures(); + /// <summary> /// Gets the countries. /// </summary> - /// <returns>IEnumerable{CountryInfo}.</returns> - Task<CountryInfo[]> GetCountries(); + /// <returns><see cref="IEnumerable{CountryInfo}" />.</returns> + IEnumerable<CountryInfo> GetCountries(); + /// <summary> /// Gets the parental ratings. /// </summary> - /// <returns>IEnumerable{ParentalRating}.</returns> + /// <returns><see cref="IEnumerable{ParentalRating}" />.</returns> IEnumerable<ParentalRating> GetParentalRatings(); + /// <summary> /// Gets the rating level. /// </summary> /// <param name="rating">The rating.</param> - /// <returns>System.Int32.</returns> + /// <returns><see cref="int" /> or <c>null</c>.</returns> int? GetRatingLevel(string rating); /// <summary> @@ -37,7 +39,7 @@ namespace MediaBrowser.Model.Globalization /// </summary> /// <param name="phrase">The phrase.</param> /// <param name="culture">The culture.</param> - /// <returns>System.String.</returns> + /// <returns><see cref="string" />.</returns> string GetLocalizedString(string phrase, string culture); /// <summary> @@ -50,13 +52,22 @@ namespace MediaBrowser.Model.Globalization /// <summary> /// Gets the localization options. /// </summary> - /// <returns>IEnumerable{LocalizatonOption}.</returns> - LocalizationOption[] GetLocalizationOptions(); - - string NormalizeFormKD(string text); + /// <returns><see cref="IEnumerable{LocalizatonOption}" />.</returns> + IEnumerable<LocalizationOption> GetLocalizationOptions(); + /// <summary> + /// Checks if the string contains a character with the specified unicode category. + /// </summary> + /// <param name="value">The string.</param> + /// <param name="category">The unicode category.</param> + /// <returns>Wether or not the string contains a character with the specified unicode category.</returns> bool HasUnicodeCategory(string value, UnicodeCategory category); + /// <summary> + /// Returns the correct <see cref="Cultureinfo" /> for the given language. + /// </summary> + /// <param name="language">The language.</param> + /// <returns>The correct <see cref="Cultureinfo" /> for the given language.</returns> CultureDto FindLanguageInfo(string language); } } diff --git a/MediaBrowser.Model/IO/IIsoManager.cs b/MediaBrowser.Model/IO/IIsoManager.cs index 24b6e5f05..eb0cb4bfb 100644 --- a/MediaBrowser.Model/IO/IIsoManager.cs +++ b/MediaBrowser.Model/IO/IIsoManager.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace MediaBrowser.Model.IO { - public interface IIsoManager : IDisposable + public interface IIsoManager { /// <summary> /// Mounts the specified iso path. diff --git a/MediaBrowser.Model/IO/IIsoMounter.cs b/MediaBrowser.Model/IO/IIsoMounter.cs index f0153a928..766a9e4e6 100644 --- a/MediaBrowser.Model/IO/IIsoMounter.cs +++ b/MediaBrowser.Model/IO/IIsoMounter.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; namespace MediaBrowser.Model.IO { - public interface IIsoMounter : IDisposable + public interface IIsoMounter { /// <summary> /// Mounts the specified iso path. diff --git a/MediaBrowser.Model/IO/StreamDefaults.cs b/MediaBrowser.Model/IO/StreamDefaults.cs index bef20e74f..1dc29e06e 100644 --- a/MediaBrowser.Model/IO/StreamDefaults.cs +++ b/MediaBrowser.Model/IO/StreamDefaults.cs @@ -13,6 +13,6 @@ namespace MediaBrowser.Model.IO /// <summary> /// The default file stream buffer size /// </summary> - public const int DefaultFileStreamBufferSize = 81920; + public const int DefaultFileStreamBufferSize = 4096; } } diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 3de2cca2d..3ed319a0d 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -10,11 +10,13 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.2.0" /> + <PackageReference Include="System.Text.Json" Version="4.6.0" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.Model/Net/ISocketFactory.cs b/MediaBrowser.Model/Net/ISocketFactory.cs index e58f4cc14..2f857f1af 100644 --- a/MediaBrowser.Model/Net/ISocketFactory.cs +++ b/MediaBrowser.Model/Net/ISocketFactory.cs @@ -17,8 +17,6 @@ namespace MediaBrowser.Model.Net ISocket CreateUdpBroadcastSocket(int localPort); - ISocket CreateTcpSocket(IPAddress remoteAddress, int remotePort); - /// <summary> /// Creates a new unicast socket using the specified local port number. /// </summary> @@ -35,14 +33,4 @@ namespace MediaBrowser.Model.Net Stream CreateNetworkStream(ISocket socket, bool ownsSocket); } - - public enum SocketType - { - Stream - } - - public enum ProtocolType - { - Tcp - } } diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs index d7bf956bb..de5e58d22 100644 --- a/MediaBrowser.Model/Net/MimeTypes.cs +++ b/MediaBrowser.Model/Net/MimeTypes.cs @@ -120,6 +120,7 @@ namespace MediaBrowser.Model.Net { ".m4b", "audio/m4b" }, { ".xsp", "audio/xsp" }, { ".dsp", "audio/dsp" }, + { ".flac", "audio/flac" }, }; private static readonly Dictionary<string, string> _extensionLookup = CreateExtensionLookup(); diff --git a/MediaBrowser.Model/Plugins/BasePluginConfiguration.cs b/MediaBrowser.Model/Plugins/BasePluginConfiguration.cs index 39db22133..ac540782c 100644 --- a/MediaBrowser.Model/Plugins/BasePluginConfiguration.cs +++ b/MediaBrowser.Model/Plugins/BasePluginConfiguration.cs @@ -1,7 +1,7 @@ namespace MediaBrowser.Model.Plugins { /// <summary> - /// Class BasePluginConfiguration + /// Class BasePluginConfiguration. /// </summary> public class BasePluginConfiguration { diff --git a/MediaBrowser.Model/Querying/QueryResult.cs b/MediaBrowser.Model/Querying/QueryResult.cs index e81f2b868..c007a45d6 100644 --- a/MediaBrowser.Model/Querying/QueryResult.cs +++ b/MediaBrowser.Model/Querying/QueryResult.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; + namespace MediaBrowser.Model.Querying { public class QueryResult<T> @@ -6,7 +9,7 @@ namespace MediaBrowser.Model.Querying /// Gets or sets the items. /// </summary> /// <value>The items.</value> - public T[] Items { get; set; } + public IReadOnlyList<T> Items { get; set; } /// <summary> /// The total number of records available @@ -16,7 +19,7 @@ namespace MediaBrowser.Model.Querying public QueryResult() { - Items = new T[] { }; + Items = Array.Empty<T>(); } } } diff --git a/MediaBrowser.Model/Search/SearchHint.cs b/MediaBrowser.Model/Search/SearchHint.cs index 8a187f18e..8f4824903 100644 --- a/MediaBrowser.Model/Search/SearchHint.cs +++ b/MediaBrowser.Model/Search/SearchHint.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace MediaBrowser.Model.Search { @@ -111,6 +112,7 @@ namespace MediaBrowser.Model.Search /// </summary> /// <value>The album.</value> public string Album { get; set; } + public Guid AlbumId { get; set; } /// <summary> @@ -123,7 +125,7 @@ namespace MediaBrowser.Model.Search /// Gets or sets the artists. /// </summary> /// <value>The artists.</value> - public string[] Artists { get; set; } + public IReadOnlyList<string> Artists { get; set; } /// <summary> /// Gets or sets the song count. diff --git a/MediaBrowser.Model/Serialization/IgnoreDataMemberAttribute.cs b/MediaBrowser.Model/Serialization/IgnoreDataMemberAttribute.cs deleted file mode 100644 index b43949fe3..000000000 --- a/MediaBrowser.Model/Serialization/IgnoreDataMemberAttribute.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace MediaBrowser.Model.Serialization -{ - [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] - public sealed class IgnoreDataMemberAttribute : Attribute - { - public IgnoreDataMemberAttribute() - { - } - } -} diff --git a/MediaBrowser.Model/Services/IHasRequestFilter.cs b/MediaBrowser.Model/Services/IHasRequestFilter.cs index d4e6aa8e0..81a2dba69 100644 --- a/MediaBrowser.Model/Services/IHasRequestFilter.cs +++ b/MediaBrowser.Model/Services/IHasRequestFilter.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Http; + namespace MediaBrowser.Model.Services { public interface IHasRequestFilter @@ -15,6 +17,6 @@ namespace MediaBrowser.Model.Services /// <param name="req">The http request wrapper</param> /// <param name="res">The http response wrapper</param> /// <param name="requestDto">The request DTO</param> - void RequestFilter(IRequest req, IResponse res, object requestDto); + void RequestFilter(IRequest req, HttpResponse res, object requestDto); } } diff --git a/MediaBrowser.Model/Services/IRequest.cs b/MediaBrowser.Model/Services/IRequest.cs index 4f6ddb476..7a4152698 100644 --- a/MediaBrowser.Model/Services/IRequest.cs +++ b/MediaBrowser.Model/Services/IRequest.cs @@ -1,16 +1,13 @@ using System; using System.Collections.Generic; using System.IO; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.IO; using Microsoft.AspNetCore.Http; namespace MediaBrowser.Model.Services { public interface IRequest { - IResponse Response { get; } + HttpResponse Response { get; } /// <summary> /// The name of the service being called (e.g. Request DTO Name) @@ -23,11 +20,6 @@ namespace MediaBrowser.Model.Services string Verb { get; } /// <summary> - /// The Request DTO, after it has been deserialized. - /// </summary> - object Dto { get; set; } - - /// <summary> /// The request ContentType /// </summary> string ContentType { get; } @@ -50,8 +42,6 @@ namespace MediaBrowser.Model.Services IQueryCollection QueryString { get; } - Task<QueryParamCollection> GetFormData(); - string RawUrl { get; } string AbsoluteUri { get; } @@ -75,11 +65,6 @@ namespace MediaBrowser.Model.Services long ContentLength { get; } /// <summary> - /// Access to the multi-part/formdata files posted on this request - /// </summary> - IHttpFile[] Files { get; } - - /// <summary> /// The value of the Referrer, null if not available /// </summary> Uri UrlReferrer { get; } @@ -98,25 +83,4 @@ namespace MediaBrowser.Model.Services { IRequest Request { get; set; } } - - public interface IResponse - { - HttpResponse OriginalResponse { get; } - - int StatusCode { get; set; } - - string StatusDescription { get; set; } - - string ContentType { get; set; } - - void AddHeader(string name, string value); - - void Redirect(string url); - - Stream OutputStream { get; } - - Task TransmitFile(string path, long offset, long count, FileShareMode fileShareMode, IFileSystem fileSystem, IStreamHelper streamHelper, CancellationToken cancellationToken); - - bool SendChunked { get; set; } - } } diff --git a/MediaBrowser.Model/System/PublicSystemInfo.cs b/MediaBrowser.Model/System/PublicSystemInfo.cs index d6e031e42..23f6d378c 100644 --- a/MediaBrowser.Model/System/PublicSystemInfo.cs +++ b/MediaBrowser.Model/System/PublicSystemInfo.cs @@ -9,12 +9,6 @@ namespace MediaBrowser.Model.System public string LocalAddress { get; set; } /// <summary> - /// Gets or sets the wan address. - /// </summary> - /// <value>The wan address.</value> - public string WanAddress { get; set; } - - /// <summary> /// Gets or sets the name of the server. /// </summary> /// <value>The name of the server.</value> @@ -25,7 +19,7 @@ namespace MediaBrowser.Model.System /// </summary> /// <value>The version.</value> public string Version { get; set; } - + /// <summary> /// The product name. This is the AssemblyProduct name. /// </summary> diff --git a/MediaBrowser.Model/System/WakeOnLanInfo.cs b/MediaBrowser.Model/System/WakeOnLanInfo.cs index 031458735..534ad19ec 100644 --- a/MediaBrowser.Model/System/WakeOnLanInfo.cs +++ b/MediaBrowser.Model/System/WakeOnLanInfo.cs @@ -1,10 +1,47 @@ +using System.Net.NetworkInformation; + namespace MediaBrowser.Model.System { + /// <summary> + /// Provides the MAC address and port for wake-on-LAN functionality. + /// </summary> public class WakeOnLanInfo { + /// <summary> + /// Returns the MAC address of the device. + /// </summary> + /// <value>The MAC address.</value> public string MacAddress { get; set; } + + /// <summary> + /// Returns the wake-on-LAN port. + /// </summary> + /// <value>The wake-on-LAN port.</value> public int Port { get; set; } + /// <summary> + /// Initializes a new instance of the <see cref="WakeOnLanInfo" /> class. + /// </summary> + /// <param name="macAddress">The MAC address.</param> + public WakeOnLanInfo(PhysicalAddress macAddress) + { + MacAddress = macAddress.ToString(); + Port = 9; + } + + /// <summary> + /// Initializes a new instance of the <see cref="WakeOnLanInfo" /> class. + /// </summary> + /// <param name="macAddress">The MAC address.</param> + public WakeOnLanInfo(string macAddress) + { + MacAddress = macAddress; + Port = 9; + } + + /// <summary> + /// Initializes a new instance of the <see cref="WakeOnLanInfo" /> class. + /// </summary> public WakeOnLanInfo() { Port = 9; diff --git a/MediaBrowser.Model/Updates/InstallationInfo.cs b/MediaBrowser.Model/Updates/InstallationInfo.cs index a3f19e236..7554e9fe2 100644 --- a/MediaBrowser.Model/Updates/InstallationInfo.cs +++ b/MediaBrowser.Model/Updates/InstallationInfo.cs @@ -36,11 +36,5 @@ namespace MediaBrowser.Model.Updates /// </summary> /// <value>The update class.</value> public PackageVersionClass UpdateClass { get; set; } - - /// <summary> - /// Gets or sets the percent complete. - /// </summary> - /// <value>The percent complete.</value> - public double? PercentComplete { get; set; } } } diff --git a/MediaBrowser.Model/Updates/PackageVersionInfo.cs b/MediaBrowser.Model/Updates/PackageVersionInfo.cs index be531770d..c0790317d 100644 --- a/MediaBrowser.Model/Updates/PackageVersionInfo.cs +++ b/MediaBrowser.Model/Updates/PackageVersionInfo.cs @@ -1,5 +1,5 @@ using System; -using MediaBrowser.Model.Serialization; +using System.Text.Json.Serialization; namespace MediaBrowser.Model.Updates { @@ -30,23 +30,25 @@ namespace MediaBrowser.Model.Updates /// The _version /// </summary> private Version _version; + /// <summary> /// Gets or sets the version. /// Had to make this an interpreted property since Protobuf can't handle Version /// </summary> /// <value>The version.</value> - [IgnoreDataMember] - public Version version => _version ?? (_version = new Version(ValueOrDefault(versionStr, "0.0.0.1"))); - - /// <summary> - /// Values the or default. - /// </summary> - /// <param name="str">The STR.</param> - /// <param name="def">The def.</param> - /// <returns>System.String.</returns> - private static string ValueOrDefault(string str, string def) + [JsonIgnore] + public Version Version { - return string.IsNullOrEmpty(str) ? def : str; + get + { + if (_version == null) + { + var ver = versionStr; + _version = new Version(string.IsNullOrEmpty(ver) ? "0.0.0.1" : ver); + } + + return _version; + } } /// <summary> diff --git a/MediaBrowser.Model/Users/UserPolicy.cs b/MediaBrowser.Model/Users/UserPolicy.cs index f63ab2bb4..9c3e1f980 100644 --- a/MediaBrowser.Model/Users/UserPolicy.cs +++ b/MediaBrowser.Model/Users/UserPolicy.cs @@ -66,7 +66,7 @@ namespace MediaBrowser.Model.Users public bool EnableAllFolders { get; set; } public int InvalidLoginAttemptCount { get; set; } - public int? LoginAttemptsBeforeLockout { get; set; } + public int LoginAttemptsBeforeLockout { get; set; } public bool EnablePublicSharing { get; set; } @@ -79,6 +79,8 @@ namespace MediaBrowser.Model.Users public UserPolicy() { + IsHidden = true; + EnableContentDeletion = false; EnableContentDeletionFromFolders = Array.Empty<string>(); diff --git a/MediaBrowser.Providers/Books/AudioBookMetadataService.cs b/MediaBrowser.Providers/Books/AudioBookMetadataService.cs index 4820e12ab..0062d5ab3 100644 --- a/MediaBrowser.Providers/Books/AudioBookMetadataService.cs +++ b/MediaBrowser.Providers/Books/AudioBookMetadataService.cs @@ -11,14 +11,31 @@ namespace MediaBrowser.Providers.Books { public class AudioBookMetadataService : MetadataService<AudioBook, SongInfo> { - protected override void MergeData(MetadataResult<AudioBook> source, MetadataResult<AudioBook> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) + public AudioBookMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + IUserDataManager userDataManager, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + { + } + + /// <inheritdoc /> + protected override void MergeData( + MetadataResult<AudioBook> source, + MetadataResult<AudioBook> target, + MetadataFields[] lockedFields, + bool replaceData, + bool mergeMetadataSettings) { ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); var sourceItem = source.Item; var targetItem = target.Item; - if (replaceData || targetItem.Artists.Length == 0) + if (replaceData || targetItem.Artists.Count == 0) { targetItem.Artists = sourceItem.Artists; } @@ -28,9 +45,5 @@ namespace MediaBrowser.Providers.Books targetItem.Album = sourceItem.Album; } } - - public AudioBookMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) - { - } } } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 860ea13cf..6a8d03f6d 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -278,7 +278,7 @@ namespace MediaBrowser.Providers.Manager var currentOptions = options; var typeOptions = libraryOptions.GetTypeOptions(item.GetType().Name); - var typeFetcherOrder = typeOptions == null ? null : typeOptions.ImageFetcherOrder; + var typeFetcherOrder = typeOptions?.ImageFetcherOrder; return ImageProviders.Where(i => CanRefresh(i, item, libraryOptions, options, refreshOptions, includeDisabled)) .OrderBy(i => @@ -287,7 +287,6 @@ namespace MediaBrowser.Providers.Manager if (!(i is ILocalImageProvider)) { var fetcherOrder = typeFetcherOrder ?? currentOptions.ImageFetcherOrder; - var index = Array.IndexOf(fetcherOrder, i.Name); if (index != -1) @@ -934,7 +933,7 @@ namespace MediaBrowser.Providers.Manager public void OnRefreshStart(BaseItem item) { - //_logger.LogInformation("OnRefreshStart {0}", item.Id.ToString("N")); + //_logger.LogInformation("OnRefreshStart {0}", item.Id.ToString("N", CultureInfo.InvariantCulture)); var id = item.Id; lock (_activeRefreshes) @@ -947,7 +946,7 @@ namespace MediaBrowser.Providers.Manager public void OnRefreshComplete(BaseItem item) { - //_logger.LogInformation("OnRefreshComplete {0}", item.Id.ToString("N")); + //_logger.LogInformation("OnRefreshComplete {0}", item.Id.ToString("N", CultureInfo.InvariantCulture)); lock (_activeRefreshes) { _activeRefreshes.Remove(item.Id); @@ -971,7 +970,7 @@ namespace MediaBrowser.Providers.Manager public void OnRefreshProgress(BaseItem item, double progress) { - //_logger.LogInformation("OnRefreshProgress {0} {1}", item.Id.ToString("N"), progress); + //_logger.LogInformation("OnRefreshProgress {0} {1}", item.Id.ToString("N", CultureInfo.InvariantCulture), progress); var id = item.Id; lock (_activeRefreshes) @@ -985,7 +984,7 @@ namespace MediaBrowser.Providers.Manager 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"))); + //throw new Exception(string.Format("Refresh for item {0} {1} is not in progress", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture))); } } } diff --git a/MediaBrowser.Providers/Manager/ProviderUtils.cs b/MediaBrowser.Providers/Manager/ProviderUtils.cs index 2031449d9..8d1588c4e 100644 --- a/MediaBrowser.Providers/Manager/ProviderUtils.cs +++ b/MediaBrowser.Providers/Manager/ProviderUtils.cs @@ -11,7 +11,8 @@ namespace MediaBrowser.Providers.Manager { public static class ProviderUtils { - public static void MergeBaseItemData<T>(MetadataResult<T> sourceResult, + public static void MergeBaseItemData<T>( + MetadataResult<T> sourceResult, MetadataResult<T> targetResult, MetadataFields[] lockedFields, bool replaceData, @@ -174,11 +175,11 @@ namespace MediaBrowser.Providers.Manager } } - MergeAlbumArtist(source, target, lockedFields, replaceData); - MergeCriticRating(source, target, lockedFields, replaceData); - MergeTrailers(source, target, lockedFields, replaceData); - MergeVideoInfo(source, target, lockedFields, replaceData); - MergeDisplayOrder(source, target, lockedFields, replaceData); + MergeAlbumArtist(source, target, replaceData); + MergeCriticRating(source, target, replaceData); + MergeTrailers(source, target, replaceData); + MergeVideoInfo(source, target, replaceData); + MergeDisplayOrder(source, target, replaceData); if (replaceData || string.IsNullOrEmpty(target.ForcedSortName)) { @@ -196,7 +197,7 @@ namespace MediaBrowser.Providers.Manager target.IsLocked = source.IsLocked; // Grab the value if it's there, but if not then don't overwrite the default - if (source.DateCreated != default(DateTime)) + if (source.DateCreated != default) { target.DateCreated = source.DateCreated; } @@ -231,12 +232,10 @@ namespace MediaBrowser.Providers.Manager } } - private static void MergeDisplayOrder(BaseItem source, BaseItem target, MetadataFields[] lockedFields, bool replaceData) + private static void MergeDisplayOrder(BaseItem source, BaseItem target, bool replaceData) { - var sourceHasDisplayOrder = source as IHasDisplayOrder; - var targetHasDisplayOrder = target as IHasDisplayOrder; - - if (sourceHasDisplayOrder != null && targetHasDisplayOrder != null) + if (source is IHasDisplayOrder sourceHasDisplayOrder + && target is IHasDisplayOrder targetHasDisplayOrder) { if (replaceData || string.IsNullOrEmpty(targetHasDisplayOrder.DisplayOrder)) { @@ -250,21 +249,19 @@ namespace MediaBrowser.Providers.Manager } } - private static void MergeAlbumArtist(BaseItem source, BaseItem target, MetadataFields[] lockedFields, bool replaceData) + private static void MergeAlbumArtist(BaseItem source, BaseItem target, bool replaceData) { - var sourceHasAlbumArtist = source as IHasAlbumArtist; - var targetHasAlbumArtist = target as IHasAlbumArtist; - - if (sourceHasAlbumArtist != null && targetHasAlbumArtist != null) + if (source is IHasAlbumArtist sourceHasAlbumArtist + && target is IHasAlbumArtist targetHasAlbumArtist) { - if (replaceData || targetHasAlbumArtist.AlbumArtists.Length == 0) + if (replaceData || targetHasAlbumArtist.AlbumArtists.Count == 0) { targetHasAlbumArtist.AlbumArtists = sourceHasAlbumArtist.AlbumArtists; } } } - private static void MergeCriticRating(BaseItem source, BaseItem target, MetadataFields[] lockedFields, bool replaceData) + private static void MergeCriticRating(BaseItem source, BaseItem target, bool replaceData) { if (replaceData || !target.CriticRating.HasValue) { @@ -272,20 +269,17 @@ namespace MediaBrowser.Providers.Manager } } - private static void MergeTrailers(BaseItem source, BaseItem target, MetadataFields[] lockedFields, bool replaceData) + private static void MergeTrailers(BaseItem source, BaseItem target, bool replaceData) { - if (replaceData || target.RemoteTrailers.Length == 0) + if (replaceData || target.RemoteTrailers.Count == 0) { target.RemoteTrailers = source.RemoteTrailers; } } - private static void MergeVideoInfo(BaseItem source, BaseItem target, MetadataFields[] lockedFields, bool replaceData) + private static void MergeVideoInfo(BaseItem source, BaseItem target, bool replaceData) { - var sourceCast = source as Video; - var targetCast = target as Video; - - if (sourceCast != null && targetCast != null) + if (source is Video sourceCast && target is Video targetCast) { if (replaceData || targetCast.Video3DFormat == null) { diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 5941ed436..c7ecc59c9 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -21,6 +21,12 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> + <PropertyGroup> + <!-- We need at least C# 7.1 --> + <LangVersion>latest</LangVersion> + </PropertyGroup> + </Project> diff --git a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs index 61a8a122b..7023ef706 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading; @@ -99,11 +100,11 @@ namespace MediaBrowser.Providers.MediaInfo if (!string.IsNullOrWhiteSpace(item.Album) && !string.IsNullOrWhiteSpace(albumArtist)) { - filename = (item.Album + "-" + albumArtist).GetMD5().ToString("N"); + filename = (item.Album + "-" + albumArtist).GetMD5().ToString("N", CultureInfo.InvariantCulture); } else { - filename = item.Id.ToString("N"); + filename = item.Id.ToString("N", CultureInfo.InvariantCulture); } filename += ".jpg"; @@ -111,7 +112,7 @@ namespace MediaBrowser.Providers.MediaInfo else { // If it's an audio book or audio podcast, allow unique image per item - filename = item.Id.ToString("N") + ".jpg"; + filename = item.Id.ToString("N", CultureInfo.InvariantCulture) + ".jpg"; } var prefix = filename.Substring(0, 1); diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs index d80084acf..e0b23108f 100644 --- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs @@ -62,7 +62,7 @@ namespace MediaBrowser.Providers.MediaInfo { var protocol = item.PathProtocol ?? MediaProtocol.File; - var inputPath = MediaEncoderHelpers.GetInputArgument(_fileSystem, item.Path, protocol, null, item.GetPlayableStreamFileNames(_mediaEncoder)); + var inputPath = MediaEncoderHelpers.GetInputArgument(_fileSystem, item.Path, null, item.GetPlayableStreamFileNames(_mediaEncoder)); var mediaStreams = item.GetMediaStreams(); diff --git a/MediaBrowser.Providers/Movies/MovieExternalIds.cs b/MediaBrowser.Providers/Movies/MovieExternalIds.cs index 3783c5032..09ed6034c 100644 --- a/MediaBrowser.Providers/Movies/MovieExternalIds.cs +++ b/MediaBrowser.Providers/Movies/MovieExternalIds.cs @@ -7,85 +7,6 @@ using MediaBrowser.Model.Entities; namespace MediaBrowser.Providers.Movies { - public class MovieDbMovieExternalId : IExternalId - { - public const string BaseMovieDbUrl = "https://www.themoviedb.org/"; - - public string Name => "TheMovieDb"; - - public string Key => MetadataProviders.Tmdb.ToString(); - - public string UrlFormatString => BaseMovieDbUrl + "movie/{0}"; - - public bool Supports(IHasProviderIds item) - { - // Supports images for tv movies - var tvProgram = item as LiveTvProgram; - if (tvProgram != null && tvProgram.IsMovie) - { - return true; - } - - return item is Movie || item is MusicVideo || item is Trailer; - } - } - - public class MovieDbSeriesExternalId : IExternalId - { - public string Name => "TheMovieDb"; - - public string Key => MetadataProviders.Tmdb.ToString(); - - public string UrlFormatString => MovieDbMovieExternalId.BaseMovieDbUrl + "tv/{0}"; - - public bool Supports(IHasProviderIds item) - { - return item is Series; - } - } - - public class MovieDbMovieCollectionExternalId : IExternalId - { - public string Name => "TheMovieDb Collection"; - - public string Key => MetadataProviders.TmdbCollection.ToString(); - - public string UrlFormatString => MovieDbMovieExternalId.BaseMovieDbUrl + "collection/{0}"; - - public bool Supports(IHasProviderIds item) - { - return item is Movie || item is MusicVideo || item is Trailer; - } - } - - public class MovieDbPersonExternalId : IExternalId - { - public string Name => "TheMovieDb"; - - public string Key => MetadataProviders.Tmdb.ToString(); - - public string UrlFormatString => MovieDbMovieExternalId.BaseMovieDbUrl + "person/{0}"; - - public bool Supports(IHasProviderIds item) - { - return item is Person; - } - } - - public class MovieDbCollectionExternalId : IExternalId - { - public string Name => "TheMovieDb"; - - public string Key => MetadataProviders.Tmdb.ToString(); - - public string UrlFormatString => MovieDbMovieExternalId.BaseMovieDbUrl + "collection/{0}"; - - public bool Supports(IHasProviderIds item) - { - return item is BoxSet; - } - } - public class ImdbExternalId : IExternalId { public string Name => "IMDb"; diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs index 33a6c2fa3..4e59b4119 100644 --- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs +++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs @@ -15,12 +15,34 @@ namespace MediaBrowser.Providers.Music { public class AlbumMetadataService : MetadataService<MusicAlbum, AlbumInfo> { + public AlbumMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + IUserDataManager userDataManager, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + { + } + + /// <inheritdoc /> + protected override bool EnableUpdatingPremiereDateFromChildren => true; + + /// <inheritdoc /> + protected override bool EnableUpdatingGenresFromChildren => true; + + /// <inheritdoc /> + protected override bool EnableUpdatingStudiosFromChildren => true; + + /// <inheritdoc /> protected override IList<BaseItem> GetChildrenForMetadataUpdates(MusicAlbum item) { return item.GetRecursiveChildren(i => i is Audio) .ToList(); } + /// <inheritdoc /> protected override ItemUpdateType UpdateMetadataFromChildren(MusicAlbum item, IList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType) { var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType); @@ -50,12 +72,6 @@ namespace MediaBrowser.Providers.Music return updateType; } - protected override bool EnableUpdatingPremiereDateFromChildren => true; - - protected override bool EnableUpdatingGenresFromChildren => true; - - protected override bool EnableUpdatingStudiosFromChildren => true; - private ItemUpdateType SetAlbumArtistFromSongs(MusicAlbum item, IEnumerable<Audio> songs) { var updateType = ItemUpdateType.None; @@ -94,21 +110,23 @@ namespace MediaBrowser.Providers.Music return updateType; } - protected override void MergeData(MetadataResult<MusicAlbum> source, MetadataResult<MusicAlbum> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) + /// <inheritdoc /> + protected override void MergeData( + MetadataResult<MusicAlbum> source, + MetadataResult<MusicAlbum> target, + MetadataFields[] lockedFields, + bool replaceData, + bool mergeMetadataSettings) { ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); var sourceItem = source.Item; var targetItem = target.Item; - if (replaceData || targetItem.Artists.Length == 0) + if (replaceData || targetItem.Artists.Count == 0) { targetItem.Artists = sourceItem.Artists; } } - - public AlbumMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) - { - } } } diff --git a/MediaBrowser.Providers/Music/AudioMetadataService.cs b/MediaBrowser.Providers/Music/AudioMetadataService.cs index 1422a4eaa..3bf854b91 100644 --- a/MediaBrowser.Providers/Music/AudioMetadataService.cs +++ b/MediaBrowser.Providers/Music/AudioMetadataService.cs @@ -11,6 +11,18 @@ namespace MediaBrowser.Providers.Music { public class AudioMetadataService : MetadataService<Audio, SongInfo> { + public AudioMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + IUserDataManager userDataManager, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + { + } + + /// <inheritdoc /> protected override void MergeData(MetadataResult<Audio> source, MetadataResult<Audio> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) { ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); @@ -18,7 +30,7 @@ namespace MediaBrowser.Providers.Music var sourceItem = source.Item; var targetItem = target.Item; - if (replaceData || targetItem.Artists.Length == 0) + if (replaceData || targetItem.Artists.Count == 0) { targetItem.Artists = sourceItem.Artists; } @@ -28,9 +40,5 @@ namespace MediaBrowser.Providers.Music targetItem.Album = sourceItem.Album; } } - - public AudioMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) - { - } } } diff --git a/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs b/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs index 93412306f..c743ffcb0 100644 --- a/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs +++ b/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs @@ -11,7 +11,24 @@ namespace MediaBrowser.Providers.Music { public class MusicVideoMetadataService : MetadataService<MusicVideo, MusicVideoInfo> { - protected override void MergeData(MetadataResult<MusicVideo> source, MetadataResult<MusicVideo> target, MetadataFields[] lockedFields, bool replaceData, bool mergeMetadataSettings) + public MusicVideoMetadataService( + IServerConfigurationManager serverConfigurationManager, + ILogger logger, + IProviderManager providerManager, + IFileSystem fileSystem, + IUserDataManager userDataManager, + ILibraryManager libraryManager) + : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) + { + } + + /// <inheritdoc /> + protected override void MergeData( + MetadataResult<MusicVideo> source, + MetadataResult<MusicVideo> target, + MetadataFields[] lockedFields, + bool replaceData, + bool mergeMetadataSettings) { ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings); @@ -23,14 +40,10 @@ namespace MediaBrowser.Providers.Music targetItem.Album = sourceItem.Album; } - if (replaceData || targetItem.Artists.Length == 0) + if (replaceData || targetItem.Artists.Count == 0) { targetItem.Artists = sourceItem.Artists; } } - - public MusicVideoMetadataService(IServerConfigurationManager serverConfigurationManager, ILogger logger, IProviderManager providerManager, IFileSystem fileSystem, IUserDataManager userDataManager, ILibraryManager libraryManager) : base(serverConfigurationManager, logger, providerManager, fileSystem, userDataManager, libraryManager) - { - } } } diff --git a/MediaBrowser.Providers/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Omdb/OmdbProvider.cs index 19dce34d6..f8b876580 100644 --- a/MediaBrowser.Providers/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Omdb/OmdbProvider.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -347,7 +348,7 @@ namespace MediaBrowser.Providers.Omdb CancellationToken = cancellationToken, BufferContent = true, EnableDefaultUserAgent = true - }, "GET"); + }, HttpMethod.Get); } internal string GetDataFilePath(string imdbId) diff --git a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs index 4b41589f1..ef412db5a 100644 --- a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs +++ b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs @@ -2,10 +2,10 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Net; -using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; @@ -143,26 +143,20 @@ namespace MediaBrowser.Providers.Studios if (!fileInfo.Exists || (DateTime.UtcNow - fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays > 1) { - var temp = await httpClient.GetTempFile(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Progress = new SimpleProgress<double>(), - Url = url - - }).ConfigureAwait(false); - Directory.CreateDirectory(Path.GetDirectoryName(file)); - try - { - File.Copy(temp, file, true); - } - catch + using (var res = await httpClient.SendAsync( + new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url + }, + HttpMethod.Get).ConfigureAwait(false)) + using (var content = res.Content) + using (var fileStream = new FileStream(file, FileMode.Create)) { - + await content.CopyToAsync(fileStream).ConfigureAwait(false); } - - return temp; } return file; diff --git a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs index 7fc6909f5..b4a4c36e5 100644 --- a/MediaBrowser.Providers/Subtitles/SubtitleManager.cs +++ b/MediaBrowser.Providers/Subtitles/SubtitleManager.cs @@ -1,12 +1,11 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; @@ -296,7 +295,7 @@ namespace MediaBrowser.Providers.Subtitles private string GetProviderId(string name) { - return name.ToLowerInvariant().GetMD5().ToString("N"); + return name.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture); } private ISubtitleProvider GetProvider(string id) diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvDbClientManager.cs b/MediaBrowser.Providers/TV/TheTVDB/TvDbClientManager.cs index 85833223e..4abe6a943 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvDbClientManager.cs +++ b/MediaBrowser.Providers/TV/TheTVDB/TvDbClientManager.cs @@ -14,11 +14,12 @@ namespace MediaBrowser.Providers.TV.TheTVDB { public class TvDbClientManager { + private const string DefaultLanguage = "en"; + private readonly SemaphoreSlim _cacheWriteLock = new SemaphoreSlim(1, 1); private readonly IMemoryCache _cache; private readonly TvDbClient _tvDbClient; private DateTime _tokenCreatedAt; - private const string DefaultLanguage = "en"; public TvDbClientManager(IMemoryCache memoryCache) { @@ -102,39 +103,50 @@ namespace MediaBrowser.Providers.TV.TheTVDB return episodes; } - public Task<TvDbResponse<SeriesSearchResult[]>> GetSeriesByImdbIdAsync(string imdbId, string language, + public Task<TvDbResponse<SeriesSearchResult[]>> GetSeriesByImdbIdAsync( + string imdbId, + string language, CancellationToken cancellationToken) { var cacheKey = GenerateKey("series", imdbId, language); return TryGetValue(cacheKey, language,() => TvDbClient.Search.SearchSeriesByImdbIdAsync(imdbId, cancellationToken)); } - public Task<TvDbResponse<SeriesSearchResult[]>> GetSeriesByZap2ItIdAsync(string zap2ItId, string language, + public Task<TvDbResponse<SeriesSearchResult[]>> GetSeriesByZap2ItIdAsync( + string zap2ItId, + string language, CancellationToken cancellationToken) { var cacheKey = GenerateKey("series", zap2ItId, language); - return TryGetValue( cacheKey, language,() => TvDbClient.Search.SearchSeriesByZap2ItIdAsync(zap2ItId, cancellationToken)); + return TryGetValue(cacheKey, language, () => TvDbClient.Search.SearchSeriesByZap2ItIdAsync(zap2ItId, cancellationToken)); } - public Task<TvDbResponse<Actor[]>> GetActorsAsync(int tvdbId, string language, + public Task<TvDbResponse<Actor[]>> GetActorsAsync( + int tvdbId, + string language, CancellationToken cancellationToken) { var cacheKey = GenerateKey("actors", tvdbId, language); - return TryGetValue(cacheKey, language,() => TvDbClient.Series.GetActorsAsync(tvdbId, cancellationToken)); + return TryGetValue(cacheKey, language, () => TvDbClient.Series.GetActorsAsync(tvdbId, cancellationToken)); } - public Task<TvDbResponse<Image[]>> GetImagesAsync(int tvdbId, ImagesQuery imageQuery, string language, + public Task<TvDbResponse<Image[]>> GetImagesAsync( + int tvdbId, + ImagesQuery imageQuery, + string language, CancellationToken cancellationToken) { var cacheKey = GenerateKey("images", tvdbId, language, imageQuery); - return TryGetValue(cacheKey, language,() => TvDbClient.Series.GetImagesAsync(tvdbId, imageQuery, cancellationToken)); + return TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesAsync(tvdbId, imageQuery, cancellationToken)); } public Task<TvDbResponse<Language[]>> GetLanguagesAsync(CancellationToken cancellationToken) { - return TryGetValue("languages", null,() => TvDbClient.Languages.GetAllAsync(cancellationToken)); + return TryGetValue("languages", null, () => TvDbClient.Languages.GetAllAsync(cancellationToken)); } - public Task<TvDbResponse<EpisodesSummary>> GetSeriesEpisodeSummaryAsync(int tvdbId, string language, + public Task<TvDbResponse<EpisodesSummary>> GetSeriesEpisodeSummaryAsync( + int tvdbId, + string language, CancellationToken cancellationToken) { var cacheKey = GenerateKey("seriesepisodesummary", tvdbId, language); @@ -142,8 +154,12 @@ namespace MediaBrowser.Providers.TV.TheTVDB () => TvDbClient.Series.GetEpisodesSummaryAsync(tvdbId, cancellationToken)); } - public Task<TvDbResponse<EpisodeRecord[]>> GetEpisodesPageAsync(int tvdbId, int page, EpisodeQuery episodeQuery, - string language, CancellationToken cancellationToken) + public Task<TvDbResponse<EpisodeRecord[]>> GetEpisodesPageAsync( + int tvdbId, + int page, + EpisodeQuery episodeQuery, + string language, + CancellationToken cancellationToken) { var cacheKey = GenerateKey(language, tvdbId, episodeQuery); @@ -151,7 +167,9 @@ namespace MediaBrowser.Providers.TV.TheTVDB () => TvDbClient.Series.GetEpisodesAsync(tvdbId, page, episodeQuery, cancellationToken)); } - public Task<string> GetEpisodeTvdbId(EpisodeInfo searchInfo, string language, + public Task<string> GetEpisodeTvdbId( + EpisodeInfo searchInfo, + string language, CancellationToken cancellationToken) { searchInfo.SeriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), @@ -162,8 +180,21 @@ namespace MediaBrowser.Providers.TV.TheTVDB // Prefer SxE over premiere date as it is more robust if (searchInfo.IndexNumber.HasValue && searchInfo.ParentIndexNumber.HasValue) { - episodeQuery.AiredEpisode = searchInfo.IndexNumber.Value; - episodeQuery.AiredSeason = searchInfo.ParentIndexNumber.Value; + switch (searchInfo.SeriesDisplayOrder) + { + case "dvd": + episodeQuery.DvdEpisode = searchInfo.IndexNumber.Value; + episodeQuery.DvdSeason = searchInfo.ParentIndexNumber.Value; + break; + case "absolute": + episodeQuery.AbsoluteNumber = searchInfo.IndexNumber.Value; + break; + default: + //aired order + episodeQuery.AiredEpisode = searchInfo.IndexNumber.Value; + episodeQuery.AiredSeason = searchInfo.ParentIndexNumber.Value; + break; + } } else if (searchInfo.PremiereDate.HasValue) { @@ -174,7 +205,9 @@ namespace MediaBrowser.Providers.TV.TheTVDB return GetEpisodeTvdbId(Convert.ToInt32(seriesTvdbId), episodeQuery, language, cancellationToken); } - public async Task<string> GetEpisodeTvdbId(int seriesTvdbId, EpisodeQuery episodeQuery, + public async Task<string> GetEpisodeTvdbId( + int seriesTvdbId, + EpisodeQuery episodeQuery, string language, CancellationToken cancellationToken) { @@ -184,8 +217,11 @@ namespace MediaBrowser.Providers.TV.TheTVDB return episodePage.Data.FirstOrDefault()?.Id.ToString(); } - public Task<TvDbResponse<EpisodeRecord[]>> GetEpisodesPageAsync(int tvdbId, EpisodeQuery episodeQuery, - string language, CancellationToken cancellationToken) + public Task<TvDbResponse<EpisodeRecord[]>> GetEpisodesPageAsync( + int tvdbId, + EpisodeQuery episodeQuery, + string language, + CancellationToken cancellationToken) { return GetEpisodesPageAsync(tvdbId, 1, episodeQuery, language, cancellationToken); } diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs index c04e98e64..eaebc13e3 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeImageProvider.cs @@ -50,27 +50,25 @@ namespace MediaBrowser.Providers.TV.TheTVDB var language = item.GetPreferredMetadataLanguage(); if (series != null && TvdbSeriesProvider.IsValidSeries(series.ProviderIds)) { - var episodeTvdbId = episode.GetProviderId(MetadataProviders.Tvdb); - // Process images try { + var episodeInfo = new EpisodeInfo + { + IndexNumber = episode.IndexNumber.Value, + ParentIndexNumber = episode.ParentIndexNumber.Value, + SeriesProviderIds = series.ProviderIds + }; + string episodeTvdbId = await _tvDbClientManager + .GetEpisodeTvdbId(episodeInfo, language, cancellationToken).ConfigureAwait(false); if (string.IsNullOrEmpty(episodeTvdbId)) { - var episodeInfo = new EpisodeInfo - { - IndexNumber = episode.IndexNumber.Value, - ParentIndexNumber = episode.ParentIndexNumber.Value, - SeriesProviderIds = series.ProviderIds - }; - episodeTvdbId = await _tvDbClientManager - .GetEpisodeTvdbId(episodeInfo, language, cancellationToken).ConfigureAwait(false); - if (string.IsNullOrEmpty(episodeTvdbId)) - { - _logger.LogError("Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}", - episodeInfo.ParentIndexNumber, episodeInfo.IndexNumber, series.GetProviderId(MetadataProviders.Tvdb)); - return imageResult; - } + _logger.LogError( + "Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}", + episodeInfo.ParentIndexNumber, + episodeInfo.IndexNumber, + series.GetProviderId(MetadataProviders.Tvdb)); + return imageResult; } var episodeResult = @@ -86,7 +84,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB } catch (TvDbServerException e) { - _logger.LogError(e, "Failed to retrieve episode images for {TvDbId}", episodeTvdbId); + _logger.LogError(e, "Failed to retrieve episode images for series {TvDbId}", series.GetProviderId(MetadataProviders.Tvdb)); } } diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs index 302d40c6b..e5287048d 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbEpisodeProvider.cs @@ -36,57 +36,33 @@ namespace MediaBrowser.Providers.TV.TheTVDB var list = new List<RemoteSearchResult>(); // The search query must either provide an episode number or date - if (!searchInfo.IndexNumber.HasValue || !searchInfo.PremiereDate.HasValue) + if (!searchInfo.IndexNumber.HasValue + || !searchInfo.PremiereDate.HasValue + || !TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds)) { return list; } - if (TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds)) + var metadataResult = await GetEpisode(searchInfo, cancellationToken).ConfigureAwait(false); + + if (!metadataResult.HasMetadata) { - try - { - var episodeTvdbId = searchInfo.GetProviderId(MetadataProviders.Tvdb); - if (string.IsNullOrEmpty(episodeTvdbId)) - { - searchInfo.SeriesProviderIds.TryGetValue(MetadataProviders.Tvdb.ToString(), - out var seriesTvdbId); - episodeTvdbId = await _tvDbClientManager - .GetEpisodeTvdbId(searchInfo, searchInfo.MetadataLanguage, cancellationToken) - .ConfigureAwait(false); - if (string.IsNullOrEmpty(episodeTvdbId)) - { - _logger.LogError("Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}", - searchInfo.ParentIndexNumber, searchInfo.IndexNumber, seriesTvdbId); - return list; - } - } + return list; + } - var episodeResult = await _tvDbClientManager.GetEpisodesAsync(Convert.ToInt32(episodeTvdbId), - searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); - var metadataResult = MapEpisodeToResult(searchInfo, episodeResult.Data); + var item = metadataResult.Item; - if (metadataResult.HasMetadata) - { - var item = metadataResult.Item; - - list.Add(new RemoteSearchResult - { - IndexNumber = item.IndexNumber, - Name = item.Name, - ParentIndexNumber = item.ParentIndexNumber, - PremiereDate = item.PremiereDate, - ProductionYear = item.ProductionYear, - ProviderIds = item.ProviderIds, - SearchProviderName = Name, - IndexNumberEnd = item.IndexNumberEnd - }); - } - } - catch (TvDbServerException e) - { - _logger.LogError(e, "Failed to retrieve episode with id {TvDbId}", searchInfo.IndexNumber); - } - } + list.Add(new RemoteSearchResult + { + IndexNumber = item.IndexNumber, + Name = item.Name, + ParentIndexNumber = item.ParentIndexNumber, + PremiereDate = item.PremiereDate, + ProductionYear = item.ProductionYear, + ProviderIds = item.ProviderIds, + SearchProviderName = Name, + IndexNumberEnd = item.IndexNumberEnd + }); return list; } @@ -103,36 +79,46 @@ namespace MediaBrowser.Providers.TV.TheTVDB if (TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds) && (searchInfo.IndexNumber.HasValue || searchInfo.PremiereDate.HasValue)) { - var tvdbId = searchInfo.GetProviderId(MetadataProviders.Tvdb); - try - { - if (string.IsNullOrEmpty(tvdbId)) - { - tvdbId = await _tvDbClientManager - .GetEpisodeTvdbId(searchInfo, searchInfo.MetadataLanguage, cancellationToken) - .ConfigureAwait(false); - if (string.IsNullOrEmpty(tvdbId)) - { - _logger.LogError("Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}", - searchInfo.ParentIndexNumber, searchInfo.IndexNumber, tvdbId); - return result; - } - } + result = await GetEpisode(searchInfo, cancellationToken).ConfigureAwait(false); + } + else + { + _logger.LogDebug("No series identity found for {EpisodeName}", searchInfo.Name); + } - var episodeResult = await _tvDbClientManager.GetEpisodesAsync( - Convert.ToInt32(tvdbId), searchInfo.MetadataLanguage, - cancellationToken).ConfigureAwait(false); + return result; + } - result = MapEpisodeToResult(searchInfo, episodeResult.Data); - } - catch (TvDbServerException e) + private async Task<MetadataResult<Episode>> GetEpisode(EpisodeInfo searchInfo, CancellationToken cancellationToken) + { + var result = new MetadataResult<Episode> + { + QueriedById = true + }; + + string seriesTvdbId = searchInfo.GetProviderId(MetadataProviders.Tvdb); + string episodeTvdbId = null; + try + { + episodeTvdbId = await _tvDbClientManager + .GetEpisodeTvdbId(searchInfo, searchInfo.MetadataLanguage, cancellationToken) + .ConfigureAwait(false); + if (string.IsNullOrEmpty(episodeTvdbId)) { - _logger.LogError(e, "Failed to retrieve episode with id {TvDbId}", tvdbId); + _logger.LogError("Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}", + searchInfo.ParentIndexNumber, searchInfo.IndexNumber, seriesTvdbId); + return result; } + + var episodeResult = await _tvDbClientManager.GetEpisodesAsync( + Convert.ToInt32(episodeTvdbId), searchInfo.MetadataLanguage, + cancellationToken).ConfigureAwait(false); + + result = MapEpisodeToResult(searchInfo, episodeResult.Data); } - else + catch (TvDbServerException e) { - _logger.LogDebug("No series identity found for {EpisodeName}", searchInfo.Name); + _logger.LogError(e, "Failed to retrieve episode with id {EpisodeTvDbId}, series id {SeriesTvdbId}", episodeTvdbId, seriesTvdbId); } return result; @@ -193,24 +179,54 @@ namespace MediaBrowser.Providers.TV.TheTVDB }); } - foreach (var person in episode.GuestStars) + // GuestStars is a weird list of names and roles + // Example: + // 1: Some Actor (Role1 + // 2: Role2 + // 3: Role3) + // 4: Another Actor (Role1 + // ... + for (var i = 0; i < episode.GuestStars.Length; ++i) { - var index = person.IndexOf('('); - string role = null; - var name = person; + var currentActor = episode.GuestStars[i]; + var roleStartIndex = currentActor.IndexOf('('); - if (index != -1) + if (roleStartIndex == -1) { - role = person.Substring(index + 1).Trim().TrimEnd(')'); + result.AddPerson(new PersonInfo + { + Type = PersonType.GuestStar, + Name = currentActor, + Role = string.Empty + }); + continue; + } + + var roles = new List<string> {currentActor.Substring(roleStartIndex + 1)}; + + // Fetch all roles + for (var j = i + 1; j < episode.GuestStars.Length; ++j) + { + var currentRole = episode.GuestStars[j]; + var roleEndIndex = currentRole.IndexOf(')'); + + if (roleEndIndex == -1) + { + roles.Add(currentRole); + continue; + } - name = person.Substring(0, index).Trim(); + roles.Add(currentRole.TrimEnd(')')); + // Update the outer index (keep in mind it adds 1 after the iteration) + i = j; + break; } result.AddPerson(new PersonInfo { Type = PersonType.GuestStar, - Name = name, - Role = role + Name = currentActor.Substring(0, roleStartIndex).Trim(), + Role = string.Join(", ", roles) }); } diff --git a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs index c739f3f49..10ed4f073 100644 --- a/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs +++ b/MediaBrowser.Providers/TV/TheTVDB/TvdbSeriesProvider.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Net; @@ -285,7 +286,7 @@ namespace MediaBrowser.Providers.TV.TheTVDB private string GetComparableName(string name) { name = name.ToLowerInvariant(); - name = _localizationManager.NormalizeFormKD(name); + name = name.Normalize(NormalizationForm.FormKD); var sb = new StringBuilder(); foreach (var c in name) { @@ -310,19 +311,9 @@ namespace MediaBrowser.Providers.TV.TheTVDB sb.Append(c); } } - name = sb.ToString(); - name = name.Replace(", the", ""); - name = name.Replace("the ", " "); - name = name.Replace(" the ", " "); + sb.Replace(", the", string.Empty).Replace("the ", " ").Replace(" the ", " "); - string prevName; - do - { - prevName = name; - name = name.Replace(" ", " "); - } while (name.Length != prevName.Length); - - return name.Trim(); + return Regex.Replace(sb.ToString().Trim(), @"\s+", " "); } private void MapSeriesToResult(MetadataResult<Series> result, TvDbSharper.Dto.Series tvdbSeries, string metadataLanguage) diff --git a/MediaBrowser.Providers/Tmdb/BoxSets/TmdbBoxSetExternalId.cs b/MediaBrowser.Providers/Tmdb/BoxSets/TmdbBoxSetExternalId.cs new file mode 100644 index 000000000..187295e1e --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/BoxSets/TmdbBoxSetExternalId.cs @@ -0,0 +1,25 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Tmdb.BoxSets +{ + public class TmdbBoxSetExternalId : IExternalId + { + /// <inheritdoc /> + public string Name => TmdbUtils.ProviderName; + + /// <inheritdoc /> + public string Key => MetadataProviders.TmdbCollection.ToString(); + + /// <inheritdoc /> + public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "collection/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) + { + return item is Movie || item is MusicVideo || item is Trailer; + } + } +} diff --git a/MediaBrowser.Providers/BoxSets/MovieDbBoxSetImageProvider.cs b/MediaBrowser.Providers/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs index 4d12b2f4a..5db0edac2 100644 --- a/MediaBrowser.Providers/BoxSets/MovieDbBoxSetImageProvider.cs +++ b/MediaBrowser.Providers/Tmdb/BoxSets/TmdbBoxSetImageProvider.cs @@ -11,21 +11,24 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; using MediaBrowser.Providers.Movies; +using MediaBrowser.Providers.Tmdb.Models.Collections; +using MediaBrowser.Providers.Tmdb.Models.General; +using MediaBrowser.Providers.Tmdb.Movies; -namespace MediaBrowser.Providers.BoxSets +namespace MediaBrowser.Providers.Tmdb.BoxSets { - public class MovieDbBoxSetImageProvider : IRemoteImageProvider, IHasOrder + public class TmdbBoxSetImageProvider : IRemoteImageProvider, IHasOrder { private readonly IHttpClient _httpClient; - public MovieDbBoxSetImageProvider(IHttpClient httpClient) + public TmdbBoxSetImageProvider(IHttpClient httpClient) { _httpClient = httpClient; } public string Name => ProviderName; - public static string ProviderName => "TheMovieDb"; + public static string ProviderName => TmdbUtils.ProviderName; public bool Supports(BaseItem item) { @@ -49,11 +52,11 @@ namespace MediaBrowser.Providers.BoxSets { var language = item.GetPreferredMetadataLanguage(); - var mainResult = await MovieDbBoxSetProvider.Current.GetMovieDbResult(tmdbId, null, cancellationToken).ConfigureAwait(false); + var mainResult = await TmdbBoxSetProvider.Current.GetMovieDbResult(tmdbId, null, cancellationToken).ConfigureAwait(false); if (mainResult != null) { - var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); + var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); @@ -64,20 +67,20 @@ namespace MediaBrowser.Providers.BoxSets return new List<RemoteImageInfo>(); } - private IEnumerable<RemoteImageInfo> GetImages(MovieDbBoxSetProvider.RootObject obj, string language, string baseUrl) + private IEnumerable<RemoteImageInfo> GetImages(CollectionResult obj, string language, string baseUrl) { var list = new List<RemoteImageInfo>(); - var images = obj.images ?? new MovieDbBoxSetProvider.Images(); + var images = obj.Images ?? new CollectionImages(); list.AddRange(GetPosters(images).Select(i => new RemoteImageInfo { - Url = baseUrl + i.file_path, - CommunityRating = i.vote_average, - VoteCount = i.vote_count, - Width = i.width, - Height = i.height, - Language = MovieDbProvider.AdjustImageLanguage(i.iso_639_1, language), + Url = baseUrl + i.File_Path, + CommunityRating = i.Vote_Average, + VoteCount = i.Vote_Count, + Width = i.Width, + Height = i.Height, + Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language), ProviderName = Name, Type = ImageType.Primary, RatingType = RatingType.Score @@ -85,11 +88,11 @@ namespace MediaBrowser.Providers.BoxSets list.AddRange(GetBackdrops(images).Select(i => new RemoteImageInfo { - Url = baseUrl + i.file_path, - CommunityRating = i.vote_average, - VoteCount = i.vote_count, - Width = i.width, - Height = i.height, + Url = baseUrl + i.File_Path, + CommunityRating = i.Vote_Average, + VoteCount = i.Vote_Count, + Width = i.Width, + Height = i.Height, ProviderName = Name, Type = ImageType.Backdrop, RatingType = RatingType.Score @@ -125,9 +128,9 @@ namespace MediaBrowser.Providers.BoxSets /// </summary> /// <param name="images">The images.</param> /// <returns>IEnumerable{MovieDbProvider.Poster}.</returns> - private IEnumerable<MovieDbBoxSetProvider.Poster> GetPosters(MovieDbBoxSetProvider.Images images) + private IEnumerable<Poster> GetPosters(CollectionImages images) { - return images.posters ?? new List<MovieDbBoxSetProvider.Poster>(); + return images.Posters ?? new List<Poster>(); } /// <summary> @@ -135,13 +138,13 @@ namespace MediaBrowser.Providers.BoxSets /// </summary> /// <param name="images">The images.</param> /// <returns>IEnumerable{MovieDbProvider.Backdrop}.</returns> - private IEnumerable<MovieDbBoxSetProvider.Backdrop> GetBackdrops(MovieDbBoxSetProvider.Images images) + private IEnumerable<Backdrop> GetBackdrops(CollectionImages images) { - var eligibleBackdrops = images.backdrops == null ? new List<MovieDbBoxSetProvider.Backdrop>() : - images.backdrops; + var eligibleBackdrops = images.Backdrops == null ? new List<Backdrop>() : + images.Backdrops; - return eligibleBackdrops.OrderByDescending(i => i.vote_average) - .ThenByDescending(i => i.vote_count); + return eligibleBackdrops.OrderByDescending(i => i.Vote_Average) + .ThenByDescending(i => i.Vote_Count); } public int Order => 0; diff --git a/MediaBrowser.Providers/BoxSets/MovieDbBoxSetProvider.cs b/MediaBrowser.Providers/Tmdb/BoxSets/TmdbBoxSetProvider.cs index 4e41694c4..a215177a9 100644 --- a/MediaBrowser.Providers/BoxSets/MovieDbBoxSetProvider.cs +++ b/MediaBrowser.Providers/Tmdb/BoxSets/TmdbBoxSetProvider.cs @@ -16,16 +16,18 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Movies; +using MediaBrowser.Providers.Tmdb.Models.Collections; +using MediaBrowser.Providers.Tmdb.Models.General; +using MediaBrowser.Providers.Tmdb.Movies; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.BoxSets +namespace MediaBrowser.Providers.Tmdb.BoxSets { - public class MovieDbBoxSetProvider : IRemoteMetadataProvider<BoxSet, BoxSetInfo> + public class TmdbBoxSetProvider : IRemoteMetadataProvider<BoxSet, BoxSetInfo> { - private const string GetCollectionInfo3 = MovieDbProvider.BaseMovieDbUrl + @"3/collection/{0}?api_key={1}&append_to_response=images"; + private const string GetCollectionInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/collection/{0}?api_key={1}&append_to_response=images"; - internal static MovieDbBoxSetProvider Current; + internal static TmdbBoxSetProvider Current; private readonly ILogger _logger; private readonly IJsonSerializer _json; @@ -35,7 +37,7 @@ namespace MediaBrowser.Providers.BoxSets private readonly IHttpClient _httpClient; private readonly ILibraryManager _libraryManager; - public MovieDbBoxSetProvider(ILogger logger, IJsonSerializer json, IServerConfigurationManager config, IFileSystem fileSystem, ILocalizationManager localization, IHttpClient httpClient, ILibraryManager libraryManager) + public TmdbBoxSetProvider(ILogger logger, IJsonSerializer json, IServerConfigurationManager config, IFileSystem fileSystem, ILocalizationManager localization, IHttpClient httpClient, ILibraryManager libraryManager) { _logger = logger; _json = json; @@ -58,29 +60,29 @@ namespace MediaBrowser.Providers.BoxSets await EnsureInfo(tmdbId, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false); var dataFilePath = GetDataFilePath(_config.ApplicationPaths, tmdbId, searchInfo.MetadataLanguage); - var info = _json.DeserializeFromFile<RootObject>(dataFilePath); + var info = _json.DeserializeFromFile<CollectionResult>(dataFilePath); - var images = (info.images ?? new Images()).posters ?? new List<Poster>(); + var images = (info.Images ?? new CollectionImages()).Posters ?? new List<Poster>(); - var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); + var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); var result = new RemoteSearchResult { - Name = info.name, + Name = info.Name, SearchProviderName = Name, - ImageUrl = images.Count == 0 ? null : (tmdbImageUrl + images[0].file_path) + ImageUrl = images.Count == 0 ? null : (tmdbImageUrl + images[0].File_Path) }; - result.SetProviderId(MetadataProviders.Tmdb, info.id.ToString(_usCulture)); + result.SetProviderId(MetadataProviders.Tmdb, info.Id.ToString(_usCulture)); return new[] { result }; } - return await new MovieDbSearch(_logger, _json, _libraryManager).GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false); + return await new TmdbSearch(_logger, _json, _libraryManager).GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false); } public async Task<MetadataResult<BoxSet>> GetMetadata(BoxSetInfo id, CancellationToken cancellationToken) @@ -90,7 +92,7 @@ namespace MediaBrowser.Providers.BoxSets // We don't already have an Id, need to fetch it if (string.IsNullOrEmpty(tmdbId)) { - var searchResults = await new MovieDbSearch(_logger, _json, _libraryManager).GetSearchResults(id, cancellationToken).ConfigureAwait(false); + var searchResults = await new TmdbSearch(_logger, _json, _libraryManager).GetSearchResults(id, cancellationToken).ConfigureAwait(false); var searchResult = searchResults.FirstOrDefault(); @@ -116,7 +118,7 @@ namespace MediaBrowser.Providers.BoxSets return result; } - internal async Task<RootObject> GetMovieDbResult(string tmdbId, string language, CancellationToken cancellationToken) + internal async Task<CollectionResult> GetMovieDbResult(string tmdbId, string language, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(tmdbId)) { @@ -129,21 +131,21 @@ namespace MediaBrowser.Providers.BoxSets if (!string.IsNullOrEmpty(dataFilePath)) { - return _json.DeserializeFromFile<RootObject>(dataFilePath); + return _json.DeserializeFromFile<CollectionResult>(dataFilePath); } return null; } - private BoxSet GetItem(RootObject obj) + private BoxSet GetItem(CollectionResult obj) { var item = new BoxSet { - Name = obj.name, - Overview = obj.overview + Name = obj.Name, + Overview = obj.Overview }; - item.SetProviderId(MetadataProviders.Tmdb, obj.id.ToString(_usCulture)); + item.SetProviderId(MetadataProviders.Tmdb, obj.Id.ToString(_usCulture)); return item; } @@ -161,61 +163,61 @@ namespace MediaBrowser.Providers.BoxSets _json.SerializeToFile(mainResult, dataFilePath); } - private async Task<RootObject> FetchMainResult(string id, string language, CancellationToken cancellationToken) + private async Task<CollectionResult> FetchMainResult(string id, string language, CancellationToken cancellationToken) { - var url = string.Format(GetCollectionInfo3, id, MovieDbProvider.ApiKey); + var url = string.Format(GetCollectionInfo3, id, TmdbUtils.ApiKey); if (!string.IsNullOrEmpty(language)) { - url += string.Format("&language={0}", MovieDbProvider.NormalizeLanguage(language)); + url += string.Format("&language={0}", TmdbMovieProvider.NormalizeLanguage(language)); // Get images in english and with no language - url += "&include_image_language=" + MovieDbProvider.GetImageLanguagesParam(language); + url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language); } cancellationToken.ThrowIfCancellationRequested(); - RootObject mainResult = null; + CollectionResult mainResult; - using (var response = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions + using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions { Url = url, CancellationToken = cancellationToken, - AcceptHeader = MovieDbSearch.AcceptHeader + AcceptHeader = TmdbUtils.AcceptHeader }).ConfigureAwait(false)) { using (var json = response.Content) { - mainResult = await _json.DeserializeFromStreamAsync<RootObject>(json).ConfigureAwait(false); + mainResult = await _json.DeserializeFromStreamAsync<CollectionResult>(json).ConfigureAwait(false); } } cancellationToken.ThrowIfCancellationRequested(); - if (mainResult != null && string.IsNullOrEmpty(mainResult.name)) + if (mainResult != null && string.IsNullOrEmpty(mainResult.Name)) { if (!string.IsNullOrEmpty(language) && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)) { - url = string.Format(GetCollectionInfo3, id, MovieDbSearch.ApiKey) + "&language=en"; + url = string.Format(GetCollectionInfo3, id, TmdbUtils.ApiKey) + "&language=en"; if (!string.IsNullOrEmpty(language)) { // Get images in english and with no language - url += "&include_image_language=" + MovieDbProvider.GetImageLanguagesParam(language); + url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language); } - using (var response = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions + using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions { Url = url, CancellationToken = cancellationToken, - AcceptHeader = MovieDbSearch.AcceptHeader + AcceptHeader = TmdbUtils.AcceptHeader }).ConfigureAwait(false)) { using (var json = response.Content) { - mainResult = await _json.DeserializeFromStreamAsync<RootObject>(json).ConfigureAwait(false); + mainResult = await _json.DeserializeFromStreamAsync<CollectionResult>(json).ConfigureAwait(false); } } } @@ -241,7 +243,7 @@ namespace MediaBrowser.Providers.BoxSets return DownloadInfo(tmdbId, preferredMetadataLanguage, cancellationToken); } - public string Name => "TheMovieDb"; + public string Name => TmdbUtils.ProviderName; private static string GetDataFilePath(IApplicationPaths appPaths, string tmdbId, string preferredLanguage) { @@ -266,54 +268,6 @@ namespace MediaBrowser.Providers.BoxSets return dataPath; } - internal class Part - { - public string title { get; set; } - public int id { get; set; } - public string release_date { get; set; } - public string poster_path { get; set; } - public string backdrop_path { get; set; } - } - - internal class Backdrop - { - public double aspect_ratio { get; set; } - public string file_path { get; set; } - public int height { get; set; } - public string iso_639_1 { get; set; } - public double vote_average { get; set; } - public int vote_count { get; set; } - public int width { get; set; } - } - - internal class Poster - { - public double aspect_ratio { get; set; } - public string file_path { get; set; } - public int height { get; set; } - public string iso_639_1 { get; set; } - public double vote_average { get; set; } - public int vote_count { get; set; } - public int width { get; set; } - } - - internal class Images - { - public List<Backdrop> backdrops { get; set; } - public List<Poster> posters { get; set; } - } - - internal class RootObject - { - public int id { get; set; } - public string name { get; set; } - public string overview { get; set; } - public string poster_path { get; set; } - public string backdrop_path { get; set; } - public List<Part> parts { get; set; } - public Images images { get; set; } - } - public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClient.GetResponse(new HttpRequestOptions diff --git a/MediaBrowser.Providers/Tmdb/Models/Collections/CollectionImages.cs b/MediaBrowser.Providers/Tmdb/Models/Collections/CollectionImages.cs new file mode 100644 index 000000000..18f26c397 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/Collections/CollectionImages.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using MediaBrowser.Providers.Tmdb.Models.General; + +namespace MediaBrowser.Providers.Tmdb.Models.Collections +{ + public class CollectionImages + { + public List<Backdrop> Backdrops { get; set; } + public List<Poster> Posters { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/Collections/CollectionResult.cs b/MediaBrowser.Providers/Tmdb/Models/Collections/CollectionResult.cs new file mode 100644 index 000000000..53d2599f8 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/Collections/CollectionResult.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Providers.Tmdb.Models.Collections +{ + public class CollectionResult + { + public int Id { get; set; } + public string Name { get; set; } + public string Overview { get; set; } + public string Poster_Path { get; set; } + public string Backdrop_Path { get; set; } + public List<Part> Parts { get; set; } + public CollectionImages Images { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/Collections/Part.cs b/MediaBrowser.Providers/Tmdb/Models/Collections/Part.cs new file mode 100644 index 000000000..ff19291c7 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/Collections/Part.cs @@ -0,0 +1,11 @@ +namespace MediaBrowser.Providers.Tmdb.Models.Collections +{ + public class Part + { + public string Title { get; set; } + public int Id { get; set; } + public string Release_Date { get; set; } + public string Poster_Path { get; set; } + public string Backdrop_Path { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/General/Backdrop.cs b/MediaBrowser.Providers/Tmdb/Models/General/Backdrop.cs new file mode 100644 index 000000000..db4cd6681 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/General/Backdrop.cs @@ -0,0 +1,13 @@ +namespace MediaBrowser.Providers.Tmdb.Models.General +{ + public class Backdrop + { + public double Aspect_Ratio { get; set; } + public string File_Path { get; set; } + public int Height { get; set; } + public string Iso_639_1 { get; set; } + public double Vote_Average { get; set; } + public int Vote_Count { get; set; } + public int Width { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/General/Crew.cs b/MediaBrowser.Providers/Tmdb/Models/General/Crew.cs new file mode 100644 index 000000000..47b985403 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/General/Crew.cs @@ -0,0 +1,12 @@ +namespace MediaBrowser.Providers.Tmdb.Models.General +{ + public class Crew + { + public int Id { get; set; } + public string Credit_Id { get; set; } + public string Name { get; set; } + public string Department { get; set; } + public string Job { get; set; } + public string Profile_Path { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/General/ExternalIds.cs b/MediaBrowser.Providers/Tmdb/Models/General/ExternalIds.cs new file mode 100644 index 000000000..37e37b0be --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/General/ExternalIds.cs @@ -0,0 +1,11 @@ +namespace MediaBrowser.Providers.Tmdb.Models.General +{ + public class ExternalIds + { + public string Imdb_Id { get; set; } + public object Freebase_Id { get; set; } + public string Freebase_Mid { get; set; } + public int Tvdb_Id { get; set; } + public int Tvrage_Id { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/General/Genre.cs b/MediaBrowser.Providers/Tmdb/Models/General/Genre.cs new file mode 100644 index 000000000..9a6686d50 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/General/Genre.cs @@ -0,0 +1,8 @@ +namespace MediaBrowser.Providers.Tmdb.Models.General +{ + public class Genre + { + public int Id { get; set; } + public string Name { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/General/Images.cs b/MediaBrowser.Providers/Tmdb/Models/General/Images.cs new file mode 100644 index 000000000..f1c99537d --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/General/Images.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Providers.Tmdb.Models.General +{ + public class Images + { + public List<Backdrop> Backdrops { get; set; } + public List<Poster> Posters { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/General/Keyword.cs b/MediaBrowser.Providers/Tmdb/Models/General/Keyword.cs new file mode 100644 index 000000000..4e3011349 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/General/Keyword.cs @@ -0,0 +1,8 @@ +namespace MediaBrowser.Providers.Tmdb.Models.General +{ + public class Keyword + { + public int Id { get; set; } + public string Name { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/General/Keywords.cs b/MediaBrowser.Providers/Tmdb/Models/General/Keywords.cs new file mode 100644 index 000000000..1950a51b3 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/General/Keywords.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Providers.Tmdb.Models.General +{ + public class Keywords + { + public List<Keyword> Results { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/General/Poster.cs b/MediaBrowser.Providers/Tmdb/Models/General/Poster.cs new file mode 100644 index 000000000..33401b15d --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/General/Poster.cs @@ -0,0 +1,13 @@ +namespace MediaBrowser.Providers.Tmdb.Models.General +{ + public class Poster + { + public double Aspect_Ratio { get; set; } + public string File_Path { get; set; } + public int Height { get; set; } + public string Iso_639_1 { get; set; } + public double Vote_Average { get; set; } + public int Vote_Count { get; set; } + public int Width { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/General/Profile.cs b/MediaBrowser.Providers/Tmdb/Models/General/Profile.cs new file mode 100644 index 000000000..73a049c73 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/General/Profile.cs @@ -0,0 +1,11 @@ +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; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/General/Still.cs b/MediaBrowser.Providers/Tmdb/Models/General/Still.cs new file mode 100644 index 000000000..15ff4a099 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/General/Still.cs @@ -0,0 +1,14 @@ +namespace MediaBrowser.Providers.Tmdb.Models.General +{ + public class Still + { + public double Aspect_Ratio { get; set; } + public string File_Path { get; set; } + public int Height { get; set; } + public string Id { get; set; } + public string Iso_639_1 { get; set; } + public double Vote_Average { get; set; } + public int Vote_Count { get; set; } + public int Width { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/General/StillImages.cs b/MediaBrowser.Providers/Tmdb/Models/General/StillImages.cs new file mode 100644 index 000000000..266965c47 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/General/StillImages.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Providers.Tmdb.Models.General +{ + public class StillImages + { + public List<Still> Stills { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/General/Video.cs b/MediaBrowser.Providers/Tmdb/Models/General/Video.cs new file mode 100644 index 000000000..fb69e7767 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/General/Video.cs @@ -0,0 +1,14 @@ +namespace MediaBrowser.Providers.Tmdb.Models.General +{ + public class Video + { + public string Id { get; set; } + public string Iso_639_1 { get; set; } + public string Iso_3166_1 { get; set; } + public string Key { get; set; } + public string Name { get; set; } + public string Site { get; set; } + public string Size { get; set; } + public string Type { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/General/Videos.cs b/MediaBrowser.Providers/Tmdb/Models/General/Videos.cs new file mode 100644 index 000000000..26812780d --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/General/Videos.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Providers.Tmdb.Models.General +{ + public class Videos + { + public List<Video> Results { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/Movies/BelongsToCollection.cs b/MediaBrowser.Providers/Tmdb/Models/Movies/BelongsToCollection.cs new file mode 100644 index 000000000..ac673df61 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/Movies/BelongsToCollection.cs @@ -0,0 +1,10 @@ +namespace MediaBrowser.Providers.Tmdb.Models.Movies +{ + public class BelongsToCollection + { + public int Id { get; set; } + public string Name { get; set; } + public string Poster_Path { get; set; } + public string Backdrop_Path { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/Movies/Cast.cs b/MediaBrowser.Providers/Tmdb/Models/Movies/Cast.cs new file mode 100644 index 000000000..44af9e568 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/Movies/Cast.cs @@ -0,0 +1,12 @@ +namespace MediaBrowser.Providers.Tmdb.Models.Movies +{ + public class Cast + { + public int Id { get; set; } + public string Name { get; set; } + public string Character { get; set; } + public int Order { get; set; } + public int Cast_Id { get; set; } + public string Profile_Path { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/Movies/Casts.cs b/MediaBrowser.Providers/Tmdb/Models/Movies/Casts.cs new file mode 100644 index 000000000..7b5094fa3 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/Movies/Casts.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using MediaBrowser.Providers.Tmdb.Models.General; + +namespace MediaBrowser.Providers.Tmdb.Models.Movies +{ + public class Casts + { + public List<Cast> Cast { get; set; } + public List<Crew> Crew { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/Movies/Country.cs b/MediaBrowser.Providers/Tmdb/Models/Movies/Country.cs new file mode 100644 index 000000000..6f843addd --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/Movies/Country.cs @@ -0,0 +1,11 @@ +using System; + +namespace MediaBrowser.Providers.Tmdb.Models.Movies +{ + public class Country + { + public string Iso_3166_1 { get; set; } + public string Certification { get; set; } + public DateTime Release_Date { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/Movies/MovieResult.cs b/MediaBrowser.Providers/Tmdb/Models/Movies/MovieResult.cs new file mode 100644 index 000000000..1b262946f --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/Movies/MovieResult.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using MediaBrowser.Providers.Tmdb.Models.General; + +namespace MediaBrowser.Providers.Tmdb.Models.Movies +{ + public class MovieResult + { + public bool Adult { get; set; } + public string Backdrop_Path { get; set; } + public BelongsToCollection Belongs_To_Collection { get; set; } + public int Budget { get; set; } + public List<Genre> Genres { get; set; } + public string Homepage { get; set; } + public int Id { get; set; } + public string Imdb_Id { get; set; } + public string Original_Title { get; set; } + public string Original_Name { get; set; } + public string Overview { get; set; } + public double Popularity { get; set; } + public string Poster_Path { get; set; } + public List<ProductionCompany> Production_Companies { get; set; } + public List<ProductionCountry> Production_Countries { get; set; } + public string Release_Date { get; set; } + public int Revenue { get; set; } + public int Runtime { get; set; } + public List<SpokenLanguage> Spoken_Languages { get; set; } + public string Status { get; set; } + public string Tagline { get; set; } + public string Title { get; set; } + public string Name { get; set; } + public double Vote_Average { get; set; } + public int Vote_Count { get; set; } + public Casts Casts { get; set; } + public Releases Releases { get; set; } + public Images Images { get; set; } + public Keywords Keywords { get; set; } + public Trailers Trailers { get; set; } + + public string GetOriginalTitle() + { + return Original_Name ?? Original_Title; + } + + public string GetTitle() + { + return Name ?? Title ?? GetOriginalTitle(); + } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/Movies/ProductionCompany.cs b/MediaBrowser.Providers/Tmdb/Models/Movies/ProductionCompany.cs new file mode 100644 index 000000000..c3382f305 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/Movies/ProductionCompany.cs @@ -0,0 +1,8 @@ +namespace MediaBrowser.Providers.Tmdb.Models.Movies +{ + public class ProductionCompany + { + public string Name { get; set; } + public int Id { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/Movies/ProductionCountry.cs b/MediaBrowser.Providers/Tmdb/Models/Movies/ProductionCountry.cs new file mode 100644 index 000000000..78112c915 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/Movies/ProductionCountry.cs @@ -0,0 +1,8 @@ +namespace MediaBrowser.Providers.Tmdb.Models.Movies +{ + public class ProductionCountry + { + public string Iso_3166_1 { get; set; } + public string Name { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/Movies/Releases.cs b/MediaBrowser.Providers/Tmdb/Models/Movies/Releases.cs new file mode 100644 index 000000000..c44f31e46 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/Movies/Releases.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Providers.Tmdb.Models.Movies +{ + public class Releases + { + public List<Country> Countries { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/Movies/SpokenLanguage.cs b/MediaBrowser.Providers/Tmdb/Models/Movies/SpokenLanguage.cs new file mode 100644 index 000000000..4bc5cfa48 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/Movies/SpokenLanguage.cs @@ -0,0 +1,8 @@ +namespace MediaBrowser.Providers.Tmdb.Models.Movies +{ + public class SpokenLanguage + { + public string Iso_639_1 { get; set; } + public string Name { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/Movies/Trailers.cs b/MediaBrowser.Providers/Tmdb/Models/Movies/Trailers.cs new file mode 100644 index 000000000..4bfa02f06 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/Movies/Trailers.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Providers.Tmdb.Models.Movies +{ + public class Trailers + { + public List<Youtube> Youtube { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/Movies/Youtube.cs b/MediaBrowser.Providers/Tmdb/Models/Movies/Youtube.cs new file mode 100644 index 000000000..069572824 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/Movies/Youtube.cs @@ -0,0 +1,9 @@ +namespace MediaBrowser.Providers.Tmdb.Models.Movies +{ + public class Youtube + { + public string Name { get; set; } + public string Size { get; set; } + public string Source { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/People/PersonImages.cs b/MediaBrowser.Providers/Tmdb/Models/People/PersonImages.cs new file mode 100644 index 000000000..113f410b2 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/People/PersonImages.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using MediaBrowser.Providers.Tmdb.Models.General; + +namespace MediaBrowser.Providers.Tmdb.Models.People +{ + public class PersonImages + { + public List<Profile> Profiles { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/People/PersonResult.cs b/MediaBrowser.Providers/Tmdb/Models/People/PersonResult.cs new file mode 100644 index 000000000..6e997050f --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/People/PersonResult.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using MediaBrowser.Providers.Tmdb.Models.General; + +namespace MediaBrowser.Providers.Tmdb.Models.People +{ + public class PersonResult + { + public bool Adult { get; set; } + public List<string> Also_Known_As { get; set; } + public string Biography { get; set; } + public string Birthday { get; set; } + public string Deathday { get; set; } + public string Homepage { get; set; } + public int Id { get; set; } + public string Imdb_Id { get; set; } + public string Name { get; set; } + public string Place_Of_Birth { get; set; } + public double Popularity { get; set; } + public string Profile_Path { get; set; } + public PersonImages Images { get; set; } + public ExternalIds External_Ids { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/Search/ExternalIdLookupResult.cs b/MediaBrowser.Providers/Tmdb/Models/Search/ExternalIdLookupResult.cs new file mode 100644 index 000000000..6d9fe7081 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/Search/ExternalIdLookupResult.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using MediaBrowser.Providers.Movies; + +namespace MediaBrowser.Providers.Tmdb.Models.Search +{ + public class ExternalIdLookupResult + { + public List<TvResult> Tv_Results { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/Search/MovieResult.cs b/MediaBrowser.Providers/Tmdb/Models/Search/MovieResult.cs new file mode 100644 index 000000000..25a211fa8 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/Search/MovieResult.cs @@ -0,0 +1,65 @@ +namespace MediaBrowser.Providers.Tmdb.Models.Search +{ + public class MovieResult + { + /// <summary> + /// Gets or sets a value indicating whether this <see cref="TmdbMovieSearchResult" /> is adult. + /// </summary> + /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value> + public bool Adult { get; set; } + /// <summary> + /// Gets or sets the backdrop_path. + /// </summary> + /// <value>The backdrop_path.</value> + public string Backdrop_Path { get; set; } + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + public int Id { get; set; } + /// <summary> + /// Gets or sets the original_title. + /// </summary> + /// <value>The original_title.</value> + public string Original_Title { get; set; } + /// <summary> + /// Gets or sets the original_name. + /// </summary> + /// <value>The original_name.</value> + public string Original_Name { get; set; } + /// <summary> + /// Gets or sets the release_date. + /// </summary> + /// <value>The release_date.</value> + public string Release_Date { get; set; } + /// <summary> + /// Gets or sets the poster_path. + /// </summary> + /// <value>The poster_path.</value> + public string Poster_Path { get; set; } + /// <summary> + /// Gets or sets the popularity. + /// </summary> + /// <value>The popularity.</value> + public double Popularity { get; set; } + /// <summary> + /// Gets or sets the title. + /// </summary> + /// <value>The title.</value> + public string Title { get; set; } + /// <summary> + /// Gets or sets the vote_average. + /// </summary> + /// <value>The vote_average.</value> + public double Vote_Average { get; set; } + /// <summary> + /// For collection search results + /// </summary> + public string Name { get; set; } + /// <summary> + /// Gets or sets the vote_count. + /// </summary> + /// <value>The vote_count.</value> + public int Vote_Count { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/Search/PersonSearchResult.cs b/MediaBrowser.Providers/Tmdb/Models/Search/PersonSearchResult.cs new file mode 100644 index 000000000..93916068f --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/Search/PersonSearchResult.cs @@ -0,0 +1,29 @@ +namespace MediaBrowser.Providers.Tmdb.Models.Search +{ + public class PersonSearchResult + { + /// <summary> + /// Gets or sets a value indicating whether this <see cref="PersonSearchResult" /> is adult. + /// </summary> + /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value> + public bool Adult { get; set; } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <value>The id.</value> + public int Id { get; set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <value>The name.</value> + public string Name { get; set; } + + /// <summary> + /// Gets or sets the profile_ path. + /// </summary> + /// <value>The profile_ path.</value> + public string Profile_Path { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/Search/TmdbSearchResult.cs b/MediaBrowser.Providers/Tmdb/Models/Search/TmdbSearchResult.cs new file mode 100644 index 000000000..a9f888e75 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/Search/TmdbSearchResult.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Providers.Tmdb.Models.Search +{ + public class TmdbSearchResult<T> + { + /// <summary> + /// Gets or sets the page. + /// </summary> + /// <value>The page.</value> + public int Page { get; set; } + + /// <summary> + /// Gets or sets the results. + /// </summary> + /// <value>The results.</value> + public List<T> Results { get; set; } + + /// <summary> + /// Gets or sets the total_pages. + /// </summary> + /// <value>The total_pages.</value> + public int Total_Pages { get; set; } + + /// <summary> + /// Gets or sets the total_results. + /// </summary> + /// <value>The total_results.</value> + public int Total_Results { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/Search/TvResult.cs b/MediaBrowser.Providers/Tmdb/Models/Search/TvResult.cs new file mode 100644 index 000000000..ed140bedd --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/Search/TvResult.cs @@ -0,0 +1,15 @@ +namespace MediaBrowser.Providers.Tmdb.Models.Search +{ + public class TvResult + { + public string Backdrop_Path { get; set; } + public string First_Air_Date { get; set; } + public int Id { get; set; } + public string Original_Name { get; set; } + public string Poster_Path { get; set; } + public double Popularity { get; set; } + public string Name { get; set; } + public double Vote_Average { get; set; } + public int Vote_Count { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/TV/Cast.cs b/MediaBrowser.Providers/Tmdb/Models/TV/Cast.cs new file mode 100644 index 000000000..c659df9ac --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/TV/Cast.cs @@ -0,0 +1,12 @@ +namespace MediaBrowser.Providers.Tmdb.Models.TV +{ + public class Cast + { + public string Character { get; set; } + public string Credit_Id { get; set; } + public int Id { get; set; } + public string Name { get; set; } + public string Profile_Path { get; set; } + public int Order { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/TV/ContentRating.cs b/MediaBrowser.Providers/Tmdb/Models/TV/ContentRating.cs new file mode 100644 index 000000000..3177cd71b --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/TV/ContentRating.cs @@ -0,0 +1,8 @@ +namespace MediaBrowser.Providers.Tmdb.Models.TV +{ + public class ContentRating + { + public string Iso_3166_1 { get; set; } + public string Rating { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/TV/ContentRatings.cs b/MediaBrowser.Providers/Tmdb/Models/TV/ContentRatings.cs new file mode 100644 index 000000000..883e605c9 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/TV/ContentRatings.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace MediaBrowser.Providers.Tmdb.Models.TV +{ + public class ContentRatings + { + public List<ContentRating> Results { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/TV/CreatedBy.cs b/MediaBrowser.Providers/Tmdb/Models/TV/CreatedBy.cs new file mode 100644 index 000000000..21588d897 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/TV/CreatedBy.cs @@ -0,0 +1,9 @@ +namespace MediaBrowser.Providers.Tmdb.Models.TV +{ + public class CreatedBy + { + public int Id { get; set; } + public string Name { get; set; } + public string Profile_Path { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/TV/Credits.cs b/MediaBrowser.Providers/Tmdb/Models/TV/Credits.cs new file mode 100644 index 000000000..b62b5f605 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/TV/Credits.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using MediaBrowser.Providers.Tmdb.Models.General; + +namespace MediaBrowser.Providers.Tmdb.Models.TV +{ + public class Credits + { + public List<Cast> Cast { get; set; } + public List<Crew> Crew { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/TV/Episode.cs b/MediaBrowser.Providers/Tmdb/Models/TV/Episode.cs new file mode 100644 index 000000000..ab11a6cd2 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/TV/Episode.cs @@ -0,0 +1,14 @@ +namespace MediaBrowser.Providers.Tmdb.Models.TV +{ + public class Episode + { + public string Air_Date { get; set; } + public int Episode_Number { get; set; } + public int Id { get; set; } + public string Name { get; set; } + public string Overview { get; set; } + public string Still_Path { get; set; } + public double Vote_Average { get; set; } + public int Vote_Count { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/TV/EpisodeCredits.cs b/MediaBrowser.Providers/Tmdb/Models/TV/EpisodeCredits.cs new file mode 100644 index 000000000..1c86be0f4 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/TV/EpisodeCredits.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using MediaBrowser.Providers.Tmdb.Models.General; + +namespace MediaBrowser.Providers.Tmdb.Models.TV +{ + public class EpisodeCredits + { + public List<Cast> Cast { get; set; } + public List<Crew> Crew { get; set; } + public List<GuestStar> Guest_Stars { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/TV/EpisodeResult.cs b/MediaBrowser.Providers/Tmdb/Models/TV/EpisodeResult.cs new file mode 100644 index 000000000..0513ce7e2 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/TV/EpisodeResult.cs @@ -0,0 +1,23 @@ +using System; +using MediaBrowser.Providers.Tmdb.Models.General; + +namespace MediaBrowser.Providers.Tmdb.Models.TV +{ + public class EpisodeResult + { + public DateTime Air_Date { get; set; } + public int Episode_Number { get; set; } + public string Name { get; set; } + public string Overview { get; set; } + public int Id { get; set; } + public object Production_Code { get; set; } + public int Season_Number { get; set; } + public string Still_Path { get; set; } + public double Vote_Average { get; set; } + public int Vote_Count { get; set; } + public StillImages Images { get; set; } + public ExternalIds External_Ids { get; set; } + public EpisodeCredits Credits { get; set; } + public Tmdb.Models.General.Videos Videos { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/TV/GuestStar.cs b/MediaBrowser.Providers/Tmdb/Models/TV/GuestStar.cs new file mode 100644 index 000000000..2dfe7a862 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/TV/GuestStar.cs @@ -0,0 +1,12 @@ +namespace MediaBrowser.Providers.Tmdb.Models.TV +{ + public class GuestStar + { + public int Id { get; set; } + public string Name { get; set; } + public string Credit_Id { get; set; } + public string Character { get; set; } + public int Order { get; set; } + public string Profile_Path { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/TV/Network.cs b/MediaBrowser.Providers/Tmdb/Models/TV/Network.cs new file mode 100644 index 000000000..f982682d1 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/TV/Network.cs @@ -0,0 +1,8 @@ +namespace MediaBrowser.Providers.Tmdb.Models.TV +{ + public class Network + { + public int Id { get; set; } + public string Name { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/TV/Season.cs b/MediaBrowser.Providers/Tmdb/Models/TV/Season.cs new file mode 100644 index 000000000..976e3c97e --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/TV/Season.cs @@ -0,0 +1,11 @@ +namespace MediaBrowser.Providers.Tmdb.Models.TV +{ + public class Season + { + public string Air_Date { get; set; } + public int Episode_Count { get; set; } + public int Id { get; set; } + public string Poster_Path { get; set; } + public int Season_Number { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/TV/SeasonImages.cs b/MediaBrowser.Providers/Tmdb/Models/TV/SeasonImages.cs new file mode 100644 index 000000000..9a93dd6ae --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/TV/SeasonImages.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using MediaBrowser.Providers.Tmdb.Models.General; + +namespace MediaBrowser.Providers.Tmdb.Models.TV +{ + public class SeasonImages + { + public List<Poster> Posters { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/TV/SeasonResult.cs b/MediaBrowser.Providers/Tmdb/Models/TV/SeasonResult.cs new file mode 100644 index 000000000..bc9213c04 --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/TV/SeasonResult.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Providers.Tmdb.Models.General; + +namespace MediaBrowser.Providers.Tmdb.Models.TV +{ + public class SeasonResult + { + public DateTime Air_Date { get; set; } + public List<Episode> Episodes { get; set; } + public string Name { get; set; } + public string Overview { get; set; } + public int Id { get; set; } + public string Poster_Path { get; set; } + public int Season_Number { get; set; } + public Credits Credits { get; set; } + public SeasonImages Images { get; set; } + public ExternalIds External_Ids { get; set; } + public General.Videos Videos { get; set; } + } +} diff --git a/MediaBrowser.Providers/Tmdb/Models/TV/SeriesResult.cs b/MediaBrowser.Providers/Tmdb/Models/TV/SeriesResult.cs new file mode 100644 index 000000000..ad95e502e --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Models/TV/SeriesResult.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Providers.Tmdb.Models.General; + +namespace MediaBrowser.Providers.Tmdb.Models.TV +{ + public class SeriesResult + { + public string Backdrop_Path { get; set; } + public List<CreatedBy> Created_By { get; set; } + public List<int> Episode_Run_Time { get; set; } + public DateTime First_Air_Date { get; set; } + public List<Genre> Genres { get; set; } + public string Homepage { get; set; } + public int Id { get; set; } + public bool In_Production { get; set; } + public List<string> Languages { get; set; } + public DateTime Last_Air_Date { get; set; } + public string Name { get; set; } + public List<Network> Networks { get; set; } + public int Number_Of_Episodes { get; set; } + public int Number_Of_Seasons { get; set; } + public string Original_Name { get; set; } + public List<string> Origin_Country { get; set; } + public string Overview { get; set; } + public string Popularity { get; set; } + public string Poster_Path { get; set; } + public List<Season> Seasons { get; set; } + public string Status { get; set; } + public double Vote_Average { get; set; } + public int Vote_Count { get; set; } + public Credits Credits { get; set; } + public Images Images { get; set; } + public Keywords Keywords { get; set; } + public ExternalIds External_Ids { get; set; } + public General.Videos Videos { get; set; } + public ContentRatings Content_Ratings { get; set; } + public string ResultLanguage { get; set; } + } +} diff --git a/MediaBrowser.Providers/Movies/GenericMovieDbInfo.cs b/MediaBrowser.Providers/Tmdb/Movies/GenericTmdbMovieInfo.cs index 4300b84a9..b7b447b68 100644 --- a/MediaBrowser.Providers/Movies/GenericMovieDbInfo.cs +++ b/MediaBrowser.Providers/Tmdb/Movies/GenericTmdbMovieInfo.cs @@ -14,11 +14,13 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.Movies; +using MediaBrowser.Providers.Tmdb.Models.Movies; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.Movies +namespace MediaBrowser.Providers.Tmdb.Movies { - public class GenericMovieDbInfo<T> + public class GenericTmdbMovieInfo<T> where T : BaseItem, new() { private readonly ILogger _logger; @@ -28,7 +30,7 @@ namespace MediaBrowser.Providers.Movies private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - public GenericMovieDbInfo(ILogger logger, IJsonSerializer jsonSerializer, ILibraryManager libraryManager, IFileSystem fileSystem) + public GenericTmdbMovieInfo(ILogger logger, IJsonSerializer jsonSerializer, ILibraryManager libraryManager, IFileSystem fileSystem) { _logger = logger; _jsonSerializer = jsonSerializer; @@ -44,7 +46,7 @@ namespace MediaBrowser.Providers.Movies // Don't search for music video id's because it is very easy to misidentify. if (string.IsNullOrEmpty(tmdbId) && string.IsNullOrEmpty(imdbId) && typeof(T) != typeof(MusicVideo)) { - var searchResults = await new MovieDbSearch(_logger, _jsonSerializer, _libraryManager).GetMovieSearchResults(itemId, cancellationToken).ConfigureAwait(false); + var searchResults = await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetMovieSearchResults(itemId, cancellationToken).ConfigureAwait(false); var searchResult = searchResults.FirstOrDefault(); @@ -81,17 +83,17 @@ namespace MediaBrowser.Providers.Movies }; string dataFilePath = null; - MovieDbProvider.CompleteMovieData movieInfo = null; + MovieResult movieInfo = null; // Id could be ImdbId or TmdbId if (string.IsNullOrEmpty(tmdbId)) { - movieInfo = await MovieDbProvider.Current.FetchMainResult(imdbId, false, language, cancellationToken).ConfigureAwait(false); + movieInfo = await TmdbMovieProvider.Current.FetchMainResult(imdbId, false, language, cancellationToken).ConfigureAwait(false); if (movieInfo != null) { - tmdbId = movieInfo.id.ToString(_usCulture); + tmdbId = movieInfo.Id.ToString(_usCulture); - dataFilePath = MovieDbProvider.Current.GetDataFilePath(tmdbId, language); + dataFilePath = TmdbMovieProvider.Current.GetDataFilePath(tmdbId, language); Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); _jsonSerializer.SerializeToFile(movieInfo, dataFilePath); } @@ -99,12 +101,12 @@ namespace MediaBrowser.Providers.Movies if (!string.IsNullOrWhiteSpace(tmdbId)) { - await MovieDbProvider.Current.EnsureMovieInfo(tmdbId, language, cancellationToken).ConfigureAwait(false); + await TmdbMovieProvider.Current.EnsureMovieInfo(tmdbId, language, cancellationToken).ConfigureAwait(false); - dataFilePath = dataFilePath ?? MovieDbProvider.Current.GetDataFilePath(tmdbId, language); - movieInfo = movieInfo ?? _jsonSerializer.DeserializeFromFile<MovieDbProvider.CompleteMovieData>(dataFilePath); + dataFilePath = dataFilePath ?? TmdbMovieProvider.Current.GetDataFilePath(tmdbId, language); + movieInfo = movieInfo ?? _jsonSerializer.DeserializeFromFile<MovieResult>(dataFilePath); - var settings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); + var settings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); ProcessMainInfo(item, settings, preferredCountryCode, movieInfo); item.HasMetadata = true; @@ -120,7 +122,7 @@ namespace MediaBrowser.Providers.Movies /// <param name="settings">The settings.</param> /// <param name="preferredCountryCode">The preferred country code.</param> /// <param name="movieData">The movie data.</param> - private void ProcessMainInfo(MetadataResult<T> resultItem, TmdbSettingsResult settings, string preferredCountryCode, MovieDbProvider.CompleteMovieData movieData) + private void ProcessMainInfo(MetadataResult<T> resultItem, TmdbSettingsResult settings, string preferredCountryCode, MovieResult movieData) { var movie = resultItem.Item; @@ -128,41 +130,39 @@ namespace MediaBrowser.Providers.Movies movie.OriginalTitle = movieData.GetOriginalTitle(); - movie.Overview = string.IsNullOrWhiteSpace(movieData.overview) ? null : WebUtility.HtmlDecode(movieData.overview); + movie.Overview = string.IsNullOrWhiteSpace(movieData.Overview) ? null : WebUtility.HtmlDecode(movieData.Overview); movie.Overview = movie.Overview != null ? movie.Overview.Replace("\n\n", "\n") : null; //movie.HomePageUrl = movieData.homepage; - if (!string.IsNullOrEmpty(movieData.tagline)) + if (!string.IsNullOrEmpty(movieData.Tagline)) { - movie.Tagline = movieData.tagline; + movie.Tagline = movieData.Tagline; } - if (movieData.production_countries != null) + if (movieData.Production_Countries != null) { movie.ProductionLocations = movieData - .production_countries - .Select(i => i.name) + .Production_Countries + .Select(i => i.Name) .ToArray(); } - movie.SetProviderId(MetadataProviders.Tmdb, movieData.id.ToString(_usCulture)); - movie.SetProviderId(MetadataProviders.Imdb, movieData.imdb_id); + movie.SetProviderId(MetadataProviders.Tmdb, movieData.Id.ToString(_usCulture)); + movie.SetProviderId(MetadataProviders.Imdb, movieData.Imdb_Id); - if (movieData.belongs_to_collection != null) + if (movieData.Belongs_To_Collection != null) { movie.SetProviderId(MetadataProviders.TmdbCollection, - movieData.belongs_to_collection.id.ToString(CultureInfo.InvariantCulture)); + movieData.Belongs_To_Collection.Id.ToString(CultureInfo.InvariantCulture)); - var movieItem = movie as Movie; - - if (movieItem != null) + if (movie is Movie movieItem) { - movieItem.CollectionName = movieData.belongs_to_collection.name; + movieItem.CollectionName = movieData.Belongs_To_Collection.Name; } } - string voteAvg = movieData.vote_average.ToString(CultureInfo.InvariantCulture); + string voteAvg = movieData.Vote_Average.ToString(CultureInfo.InvariantCulture); if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var rating)) { @@ -171,17 +171,17 @@ namespace MediaBrowser.Providers.Movies //movie.VoteCount = movieData.vote_count; - if (movieData.releases != null && movieData.releases.countries != null) + if (movieData.Releases != null && movieData.Releases.Countries != null) { - var releases = movieData.releases.countries.Where(i => !string.IsNullOrWhiteSpace(i.certification)).ToList(); + var releases = movieData.Releases.Countries.Where(i => !string.IsNullOrWhiteSpace(i.Certification)).ToList(); - var ourRelease = releases.FirstOrDefault(c => string.Equals(c.iso_3166_1, preferredCountryCode, StringComparison.OrdinalIgnoreCase)); - var usRelease = releases.FirstOrDefault(c => string.Equals(c.iso_3166_1, "US", StringComparison.OrdinalIgnoreCase)); + var ourRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, preferredCountryCode, StringComparison.OrdinalIgnoreCase)); + var usRelease = releases.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase)); if (ourRelease != null) { var ratingPrefix = string.Equals(preferredCountryCode, "us", StringComparison.OrdinalIgnoreCase) ? "" : preferredCountryCode + "-"; - var newRating = ratingPrefix + ourRelease.certification; + var newRating = ratingPrefix + ourRelease.Certification; newRating = newRating.Replace("de-", "FSK-", StringComparison.OrdinalIgnoreCase); @@ -189,14 +189,14 @@ namespace MediaBrowser.Providers.Movies } else if (usRelease != null) { - movie.OfficialRating = usRelease.certification; + movie.OfficialRating = usRelease.Certification; } } - if (!string.IsNullOrWhiteSpace(movieData.release_date)) + if (!string.IsNullOrWhiteSpace(movieData.Release_Date)) { // These dates are always in this exact format - if (DateTime.TryParse(movieData.release_date, _usCulture, DateTimeStyles.None, out var r)) + if (DateTime.TryParse(movieData.Release_Date, _usCulture, DateTimeStyles.None, out var r)) { movie.PremiereDate = r.ToUniversalTime(); movie.ProductionYear = movie.PremiereDate.Value.Year; @@ -204,16 +204,16 @@ namespace MediaBrowser.Providers.Movies } //studios - if (movieData.production_companies != null) + if (movieData.Production_Companies != null) { - movie.SetStudios(movieData.production_companies.Select(c => c.name)); + movie.SetStudios(movieData.Production_Companies.Select(c => c.Name)); } // genres // Movies get this from imdb - var genres = movieData.genres ?? new List<MovieDbProvider.GenreItem>(); + var genres = movieData.Genres ?? new List<Tmdb.Models.General.Genre>(); - foreach (var genre in genres.Select(g => g.name)) + foreach (var genre in genres.Select(g => g.Name)) { movie.AddGenre(genre); } @@ -223,26 +223,26 @@ namespace MediaBrowser.Providers.Movies //Actors, Directors, Writers - all in People //actors come from cast - if (movieData.casts != null && movieData.casts.cast != null) + if (movieData.Casts != null && movieData.Casts.Cast != null) { - foreach (var actor in movieData.casts.cast.OrderBy(a => a.order)) + foreach (var actor in movieData.Casts.Cast.OrderBy(a => a.Order)) { var personInfo = new PersonInfo { - Name = actor.name.Trim(), - Role = actor.character, + Name = actor.Name.Trim(), + Role = actor.Character, Type = PersonType.Actor, - SortOrder = actor.order + SortOrder = actor.Order }; - if (!string.IsNullOrWhiteSpace(actor.profile_path)) + if (!string.IsNullOrWhiteSpace(actor.Profile_Path)) { - personInfo.ImageUrl = tmdbImageUrl + actor.profile_path; + personInfo.ImageUrl = tmdbImageUrl + actor.Profile_Path; } - if (actor.id > 0) + if (actor.Id > 0) { - personInfo.SetProviderId(MetadataProviders.Tmdb, actor.id.ToString(CultureInfo.InvariantCulture)); + personInfo.SetProviderId(MetadataProviders.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture)); } resultItem.AddPerson(personInfo); @@ -250,45 +250,41 @@ namespace MediaBrowser.Providers.Movies } //and the rest from crew - if (movieData.casts != null && movieData.casts.crew != null) + if (movieData.Casts?.Crew != null) { var keepTypes = new[] { PersonType.Director, - //PersonType.Writer, - //PersonType.Producer + PersonType.Writer, + PersonType.Producer }; - foreach (var person in movieData.casts.crew) + foreach (var person in movieData.Casts.Crew) { // Normalize this - var type = person.department; - if (string.Equals(type, "writing", StringComparison.OrdinalIgnoreCase)) - { - type = PersonType.Writer; - } + var type = TmdbUtils.MapCrewToPersonType(person); - if (!keepTypes.Contains(type ?? string.Empty, StringComparer.OrdinalIgnoreCase) && - !keepTypes.Contains(person.job ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) && + !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase)) { continue; } var personInfo = new PersonInfo { - Name = person.name.Trim(), - Role = person.job, + Name = person.Name.Trim(), + Role = person.Job, Type = type }; - if (!string.IsNullOrWhiteSpace(person.profile_path)) + if (!string.IsNullOrWhiteSpace(person.Profile_Path)) { - personInfo.ImageUrl = tmdbImageUrl + person.profile_path; + personInfo.ImageUrl = tmdbImageUrl + person.Profile_Path; } - if (person.id > 0) + if (person.Id > 0) { - personInfo.SetProviderId(MetadataProviders.Tmdb, person.id.ToString(CultureInfo.InvariantCulture)); + personInfo.SetProviderId(MetadataProviders.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture)); } resultItem.AddPerson(personInfo); @@ -300,12 +296,12 @@ namespace MediaBrowser.Providers.Movies // movie.Keywords = movieData.keywords.keywords.Select(i => i.name).ToList(); //} - if (movieData.trailers != null && movieData.trailers.youtube != null) + if (movieData.Trailers != null && movieData.Trailers.Youtube != null) { - movie.RemoteTrailers = movieData.trailers.youtube.Select(i => new MediaUrl + movie.RemoteTrailers = movieData.Trailers.Youtube.Select(i => new MediaUrl { - Url = string.Format("https://www.youtube.com/watch?v={0}", i.source), - Name = i.name + Url = string.Format("https://www.youtube.com/watch?v={0}", i.Source), + Name = i.Name }).ToArray(); } diff --git a/MediaBrowser.Providers/Movies/MovieDbImageProvider.cs b/MediaBrowser.Providers/Tmdb/Movies/TmdbImageProvider.cs index 20b53d58a..cdb96e6ac 100644 --- a/MediaBrowser.Providers/Movies/MovieDbImageProvider.cs +++ b/MediaBrowser.Providers/Tmdb/Movies/TmdbImageProvider.cs @@ -13,16 +13,19 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.Movies; +using MediaBrowser.Providers.Tmdb.Models.General; +using MediaBrowser.Providers.Tmdb.Models.Movies; -namespace MediaBrowser.Providers.Movies +namespace MediaBrowser.Providers.Tmdb.Movies { - public class MovieDbImageProvider : IRemoteImageProvider, IHasOrder + public class TmdbImageProvider : IRemoteImageProvider, IHasOrder { private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClient _httpClient; private readonly IFileSystem _fileSystem; - public MovieDbImageProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, IFileSystem fileSystem) + public TmdbImageProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, IFileSystem fileSystem) { _jsonSerializer = jsonSerializer; _httpClient = httpClient; @@ -31,7 +34,7 @@ namespace MediaBrowser.Providers.Movies public string Name => ProviderName; - public static string ProviderName => "TheMovieDb"; + public static string ProviderName => TmdbUtils.ProviderName; public bool Supports(BaseItem item) { @@ -60,7 +63,7 @@ namespace MediaBrowser.Providers.Movies return list; } - var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); + var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); @@ -70,12 +73,12 @@ namespace MediaBrowser.Providers.Movies { list.AddRange(GetPosters(results).Select(i => new RemoteImageInfo { - Url = tmdbImageUrl + i.file_path, - CommunityRating = i.vote_average, - VoteCount = i.vote_count, - Width = i.width, - Height = i.height, - Language = MovieDbProvider.AdjustImageLanguage(i.iso_639_1, language), + Url = tmdbImageUrl + i.File_Path, + CommunityRating = i.Vote_Average, + VoteCount = i.Vote_Count, + Width = i.Width, + Height = i.Height, + Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language), ProviderName = Name, Type = ImageType.Primary, RatingType = RatingType.Score @@ -86,11 +89,11 @@ namespace MediaBrowser.Providers.Movies { list.AddRange(GetBackdrops(results).Select(i => new RemoteImageInfo { - Url = tmdbImageUrl + i.file_path, - CommunityRating = i.vote_average, - VoteCount = i.vote_count, - Width = i.width, - Height = i.height, + Url = tmdbImageUrl + i.File_Path, + CommunityRating = i.Vote_Average, + VoteCount = i.Vote_Count, + Width = i.Width, + Height = i.Height, ProviderName = Name, Type = ImageType.Backdrop, RatingType = RatingType.Score @@ -127,9 +130,9 @@ namespace MediaBrowser.Providers.Movies /// </summary> /// <param name="images">The images.</param> /// <returns>IEnumerable{MovieDbProvider.Poster}.</returns> - private IEnumerable<MovieDbProvider.Poster> GetPosters(MovieDbProvider.Images images) + private IEnumerable<Poster> GetPosters(Images images) { - return images.posters ?? new List<MovieDbProvider.Poster>(); + return images.Posters ?? new List<Poster>(); } /// <summary> @@ -137,13 +140,13 @@ namespace MediaBrowser.Providers.Movies /// </summary> /// <param name="images">The images.</param> /// <returns>IEnumerable{MovieDbProvider.Backdrop}.</returns> - private IEnumerable<MovieDbProvider.Backdrop> GetBackdrops(MovieDbProvider.Images images) + private IEnumerable<Backdrop> GetBackdrops(Images images) { - var eligibleBackdrops = images.backdrops == null ? new List<MovieDbProvider.Backdrop>() : - images.backdrops; + var eligibleBackdrops = images.Backdrops == null ? new List<Backdrop>() : + images.Backdrops; - return eligibleBackdrops.OrderByDescending(i => i.vote_average) - .ThenByDescending(i => i.vote_count); + return eligibleBackdrops.OrderByDescending(i => i.Vote_Average) + .ThenByDescending(i => i.Vote_Count); } /// <summary> @@ -154,7 +157,7 @@ namespace MediaBrowser.Providers.Movies /// <param name="jsonSerializer">The json serializer.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task{MovieImages}.</returns> - private async Task<MovieDbProvider.Images> FetchImages(BaseItem item, string language, IJsonSerializer jsonSerializer, CancellationToken cancellationToken) + private async Task<Images> FetchImages(BaseItem item, string language, IJsonSerializer jsonSerializer, CancellationToken cancellationToken) { var tmdbId = item.GetProviderId(MetadataProviders.Tmdb); @@ -163,10 +166,10 @@ namespace MediaBrowser.Providers.Movies var imdbId = item.GetProviderId(MetadataProviders.Imdb); if (!string.IsNullOrWhiteSpace(imdbId)) { - var movieInfo = await MovieDbProvider.Current.FetchMainResult(imdbId, false, language, cancellationToken).ConfigureAwait(false); + var movieInfo = await TmdbMovieProvider.Current.FetchMainResult(imdbId, false, language, cancellationToken).ConfigureAwait(false); if (movieInfo != null) { - tmdbId = movieInfo.id.ToString(CultureInfo.InvariantCulture); + tmdbId = movieInfo.Id.ToString(CultureInfo.InvariantCulture); } } } @@ -176,9 +179,9 @@ namespace MediaBrowser.Providers.Movies return null; } - await MovieDbProvider.Current.EnsureMovieInfo(tmdbId, language, cancellationToken).ConfigureAwait(false); + await TmdbMovieProvider.Current.EnsureMovieInfo(tmdbId, language, cancellationToken).ConfigureAwait(false); - var path = MovieDbProvider.Current.GetDataFilePath(tmdbId, language); + var path = TmdbMovieProvider.Current.GetDataFilePath(tmdbId, language); if (!string.IsNullOrEmpty(path)) { @@ -186,7 +189,7 @@ namespace MediaBrowser.Providers.Movies if (fileInfo.Exists) { - return jsonSerializer.DeserializeFromFile<MovieDbProvider.CompleteMovieData>(path).images; + return jsonSerializer.DeserializeFromFile<MovieResult>(path).Images; } } diff --git a/MediaBrowser.Providers/Tmdb/Movies/TmdbMovieExternalId.cs b/MediaBrowser.Providers/Tmdb/Movies/TmdbMovieExternalId.cs new file mode 100644 index 000000000..fc7a4583f --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/Movies/TmdbMovieExternalId.cs @@ -0,0 +1,32 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Tmdb.Movies +{ + public class TmdbMovieExternalId : IExternalId + { + /// <inheritdoc /> + public string Name => TmdbUtils.ProviderName; + + /// <inheritdoc /> + public string Key => MetadataProviders.Tmdb.ToString(); + + /// <inheritdoc /> + public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "movie/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) + { + // Supports images for tv movies + if (item is LiveTvProgram tvProgram && tvProgram.IsMovie) + { + return true; + } + + return item is Movie || item is MusicVideo || item is Trailer; + } + } +} diff --git a/MediaBrowser.Providers/Movies/MovieDbProvider.cs b/MediaBrowser.Providers/Tmdb/Movies/TmdbMovieProvider.cs index 3ff63a4bf..a1bea5847 100644 --- a/MediaBrowser.Providers/Movies/MovieDbProvider.cs +++ b/MediaBrowser.Providers/Tmdb/Movies/TmdbMovieProvider.cs @@ -19,16 +19,18 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.Movies; +using MediaBrowser.Providers.Tmdb.Models.Movies; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.Movies +namespace MediaBrowser.Providers.Tmdb.Movies { /// <summary> /// Class MovieDbProvider /// </summary> - public class MovieDbProvider : IRemoteMetadataProvider<Movie, MovieInfo>, IHasOrder + public class TmdbMovieProvider : IRemoteMetadataProvider<Movie, MovieInfo>, IHasOrder { - internal static MovieDbProvider Current { get; private set; } + internal static TmdbMovieProvider Current { get; private set; } private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClient _httpClient; @@ -41,7 +43,7 @@ namespace MediaBrowser.Providers.Movies private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - public MovieDbProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILogger logger, ILocalizationManager localization, ILibraryManager libraryManager, IApplicationHost appHost) + public TmdbMovieProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILogger logger, ILocalizationManager localization, ILibraryManager libraryManager, IApplicationHost appHost) { _jsonSerializer = jsonSerializer; _httpClient = httpClient; @@ -71,7 +73,7 @@ namespace MediaBrowser.Providers.Movies var dataFilePath = GetDataFilePath(tmdbId, searchInfo.MetadataLanguage); - var obj = _jsonSerializer.DeserializeFromFile<CompleteMovieData>(dataFilePath); + var obj = _jsonSerializer.DeserializeFromFile<MovieResult>(dataFilePath); var tmdbSettings = await GetTmdbSettings(cancellationToken).ConfigureAwait(false); @@ -81,30 +83,30 @@ namespace MediaBrowser.Providers.Movies { Name = obj.GetTitle(), SearchProviderName = Name, - ImageUrl = string.IsNullOrWhiteSpace(obj.poster_path) ? null : tmdbImageUrl + obj.poster_path + ImageUrl = string.IsNullOrWhiteSpace(obj.Poster_Path) ? null : tmdbImageUrl + obj.Poster_Path }; - if (!string.IsNullOrWhiteSpace(obj.release_date)) + if (!string.IsNullOrWhiteSpace(obj.Release_Date)) { // These dates are always in this exact format - if (DateTime.TryParse(obj.release_date, _usCulture, DateTimeStyles.None, out var r)) + if (DateTime.TryParse(obj.Release_Date, _usCulture, DateTimeStyles.None, out var r)) { remoteResult.PremiereDate = r.ToUniversalTime(); remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year; } } - remoteResult.SetProviderId(MetadataProviders.Tmdb, obj.id.ToString(_usCulture)); + remoteResult.SetProviderId(MetadataProviders.Tmdb, obj.Id.ToString(_usCulture)); - if (!string.IsNullOrWhiteSpace(obj.imdb_id)) + if (!string.IsNullOrWhiteSpace(obj.Imdb_Id)) { - remoteResult.SetProviderId(MetadataProviders.Imdb, obj.imdb_id); + remoteResult.SetProviderId(MetadataProviders.Imdb, obj.Imdb_Id); } return new[] { remoteResult }; } - return await new MovieDbSearch(_logger, _jsonSerializer, _libraryManager).GetMovieSearchResults(searchInfo, cancellationToken).ConfigureAwait(false); + return await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetMovieSearchResults(searchInfo, cancellationToken).ConfigureAwait(false); } public Task<MetadataResult<Movie>> GetMetadata(MovieInfo info, CancellationToken cancellationToken) @@ -115,12 +117,12 @@ namespace MediaBrowser.Providers.Movies public Task<MetadataResult<T>> GetItemMetadata<T>(ItemLookupInfo id, CancellationToken cancellationToken) where T : BaseItem, new() { - var movieDb = new GenericMovieDbInfo<T>(_logger, _jsonSerializer, _libraryManager, _fileSystem); + var movieDb = new GenericTmdbMovieInfo<T>(_logger, _jsonSerializer, _libraryManager, _fileSystem); return movieDb.GetMetadata(id, cancellationToken); } - public string Name => "TheMovieDb"; + public string Name => TmdbUtils.ProviderName; /// <summary> /// The _TMDB settings task @@ -140,9 +142,9 @@ namespace MediaBrowser.Providers.Movies using (HttpResponseInfo response = await GetMovieDbResponse(new HttpRequestOptions { - Url = string.Format(TmdbConfigUrl, ApiKey), + Url = string.Format(TmdbConfigUrl, TmdbUtils.ApiKey), CancellationToken = cancellationToken, - AcceptHeader = AcceptHeader + AcceptHeader = TmdbUtils.AcceptHeader }).ConfigureAwait(false)) { @@ -155,13 +157,8 @@ namespace MediaBrowser.Providers.Movies } } - public const string BaseMovieDbUrl = "https://api.themoviedb.org/"; - - private const string TmdbConfigUrl = BaseMovieDbUrl + "3/configuration?api_key={0}"; - private const string GetMovieInfo3 = BaseMovieDbUrl + @"3/movie/{0}?api_key={1}&append_to_response=casts,releases,images,keywords,trailers"; - - internal static string ApiKey = "4219e299c89411838049ab0dab19ebd5"; - internal static string AcceptHeader = "application/json,image/*"; + private const string TmdbConfigUrl = TmdbUtils.BaseTmdbApiUrl + "3/configuration?api_key={0}"; + private const string GetMovieInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/movie/{0}?api_key={1}&append_to_response=casts,releases,images,keywords,trailers"; /// <summary> /// Gets the movie data path. @@ -314,9 +311,9 @@ namespace MediaBrowser.Providers.Movies /// <param name="language">The language.</param> /// <param name="cancellationToken">The cancellation token</param> /// <returns>Task{CompleteMovieData}.</returns> - internal async Task<CompleteMovieData> FetchMainResult(string id, bool isTmdbId, string language, CancellationToken cancellationToken) + internal async Task<MovieResult> FetchMainResult(string id, bool isTmdbId, string language, CancellationToken cancellationToken) { - var url = string.Format(GetMovieInfo3, id, ApiKey); + var url = string.Format(GetMovieInfo3, id, TmdbUtils.ApiKey); if (!string.IsNullOrEmpty(language)) { @@ -326,7 +323,7 @@ namespace MediaBrowser.Providers.Movies url += "&include_image_language=" + GetImageLanguagesParam(language); } - CompleteMovieData mainResult; + MovieResult mainResult; cancellationToken.ThrowIfCancellationRequested(); @@ -340,7 +337,7 @@ namespace MediaBrowser.Providers.Movies { Url = url, CancellationToken = cancellationToken, - AcceptHeader = AcceptHeader, + AcceptHeader = TmdbUtils.AcceptHeader, CacheMode = cacheMode, CacheLength = cacheLength @@ -348,7 +345,7 @@ namespace MediaBrowser.Providers.Movies { using (var json = response.Content) { - mainResult = await _jsonSerializer.DeserializeFromStreamAsync<CompleteMovieData>(json).ConfigureAwait(false); + mainResult = await _jsonSerializer.DeserializeFromStreamAsync<MovieResult>(json).ConfigureAwait(false); } } } @@ -367,13 +364,13 @@ namespace MediaBrowser.Providers.Movies // If the language preference isn't english, then have the overview fallback to english if it's blank if (mainResult != null && - string.IsNullOrEmpty(mainResult.overview) && + string.IsNullOrEmpty(mainResult.Overview) && !string.IsNullOrEmpty(language) && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)) { _logger.LogInformation("MovieDbProvider couldn't find meta for language " + language + ". Trying English..."); - url = string.Format(GetMovieInfo3, id, ApiKey) + "&language=en"; + url = string.Format(GetMovieInfo3, id, TmdbUtils.ApiKey) + "&language=en"; if (!string.IsNullOrEmpty(language)) { @@ -385,7 +382,7 @@ namespace MediaBrowser.Providers.Movies { Url = url, CancellationToken = cancellationToken, - AcceptHeader = AcceptHeader, + AcceptHeader = TmdbUtils.AcceptHeader, CacheMode = cacheMode, CacheLength = cacheLength @@ -393,9 +390,9 @@ namespace MediaBrowser.Providers.Movies { using (var json = response.Content) { - var englishResult = await _jsonSerializer.DeserializeFromStreamAsync<CompleteMovieData>(json).ConfigureAwait(false); + var englishResult = await _jsonSerializer.DeserializeFromStreamAsync<MovieResult>(json).ConfigureAwait(false); - mainResult.overview = englishResult.overview; + mainResult.Overview = englishResult.Overview; } } } @@ -429,205 +426,6 @@ namespace MediaBrowser.Providers.Movies return await _httpClient.SendAsync(options, "GET").ConfigureAwait(false); } - /// <summary> - /// Class TmdbTitle - /// </summary> - internal class TmdbTitle - { - /// <summary> - /// Gets or sets the iso_3166_1. - /// </summary> - /// <value>The iso_3166_1.</value> - public string iso_3166_1 { get; set; } - /// <summary> - /// Gets or sets the title. - /// </summary> - /// <value>The title.</value> - public string title { get; set; } - } - - /// <summary> - /// Class TmdbAltTitleResults - /// </summary> - internal class TmdbAltTitleResults - { - /// <summary> - /// Gets or sets the id. - /// </summary> - /// <value>The id.</value> - public int id { get; set; } - /// <summary> - /// Gets or sets the titles. - /// </summary> - /// <value>The titles.</value> - public List<TmdbTitle> titles { get; set; } - } - - internal class BelongsToCollection - { - public int id { get; set; } - public string name { get; set; } - public string poster_path { get; set; } - public string backdrop_path { get; set; } - } - - internal class GenreItem - { - public int id { get; set; } - public string name { get; set; } - } - - internal class ProductionCompany - { - public string name { get; set; } - public int id { get; set; } - } - - internal class ProductionCountry - { - public string iso_3166_1 { get; set; } - public string name { get; set; } - } - - internal class SpokenLanguage - { - public string iso_639_1 { get; set; } - public string name { get; set; } - } - - internal class Cast - { - public int id { get; set; } - public string name { get; set; } - public string character { get; set; } - public int order { get; set; } - public int cast_id { get; set; } - public string profile_path { get; set; } - } - - internal class Crew - { - public int id { get; set; } - public string name { get; set; } - public string department { get; set; } - public string job { get; set; } - public string profile_path { get; set; } - } - - internal class Casts - { - public List<Cast> cast { get; set; } - public List<Crew> crew { get; set; } - } - - internal class Country - { - public string iso_3166_1 { get; set; } - public string certification { get; set; } - public DateTime release_date { get; set; } - } - - internal class Releases - { - public List<Country> countries { get; set; } - } - - internal class Backdrop - { - 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 double vote_average { get; set; } - public int vote_count { get; set; } - } - - internal class Poster - { - public string file_path { get; set; } - public int width { get; set; } - public int height { get; set; } - public string iso_639_1 { get; set; } - public double aspect_ratio { get; set; } - public double vote_average { get; set; } - public int vote_count { get; set; } - } - - internal class Images - { - public List<Backdrop> backdrops { get; set; } - public List<Poster> posters { get; set; } - } - - internal class Keyword - { - public int id { get; set; } - public string name { get; set; } - } - - internal class Keywords - { - public List<Keyword> keywords { get; set; } - } - - internal class Youtube - { - public string name { get; set; } - public string size { get; set; } - public string source { get; set; } - } - - internal class Trailers - { - public List<object> quicktime { get; set; } - public List<Youtube> youtube { get; set; } - } - - internal class CompleteMovieData - { - public bool adult { get; set; } - public string backdrop_path { get; set; } - public BelongsToCollection belongs_to_collection { get; set; } - public int budget { get; set; } - public List<GenreItem> genres { get; set; } - public string homepage { get; set; } - public int id { get; set; } - public string imdb_id { get; set; } - public string original_title { get; set; } - public string original_name { get; set; } - public string overview { get; set; } - public double popularity { get; set; } - public string poster_path { get; set; } - public List<ProductionCompany> production_companies { get; set; } - public List<ProductionCountry> production_countries { get; set; } - public string release_date { get; set; } - public int revenue { get; set; } - public int runtime { get; set; } - public List<SpokenLanguage> spoken_languages { get; set; } - public string status { get; set; } - public string tagline { get; set; } - public string title { get; set; } - public string name { get; set; } - public double vote_average { get; set; } - public int vote_count { get; set; } - public Casts casts { get; set; } - public Releases releases { get; set; } - public Images images { get; set; } - public Keywords keywords { get; set; } - public Trailers trailers { get; set; } - - public string GetOriginalTitle() - { - return original_name ?? original_title; - } - - public string GetTitle() - { - return name ?? title ?? GetOriginalTitle(); - } - } - public int Order => 1; public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/Movies/MovieDbSearch.cs b/MediaBrowser.Providers/Tmdb/Movies/TmdbSearch.cs index e466d40a0..223cef086 100644 --- a/MediaBrowser.Providers/Movies/MovieDbSearch.cs +++ b/MediaBrowser.Providers/Tmdb/Movies/TmdbSearch.cs @@ -11,23 +11,21 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; +using MediaBrowser.Providers.Tmdb.Models.Search; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.Movies +namespace MediaBrowser.Providers.Tmdb.Movies { - public class MovieDbSearch + public class TmdbSearch { private static readonly CultureInfo EnUs = new CultureInfo("en-US"); - private const string Search3 = MovieDbProvider.BaseMovieDbUrl + @"3/search/{3}?api_key={1}&query={0}&language={2}"; - - internal static string ApiKey = "4219e299c89411838049ab0dab19ebd5"; - internal static string AcceptHeader = "application/json,image/*"; + private const string Search3 = TmdbUtils.BaseTmdbApiUrl + @"3/search/{3}?api_key={1}&query={0}&language={2}"; private readonly ILogger _logger; private readonly IJsonSerializer _json; private readonly ILibraryManager _libraryManager; - public MovieDbSearch(ILogger logger, IJsonSerializer json, ILibraryManager libraryManager) + public TmdbSearch(ILogger logger, IJsonSerializer json, ILibraryManager libraryManager) { _logger = logger; _json = json; @@ -59,7 +57,7 @@ namespace MediaBrowser.Providers.Movies return new List<RemoteSearchResult>(); } - var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); + var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); @@ -152,43 +150,43 @@ namespace MediaBrowser.Providers.Movies throw new ArgumentException("name"); } - var url3 = string.Format(Search3, WebUtility.UrlEncode(name), ApiKey, language, type); + var url3 = string.Format(Search3, WebUtility.UrlEncode(name), TmdbUtils.ApiKey, language, type); - using (var response = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions + using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions { Url = url3, CancellationToken = cancellationToken, - AcceptHeader = AcceptHeader + AcceptHeader = TmdbUtils.AcceptHeader }).ConfigureAwait(false)) { using (var json = response.Content) { - var searchResults = await _json.DeserializeFromStreamAsync<TmdbMovieSearchResults>(json).ConfigureAwait(false); + var searchResults = await _json.DeserializeFromStreamAsync<TmdbSearchResult<MovieResult>>(json).ConfigureAwait(false); - var results = searchResults.results ?? new List<TmdbMovieSearchResult>(); + var results = searchResults.Results ?? new List<MovieResult>(); return results .Select(i => { var remoteResult = new RemoteSearchResult { - SearchProviderName = MovieDbProvider.Current.Name, - Name = i.title ?? i.name ?? i.original_title, - ImageUrl = string.IsNullOrWhiteSpace(i.poster_path) ? null : baseImageUrl + i.poster_path + SearchProviderName = TmdbMovieProvider.Current.Name, + Name = i.Title ?? i.Name ?? i.Original_Title, + ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path }; - if (!string.IsNullOrWhiteSpace(i.release_date)) + if (!string.IsNullOrWhiteSpace(i.Release_Date)) { // These dates are always in this exact format - if (DateTime.TryParseExact(i.release_date, "yyyy-MM-dd", EnUs, DateTimeStyles.None, out var r)) + if (DateTime.TryParseExact(i.Release_Date, "yyyy-MM-dd", EnUs, DateTimeStyles.None, out var r)) { remoteResult.PremiereDate = r.ToUniversalTime(); remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year; } } - remoteResult.SetProviderId(MetadataProviders.Tmdb, i.id.ToString(EnUs)); + remoteResult.SetProviderId(MetadataProviders.Tmdb, i.Id.ToString(EnUs)); return remoteResult; @@ -205,43 +203,43 @@ namespace MediaBrowser.Providers.Movies throw new ArgumentException("name"); } - var url3 = string.Format(Search3, WebUtility.UrlEncode(name), ApiKey, language, "tv"); + var url3 = string.Format(Search3, WebUtility.UrlEncode(name), TmdbUtils.ApiKey, language, "tv"); - using (var response = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions + using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions { Url = url3, CancellationToken = cancellationToken, - AcceptHeader = AcceptHeader + AcceptHeader = TmdbUtils.AcceptHeader }).ConfigureAwait(false)) { using (var json = response.Content) { - var searchResults = await _json.DeserializeFromStreamAsync<TmdbTvSearchResults>(json).ConfigureAwait(false); + var searchResults = await _json.DeserializeFromStreamAsync<TmdbSearchResult<TvResult>>(json).ConfigureAwait(false); - var results = searchResults.results ?? new List<TvResult>(); + var results = searchResults.Results ?? new List<TvResult>(); return results .Select(i => { var remoteResult = new RemoteSearchResult { - SearchProviderName = MovieDbProvider.Current.Name, - Name = i.name ?? i.original_name, - ImageUrl = string.IsNullOrWhiteSpace(i.poster_path) ? null : baseImageUrl + i.poster_path + SearchProviderName = TmdbMovieProvider.Current.Name, + Name = i.Name ?? i.Original_Name, + ImageUrl = string.IsNullOrWhiteSpace(i.Poster_Path) ? null : baseImageUrl + i.Poster_Path }; - if (!string.IsNullOrWhiteSpace(i.first_air_date)) + if (!string.IsNullOrWhiteSpace(i.First_Air_Date)) { // These dates are always in this exact format - if (DateTime.TryParseExact(i.first_air_date, "yyyy-MM-dd", EnUs, DateTimeStyles.None, out var r)) + if (DateTime.TryParseExact(i.First_Air_Date, "yyyy-MM-dd", EnUs, DateTimeStyles.None, out var r)) { remoteResult.PremiereDate = r.ToUniversalTime(); remoteResult.ProductionYear = remoteResult.PremiereDate.Value.Year; } } - remoteResult.SetProviderId(MetadataProviders.Tmdb, i.id.ToString(EnUs)); + remoteResult.SetProviderId(MetadataProviders.Tmdb, i.Id.ToString(EnUs)); return remoteResult; @@ -250,145 +248,5 @@ namespace MediaBrowser.Providers.Movies } } } - - /// <summary> - /// Class TmdbMovieSearchResult - /// </summary> - public class TmdbMovieSearchResult - { - /// <summary> - /// Gets or sets a value indicating whether this <see cref="TmdbMovieSearchResult" /> is adult. - /// </summary> - /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value> - public bool adult { get; set; } - /// <summary> - /// Gets or sets the backdrop_path. - /// </summary> - /// <value>The backdrop_path.</value> - public string backdrop_path { get; set; } - /// <summary> - /// Gets or sets the id. - /// </summary> - /// <value>The id.</value> - public int id { get; set; } - /// <summary> - /// Gets or sets the original_title. - /// </summary> - /// <value>The original_title.</value> - public string original_title { get; set; } - /// <summary> - /// Gets or sets the original_name. - /// </summary> - /// <value>The original_name.</value> - public string original_name { get; set; } - /// <summary> - /// Gets or sets the release_date. - /// </summary> - /// <value>The release_date.</value> - public string release_date { get; set; } - /// <summary> - /// Gets or sets the poster_path. - /// </summary> - /// <value>The poster_path.</value> - public string poster_path { get; set; } - /// <summary> - /// Gets or sets the popularity. - /// </summary> - /// <value>The popularity.</value> - public double popularity { get; set; } - /// <summary> - /// Gets or sets the title. - /// </summary> - /// <value>The title.</value> - public string title { get; set; } - /// <summary> - /// Gets or sets the vote_average. - /// </summary> - /// <value>The vote_average.</value> - public double vote_average { get; set; } - /// <summary> - /// For collection search results - /// </summary> - public string name { get; set; } - /// <summary> - /// Gets or sets the vote_count. - /// </summary> - /// <value>The vote_count.</value> - public int vote_count { get; set; } - } - - /// <summary> - /// Class TmdbMovieSearchResults - /// </summary> - private class TmdbMovieSearchResults - { - /// <summary> - /// Gets or sets the page. - /// </summary> - /// <value>The page.</value> - public int page { get; set; } - /// <summary> - /// Gets or sets the results. - /// </summary> - /// <value>The results.</value> - public List<TmdbMovieSearchResult> results { get; set; } - /// <summary> - /// Gets or sets the total_pages. - /// </summary> - /// <value>The total_pages.</value> - public int total_pages { get; set; } - /// <summary> - /// Gets or sets the total_results. - /// </summary> - /// <value>The total_results.</value> - public int total_results { get; set; } - } - - public class TvResult - { - public string backdrop_path { get; set; } - public string first_air_date { get; set; } - public int id { get; set; } - public string original_name { get; set; } - public string poster_path { get; set; } - public double popularity { get; set; } - public string name { get; set; } - public double vote_average { get; set; } - public int vote_count { get; set; } - } - - /// <summary> - /// Class TmdbTvSearchResults - /// </summary> - private class TmdbTvSearchResults - { - /// <summary> - /// Gets or sets the page. - /// </summary> - /// <value>The page.</value> - public int page { get; set; } - /// <summary> - /// Gets or sets the results. - /// </summary> - /// <value>The results.</value> - public List<TvResult> results { get; set; } - /// <summary> - /// Gets or sets the total_pages. - /// </summary> - /// <value>The total_pages.</value> - public int total_pages { get; set; } - /// <summary> - /// Gets or sets the total_results. - /// </summary> - /// <value>The total_results.</value> - public int total_results { get; set; } - } - - public class ExternalIdLookupResult - { - public List<object> movie_results { get; set; } - public List<object> person_results { get; set; } - public List<TvResult> tv_results { get; set; } - } } } diff --git a/MediaBrowser.Providers/Movies/TmdbSettings.cs b/MediaBrowser.Providers/Tmdb/Movies/TmdbSettings.cs index 119bbfca6..dca406b99 100644 --- a/MediaBrowser.Providers/Movies/TmdbSettings.cs +++ b/MediaBrowser.Providers/Tmdb/Movies/TmdbSettings.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace MediaBrowser.Providers.Movies +namespace MediaBrowser.Providers.Tmdb.Movies { internal class TmdbImageSettings { diff --git a/MediaBrowser.Providers/Music/MovieDbMusicVideoProvider.cs b/MediaBrowser.Providers/Tmdb/Music/TmdbMusicVideoProvider.cs index 506cbfb89..f3f8a92cf 100644 --- a/MediaBrowser.Providers/Music/MovieDbMusicVideoProvider.cs +++ b/MediaBrowser.Providers/Tmdb/Music/TmdbMusicVideoProvider.cs @@ -7,14 +7,15 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Providers; using MediaBrowser.Providers.Movies; +using MediaBrowser.Providers.Tmdb.Movies; -namespace MediaBrowser.Providers.Music +namespace MediaBrowser.Providers.Tmdb.Music { - public class MovieDbMusicVideoProvider : IRemoteMetadataProvider<MusicVideo, MusicVideoInfo> + public class TmdbMusicVideoProvider : IRemoteMetadataProvider<MusicVideo, MusicVideoInfo> { public Task<MetadataResult<MusicVideo>> GetMetadata(MusicVideoInfo info, CancellationToken cancellationToken) { - return MovieDbProvider.Current.GetItemMetadata<MusicVideo>(info, cancellationToken); + return TmdbMovieProvider.Current.GetItemMetadata<MusicVideo>(info, cancellationToken); } public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(MusicVideoInfo searchInfo, CancellationToken cancellationToken) @@ -22,7 +23,7 @@ namespace MediaBrowser.Providers.Music return Task.FromResult((IEnumerable<RemoteSearchResult>)new List<RemoteSearchResult>()); } - public string Name => MovieDbProvider.Current.Name; + public string Name => TmdbMovieProvider.Current.Name; public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) { diff --git a/MediaBrowser.Providers/Tmdb/People/TmdbPersonExternalId.cs b/MediaBrowser.Providers/Tmdb/People/TmdbPersonExternalId.cs new file mode 100644 index 000000000..2c61bc70a --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/People/TmdbPersonExternalId.cs @@ -0,0 +1,24 @@ +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Tmdb.People +{ + public class TmdbPersonExternalId : IExternalId + { + /// <inheritdoc /> + public string Name => TmdbUtils.ProviderName; + + /// <inheritdoc /> + public string Key => MetadataProviders.Tmdb.ToString(); + + /// <inheritdoc /> + public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "person/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) + { + return item is Person; + } + } +} diff --git a/MediaBrowser.Providers/People/MovieDbPersonImageProvider.cs b/MediaBrowser.Providers/Tmdb/People/TmdbPersonImageProvider.cs index 3019ee506..44ccbf453 100644 --- a/MediaBrowser.Providers/People/MovieDbPersonImageProvider.cs +++ b/MediaBrowser.Providers/Tmdb/People/TmdbPersonImageProvider.cs @@ -11,16 +11,19 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; using MediaBrowser.Providers.Movies; +using MediaBrowser.Providers.Tmdb.Models.General; +using MediaBrowser.Providers.Tmdb.Models.People; +using MediaBrowser.Providers.Tmdb.Movies; -namespace MediaBrowser.Providers.People +namespace MediaBrowser.Providers.Tmdb.People { - public class MovieDbPersonImageProvider : IRemoteImageProvider, IHasOrder + public class TmdbPersonImageProvider : IRemoteImageProvider, IHasOrder { private readonly IServerConfigurationManager _config; private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClient _httpClient; - public MovieDbPersonImageProvider(IServerConfigurationManager config, IJsonSerializer jsonSerializer, IHttpClient httpClient) + public TmdbPersonImageProvider(IServerConfigurationManager config, IJsonSerializer jsonSerializer, IHttpClient httpClient) { _config = config; _jsonSerializer = jsonSerializer; @@ -29,7 +32,7 @@ namespace MediaBrowser.Providers.People public string Name => ProviderName; - public static string ProviderName => "TheMovieDb"; + public static string ProviderName => TmdbUtils.ProviderName; public bool Supports(BaseItem item) { @@ -51,15 +54,15 @@ namespace MediaBrowser.Providers.People if (!string.IsNullOrEmpty(id)) { - await MovieDbPersonProvider.Current.EnsurePersonInfo(id, cancellationToken).ConfigureAwait(false); + await TmdbPersonProvider.Current.EnsurePersonInfo(id, cancellationToken).ConfigureAwait(false); - var dataFilePath = MovieDbPersonProvider.GetPersonDataFilePath(_config.ApplicationPaths, id); + var dataFilePath = TmdbPersonProvider.GetPersonDataFilePath(_config.ApplicationPaths, id); - var result = _jsonSerializer.DeserializeFromFile<MovieDbPersonProvider.PersonResult>(dataFilePath); + var result = _jsonSerializer.DeserializeFromFile<PersonResult>(dataFilePath); - var images = result.images ?? new MovieDbPersonProvider.Images(); + var images = result.Images ?? new PersonImages(); - var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); + var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); @@ -69,20 +72,20 @@ namespace MediaBrowser.Providers.People return new List<RemoteImageInfo>(); } - private IEnumerable<RemoteImageInfo> GetImages(MovieDbPersonProvider.Images images, string preferredLanguage, string baseImageUrl) + private IEnumerable<RemoteImageInfo> GetImages(PersonImages images, string preferredLanguage, string baseImageUrl) { var list = new List<RemoteImageInfo>(); - if (images.profiles != null) + if (images.Profiles != null) { - list.AddRange(images.profiles.Select(i => new RemoteImageInfo + list.AddRange(images.Profiles.Select(i => new RemoteImageInfo { ProviderName = Name, Type = ImageType.Primary, - Width = i.width, - Height = i.height, + Width = i.Width, + Height = i.Height, Language = GetLanguage(i), - Url = baseImageUrl + i.file_path + Url = baseImageUrl + i.File_Path })); } @@ -113,9 +116,9 @@ namespace MediaBrowser.Providers.People .ThenByDescending(i => i.VoteCount ?? 0); } - private string GetLanguage(MovieDbPersonProvider.Profile profile) + private string GetLanguage(Profile profile) { - return profile.iso_639_1 == null ? null : profile.iso_639_1.ToString(); + return profile.Iso_639_1?.ToString(); } public int Order => 0; diff --git a/MediaBrowser.Providers/People/MovieDbPersonProvider.cs b/MediaBrowser.Providers/Tmdb/People/TmdbPersonProvider.cs index 6d9d66f80..130403e4d 100644 --- a/MediaBrowser.Providers/People/MovieDbPersonProvider.cs +++ b/MediaBrowser.Providers/Tmdb/People/TmdbPersonProvider.cs @@ -17,16 +17,19 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; -using MediaBrowser.Providers.Movies; +using MediaBrowser.Providers.Tmdb.Models.General; +using MediaBrowser.Providers.Tmdb.Models.People; +using MediaBrowser.Providers.Tmdb.Models.Search; +using MediaBrowser.Providers.Tmdb.Movies; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.People +namespace MediaBrowser.Providers.Tmdb.People { - public class MovieDbPersonProvider : IRemoteMetadataProvider<Person, PersonLookupInfo> + public class TmdbPersonProvider : IRemoteMetadataProvider<Person, PersonLookupInfo> { const string DataFileName = "info.json"; - internal static MovieDbPersonProvider Current { get; private set; } + internal static TmdbPersonProvider Current { get; private set; } private readonly IJsonSerializer _jsonSerializer; private readonly IFileSystem _fileSystem; @@ -34,7 +37,7 @@ namespace MediaBrowser.Providers.People private readonly IHttpClient _httpClient; private readonly ILogger _logger; - public MovieDbPersonProvider(IFileSystem fileSystem, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IHttpClient httpClient, ILogger logger) + public TmdbPersonProvider(IFileSystem fileSystem, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IHttpClient httpClient, ILogger logger) { _fileSystem = fileSystem; _configurationManager = configurationManager; @@ -44,13 +47,13 @@ namespace MediaBrowser.Providers.People Current = this; } - public string Name => "TheMovieDb"; + public string Name => TmdbUtils.ProviderName; public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(PersonLookupInfo searchInfo, CancellationToken cancellationToken) { var tmdbId = searchInfo.GetProviderId(MetadataProviders.Tmdb); - var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); + var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); @@ -61,19 +64,19 @@ namespace MediaBrowser.Providers.People var dataFilePath = GetPersonDataFilePath(_configurationManager.ApplicationPaths, tmdbId); var info = _jsonSerializer.DeserializeFromFile<PersonResult>(dataFilePath); - var images = (info.images ?? new Images()).profiles ?? new List<Profile>(); + var images = (info.Images ?? new PersonImages()).Profiles ?? new List<Profile>(); var result = new RemoteSearchResult { - Name = info.name, + Name = info.Name, SearchProviderName = Name, - ImageUrl = images.Count == 0 ? null : (tmdbImageUrl + images[0].file_path) + ImageUrl = images.Count == 0 ? null : (tmdbImageUrl + images[0].File_Path) }; - result.SetProviderId(MetadataProviders.Tmdb, info.id.ToString(_usCulture)); - result.SetProviderId(MetadataProviders.Imdb, info.imdb_id); + result.SetProviderId(MetadataProviders.Tmdb, info.Id.ToString(_usCulture)); + result.SetProviderId(MetadataProviders.Imdb, info.Imdb_Id); return new[] { result }; } @@ -84,20 +87,20 @@ namespace MediaBrowser.Providers.People return new List<RemoteSearchResult>(); } - var url = string.Format(MovieDbProvider.BaseMovieDbUrl + @"3/search/person?api_key={1}&query={0}", WebUtility.UrlEncode(searchInfo.Name), MovieDbProvider.ApiKey); + var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/search/person?api_key={1}&query={0}", WebUtility.UrlEncode(searchInfo.Name), TmdbUtils.ApiKey); - using (var response = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions + using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions { Url = url, CancellationToken = cancellationToken, - AcceptHeader = MovieDbProvider.AcceptHeader + AcceptHeader = TmdbUtils.AcceptHeader }).ConfigureAwait(false)) { using (var json = response.Content) { - var result = await _jsonSerializer.DeserializeFromStreamAsync<PersonSearchResults>(json).ConfigureAwait(false) ?? - new PersonSearchResults(); + var result = await _jsonSerializer.DeserializeFromStreamAsync<TmdbSearchResult<PersonSearchResult>>(json).ConfigureAwait(false) ?? + new TmdbSearchResult<PersonSearchResult>(); return result.Results.Select(i => GetSearchResult(i, tmdbImageUrl)); } @@ -112,7 +115,7 @@ namespace MediaBrowser.Providers.People Name = i.Name, - ImageUrl = string.IsNullOrEmpty(i.Profile_Path) ? null : (baseImageUrl + i.Profile_Path) + ImageUrl = string.IsNullOrEmpty(i.Profile_Path) ? null : baseImageUrl + i.Profile_Path }; result.SetProviderId(MetadataProviders.Tmdb, i.Id.ToString(_usCulture)); @@ -161,27 +164,27 @@ namespace MediaBrowser.Providers.People //item.HomePageUrl = info.homepage; - if (!string.IsNullOrWhiteSpace(info.place_of_birth)) + if (!string.IsNullOrWhiteSpace(info.Place_Of_Birth)) { - item.ProductionLocations = new string[] { info.place_of_birth }; + item.ProductionLocations = new string[] { info.Place_Of_Birth }; } - item.Overview = info.biography; + item.Overview = info.Biography; - if (DateTime.TryParseExact(info.birthday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out var date)) + if (DateTime.TryParseExact(info.Birthday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out var date)) { item.PremiereDate = date.ToUniversalTime(); } - if (DateTime.TryParseExact(info.deathday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out date)) + if (DateTime.TryParseExact(info.Deathday, "yyyy-MM-dd", new CultureInfo("en-US"), DateTimeStyles.None, out date)) { item.EndDate = date.ToUniversalTime(); } - item.SetProviderId(MetadataProviders.Tmdb, info.id.ToString(_usCulture)); + item.SetProviderId(MetadataProviders.Tmdb, info.Id.ToString(_usCulture)); - if (!string.IsNullOrEmpty(info.imdb_id)) + if (!string.IsNullOrEmpty(info.Imdb_Id)) { - item.SetProviderId(MetadataProviders.Imdb, info.imdb_id); + item.SetProviderId(MetadataProviders.Imdb, info.Imdb_Id); } result.HasMetadata = true; @@ -217,13 +220,13 @@ namespace MediaBrowser.Providers.People return; } - var url = string.Format(MovieDbProvider.BaseMovieDbUrl + @"3/person/{1}?api_key={0}&append_to_response=credits,images,external_ids", MovieDbProvider.ApiKey, id); + var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/person/{1}?api_key={0}&append_to_response=credits,images,external_ids", TmdbUtils.ApiKey, id); - using (var response = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions + using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions { Url = url, CancellationToken = cancellationToken, - AcceptHeader = MovieDbProvider.AcceptHeader + AcceptHeader = TmdbUtils.AcceptHeader }).ConfigureAwait(false)) { @@ -256,133 +259,6 @@ namespace MediaBrowser.Providers.People return Path.Combine(appPaths.CachePath, "tmdb-people"); } - #region Result Objects - /// <summary> - /// Class PersonSearchResult - /// </summary> - public class PersonSearchResult - { - /// <summary> - /// Gets or sets a value indicating whether this <see cref="PersonSearchResult" /> is adult. - /// </summary> - /// <value><c>true</c> if adult; otherwise, <c>false</c>.</value> - public bool Adult { get; set; } - /// <summary> - /// Gets or sets the id. - /// </summary> - /// <value>The id.</value> - public int Id { get; set; } - /// <summary> - /// Gets or sets the name. - /// </summary> - /// <value>The name.</value> - public string Name { get; set; } - /// <summary> - /// Gets or sets the profile_ path. - /// </summary> - /// <value>The profile_ path.</value> - public string Profile_Path { get; set; } - } - - /// <summary> - /// Class PersonSearchResults - /// </summary> - public class PersonSearchResults - { - /// <summary> - /// Gets or sets the page. - /// </summary> - /// <value>The page.</value> - public int Page { get; set; } - /// <summary> - /// Gets or sets the results. - /// </summary> - /// <value>The results.</value> - public List<PersonSearchResult> Results { get; set; } - /// <summary> - /// Gets or sets the total_ pages. - /// </summary> - /// <value>The total_ pages.</value> - public int Total_Pages { get; set; } - /// <summary> - /// Gets or sets the total_ results. - /// </summary> - /// <value>The total_ results.</value> - public int Total_Results { get; set; } - } - - public class Cast - { - public int id { get; set; } - public string title { get; set; } - public string character { get; set; } - public string original_title { get; set; } - public string poster_path { get; set; } - public string release_date { get; set; } - public bool adult { get; set; } - } - - public class Crew - { - public int id { get; set; } - public string title { get; set; } - public string original_title { get; set; } - public string department { get; set; } - public string job { get; set; } - public string poster_path { get; set; } - public string release_date { get; set; } - public bool adult { get; set; } - } - - public class Credits - { - public List<Cast> cast { get; set; } - public List<Crew> crew { get; set; } - } - - 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 class Images - { - public List<Profile> profiles { get; set; } - } - - public class ExternalIds - { - public string imdb_id { get; set; } - public string freebase_mid { get; set; } - public string freebase_id { get; set; } - public int tvrage_id { get; set; } - } - - public class PersonResult - { - public bool adult { get; set; } - public List<object> also_known_as { get; set; } - public string biography { get; set; } - public string birthday { get; set; } - public string deathday { get; set; } - public string homepage { get; set; } - public int id { get; set; } - public string imdb_id { get; set; } - public string name { get; set; } - public string place_of_birth { get; set; } - public double popularity { get; set; } - public string profile_path { get; set; } - public Credits credits { get; set; } - public Images images { get; set; } - public ExternalIds external_ids { get; set; } - } - - #endregion - public Task<HttpResponseInfo> GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClient.GetResponse(new HttpRequestOptions diff --git a/MediaBrowser.Providers/TV/TheMovieDb/MovieDbEpisodeImageProvider.cs b/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeImageProvider.cs index e4248afe1..51e7891a1 100644 --- a/MediaBrowser.Providers/TV/TheMovieDb/MovieDbEpisodeImageProvider.cs +++ b/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeImageProvider.cs @@ -14,16 +14,18 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; using MediaBrowser.Providers.Movies; +using MediaBrowser.Providers.Tmdb.Models.General; +using MediaBrowser.Providers.Tmdb.Movies; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.TV.TheMovieDb +namespace MediaBrowser.Providers.Tmdb.TV { - public class MovieDbEpisodeImageProvider : - MovieDbProviderBase, + public class TmdbEpisodeImageProvider : + TmdbEpisodeProviderBase, IRemoteImageProvider, IHasOrder { - public MovieDbEpisodeImageProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory) + public TmdbEpisodeImageProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory) : base(httpClient, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory) { } @@ -62,18 +64,18 @@ namespace MediaBrowser.Providers.TV.TheMovieDb var response = await GetEpisodeInfo(seriesId, seasonNumber.Value, episodeNumber.Value, language, cancellationToken).ConfigureAwait(false); - var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); + var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); - list.AddRange(GetPosters(response.images).Select(i => new RemoteImageInfo + list.AddRange(GetPosters(response.Images).Select(i => new RemoteImageInfo { - Url = tmdbImageUrl + i.file_path, - CommunityRating = i.vote_average, - VoteCount = i.vote_count, - Width = i.width, - Height = i.height, - Language = MovieDbProvider.AdjustImageLanguage(i.iso_639_1, language), + Url = tmdbImageUrl + i.File_Path, + CommunityRating = i.Vote_Average, + VoteCount = i.Vote_Count, + Width = i.Width, + Height = i.Height, + Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language), ProviderName = Name, Type = ImageType.Primary, RatingType = RatingType.Score @@ -106,9 +108,9 @@ namespace MediaBrowser.Providers.TV.TheMovieDb } - private IEnumerable<Still> GetPosters(Images images) + private IEnumerable<Still> GetPosters(StillImages images) { - return images.stills ?? new List<Still>(); + return images.Stills ?? new List<Still>(); } @@ -117,12 +119,13 @@ namespace MediaBrowser.Providers.TV.TheMovieDb return GetResponse(url, cancellationToken); } - public string Name => "TheMovieDb"; + public string Name => TmdbUtils.ProviderName; public bool Supports(BaseItem item) { return item is Controller.Entities.TV.Episode; } + // After TheTvDb public int Order => 1; } diff --git a/MediaBrowser.Providers/TV/TheMovieDb/MovieDbEpisodeProvider.cs b/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeProvider.cs index 3d7745085..a17f5d17a 100644 --- a/MediaBrowser.Providers/TV/TheMovieDb/MovieDbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeProvider.cs @@ -18,14 +18,14 @@ using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.TV.TheMovieDb +namespace MediaBrowser.Providers.Tmdb.TV { - public class MovieDbEpisodeProvider : - MovieDbProviderBase, + public class TmdbEpisodeProvider : + TmdbEpisodeProviderBase, IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder { - public MovieDbEpisodeProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory) + public TmdbEpisodeProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory) : base(httpClient, configurationManager, jsonSerializer, fileSystem, localization, loggerFactory) { } @@ -93,7 +93,7 @@ namespace MediaBrowser.Providers.TV.TheMovieDb result.HasMetadata = true; result.QueriedById = true; - if (!string.IsNullOrEmpty(response.overview)) + if (!string.IsNullOrEmpty(response.Overview)) { // if overview is non-empty, we can assume that localized data was returned result.ResultLanguage = info.MetadataLanguage; @@ -107,30 +107,29 @@ namespace MediaBrowser.Providers.TV.TheMovieDb item.ParentIndexNumber = info.ParentIndexNumber; item.IndexNumberEnd = info.IndexNumberEnd; - if (response.external_ids.tvdb_id > 0) + if (response.External_Ids.Tvdb_Id > 0) { - item.SetProviderId(MetadataProviders.Tvdb, response.external_ids.tvdb_id.ToString(CultureInfo.InvariantCulture)); + item.SetProviderId(MetadataProviders.Tvdb, response.External_Ids.Tvdb_Id.ToString(CultureInfo.InvariantCulture)); } - item.PremiereDate = response.air_date; + item.PremiereDate = response.Air_Date; item.ProductionYear = result.Item.PremiereDate.Value.Year; - item.Name = response.name; - item.Overview = response.overview; + item.Name = response.Name; + item.Overview = response.Overview; - item.CommunityRating = (float)response.vote_average; - //item.VoteCount = response.vote_count; + item.CommunityRating = (float)response.Vote_Average; - if (response.videos != null && response.videos.results != null) + if (response.Videos?.Results != null) { - foreach (var video in response.videos.results) + foreach (var video in response.Videos.Results) { - if (video.type.Equals("trailer", System.StringComparison.OrdinalIgnoreCase) - || video.type.Equals("clip", System.StringComparison.OrdinalIgnoreCase)) + if (video.Type.Equals("trailer", System.StringComparison.OrdinalIgnoreCase) + || video.Type.Equals("clip", System.StringComparison.OrdinalIgnoreCase)) { - if (video.site.Equals("youtube", System.StringComparison.OrdinalIgnoreCase)) + if (video.Site.Equals("youtube", System.StringComparison.OrdinalIgnoreCase)) { - var videoUrl = string.Format("http://www.youtube.com/watch?v={0}", video.key); + var videoUrl = string.Format("http://www.youtube.com/watch?v={0}", video.Key); item.AddTrailerUrl(videoUrl); } } @@ -139,54 +138,50 @@ namespace MediaBrowser.Providers.TV.TheMovieDb result.ResetPeople(); - var credits = response.credits; + var credits = response.Credits; if (credits != null) { //Actors, Directors, Writers - all in People //actors come from cast - if (credits.cast != null) + if (credits.Cast != null) { - foreach (var actor in credits.cast.OrderBy(a => a.order)) + foreach (var actor in credits.Cast.OrderBy(a => a.Order)) { - result.AddPerson(new PersonInfo { Name = actor.name.Trim(), Role = actor.character, Type = PersonType.Actor, SortOrder = actor.order }); + result.AddPerson(new PersonInfo { Name = actor.Name.Trim(), Role = actor.Character, Type = PersonType.Actor, SortOrder = actor.Order }); } } // guest stars - if (credits.guest_stars != null) + if (credits.Guest_Stars != null) { - foreach (var guest in credits.guest_stars.OrderBy(a => a.order)) + foreach (var guest in credits.Guest_Stars.OrderBy(a => a.Order)) { - result.AddPerson(new PersonInfo { Name = guest.name.Trim(), Role = guest.character, Type = PersonType.GuestStar, SortOrder = guest.order }); + result.AddPerson(new PersonInfo { Name = guest.Name.Trim(), Role = guest.Character, Type = PersonType.GuestStar, SortOrder = guest.Order }); } } //and the rest from crew - if (credits.crew != null) + if (credits.Crew != null) { var keepTypes = new[] { PersonType.Director, - //PersonType.Writer, - //PersonType.Producer + PersonType.Writer, + PersonType.Producer }; - foreach (var person in credits.crew) + foreach (var person in credits.Crew) { // Normalize this - var type = person.department; - if (string.Equals(type, "writing", StringComparison.OrdinalIgnoreCase)) - { - type = PersonType.Writer; - } + var type = TmdbUtils.MapCrewToPersonType(person); - if (!keepTypes.Contains(type ?? string.Empty, StringComparer.OrdinalIgnoreCase) && - !keepTypes.Contains(person.job ?? string.Empty, StringComparer.OrdinalIgnoreCase)) + if (!keepTypes.Contains(type, StringComparer.OrdinalIgnoreCase) && + !keepTypes.Contains(person.Job ?? string.Empty, StringComparer.OrdinalIgnoreCase)) { continue; } - result.AddPerson(new PersonInfo { Name = person.name.Trim(), Role = person.job, Type = type }); + result.AddPerson(new PersonInfo { Name = person.Name.Trim(), Role = person.Job, Type = type }); } } } @@ -211,6 +206,6 @@ namespace MediaBrowser.Providers.TV.TheMovieDb // After TheTvDb public int Order => 1; - public string Name => "TheMovieDb"; + public string Name => TmdbUtils.ProviderName; } } diff --git a/MediaBrowser.Providers/TV/TheMovieDb/MovieDbProviderBase.cs b/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeProviderBase.cs index 6e438ebd8..2003261c9 100644 --- a/MediaBrowser.Providers/TV/TheMovieDb/MovieDbProviderBase.cs +++ b/MediaBrowser.Providers/Tmdb/TV/TmdbEpisodeProviderBase.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Globalization; using System.IO; using System.Threading; @@ -10,13 +9,15 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using MediaBrowser.Providers.Movies; +using MediaBrowser.Providers.Tmdb.Models.TV; +using MediaBrowser.Providers.Tmdb.Movies; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.TV.TheMovieDb +namespace MediaBrowser.Providers.Tmdb.TV { - public abstract class MovieDbProviderBase + public abstract class TmdbEpisodeProviderBase { - private const string EpisodeUrlPattern = MovieDbProvider.BaseMovieDbUrl + @"3/tv/{0}/season/{1}/episode/{2}?api_key={3}&append_to_response=images,external_ids,credits,videos"; + private const string EpisodeUrlPattern = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}/season/{1}/episode/{2}?api_key={3}&append_to_response=images,external_ids,credits,videos"; private readonly IHttpClient _httpClient; private readonly IServerConfigurationManager _configurationManager; private readonly IJsonSerializer _jsonSerializer; @@ -24,7 +25,7 @@ namespace MediaBrowser.Providers.TV.TheMovieDb private readonly ILocalizationManager _localization; private readonly ILogger _logger; - public MovieDbProviderBase(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory) + protected TmdbEpisodeProviderBase(IHttpClient httpClient, IServerConfigurationManager configurationManager, IJsonSerializer jsonSerializer, IFileSystem fileSystem, ILocalizationManager localization, ILoggerFactory loggerFactory) { _httpClient = httpClient; _configurationManager = configurationManager; @@ -36,7 +37,7 @@ namespace MediaBrowser.Providers.TV.TheMovieDb protected ILogger Logger => _logger; - protected async Task<RootObject> GetEpisodeInfo(string seriesTmdbId, int season, int episodeNumber, string preferredMetadataLanguage, + protected async Task<EpisodeResult> GetEpisodeInfo(string seriesTmdbId, int season, int episodeNumber, string preferredMetadataLanguage, CancellationToken cancellationToken) { await EnsureEpisodeInfo(seriesTmdbId, season, episodeNumber, preferredMetadataLanguage, cancellationToken) @@ -44,7 +45,7 @@ namespace MediaBrowser.Providers.TV.TheMovieDb var dataFilePath = GetDataFilePath(seriesTmdbId, season, episodeNumber, preferredMetadataLanguage); - return _jsonSerializer.DeserializeFromFile<RootObject>(dataFilePath); + return _jsonSerializer.DeserializeFromFile<EpisodeResult>(dataFilePath); } internal Task EnsureEpisodeInfo(string tmdbId, int seasonNumber, int episodeNumber, string language, CancellationToken cancellationToken) @@ -85,7 +86,7 @@ namespace MediaBrowser.Providers.TV.TheMovieDb throw new ArgumentNullException(nameof(preferredLanguage)); } - var path = MovieDbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId); + var path = TmdbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId); var filename = string.Format("season-{0}-episode-{1}-{2}.json", seasonNumber.ToString(CultureInfo.InvariantCulture), @@ -105,32 +106,32 @@ namespace MediaBrowser.Providers.TV.TheMovieDb _jsonSerializer.SerializeToFile(mainResult, dataFilePath); } - internal async Task<RootObject> FetchMainResult(string urlPattern, string id, int seasonNumber, int episodeNumber, string language, CancellationToken cancellationToken) + internal async Task<EpisodeResult> FetchMainResult(string urlPattern, string id, int seasonNumber, int episodeNumber, string language, CancellationToken cancellationToken) { - var url = string.Format(urlPattern, id, seasonNumber.ToString(CultureInfo.InvariantCulture), episodeNumber, MovieDbProvider.ApiKey); + var url = string.Format(urlPattern, id, seasonNumber.ToString(CultureInfo.InvariantCulture), episodeNumber, TmdbUtils.ApiKey); if (!string.IsNullOrEmpty(language)) { url += string.Format("&language={0}", language); } - var includeImageLanguageParam = MovieDbProvider.GetImageLanguagesParam(language); + var includeImageLanguageParam = TmdbMovieProvider.GetImageLanguagesParam(language); // Get images in english and with no language url += "&include_image_language=" + includeImageLanguageParam; cancellationToken.ThrowIfCancellationRequested(); - using (var response = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions + using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions { Url = url, CancellationToken = cancellationToken, - AcceptHeader = MovieDbProvider.AcceptHeader + AcceptHeader = TmdbUtils.AcceptHeader }).ConfigureAwait(false)) { using (var json = response.Content) { - return await _jsonSerializer.DeserializeFromStreamAsync<RootObject>(json).ConfigureAwait(false); + return await _jsonSerializer.DeserializeFromStreamAsync<EpisodeResult>(json).ConfigureAwait(false); } } } @@ -143,103 +144,5 @@ namespace MediaBrowser.Providers.TV.TheMovieDb Url = url }); } - - public class Still - { - public double aspect_ratio { get; set; } - public string file_path { get; set; } - public int height { get; set; } - public string id { get; set; } - public string iso_639_1 { get; set; } - public double vote_average { get; set; } - public int vote_count { get; set; } - public int width { get; set; } - } - - public class Images - { - public List<Still> stills { get; set; } - } - - public class ExternalIds - { - public string imdb_id { get; set; } - public object freebase_id { get; set; } - public string freebase_mid { get; set; } - public int tvdb_id { get; set; } - public int tvrage_id { get; set; } - } - - public class Cast - { - public string character { get; set; } - public string credit_id { get; set; } - public int id { get; set; } - public string name { get; set; } - public string profile_path { get; set; } - public int order { get; set; } - } - - public class Crew - { - public int id { get; set; } - public string credit_id { get; set; } - public string name { get; set; } - public string department { get; set; } - public string job { get; set; } - public string profile_path { get; set; } - } - - public class GuestStar - { - public int id { get; set; } - public string name { get; set; } - public string credit_id { get; set; } - public string character { get; set; } - public int order { get; set; } - public string profile_path { get; set; } - } - - public class Credits - { - public List<Cast> cast { get; set; } - public List<Crew> crew { get; set; } - public List<GuestStar> guest_stars { get; set; } - } - - public class Videos - { - public List<Video> results { get; set; } - } - - public class Video - { - public string id { get; set; } - public string iso_639_1 { get; set; } - public string iso_3166_1 { get; set; } - public string key { get; set; } - public string name { get; set; } - public string site { get; set; } - public string size { get; set; } - public string type { get; set; } - } - - public class RootObject - { - public DateTime air_date { get; set; } - public int episode_number { get; set; } - public string name { get; set; } - public string overview { get; set; } - public int id { get; set; } - public object production_code { get; set; } - public int season_number { get; set; } - public string still_path { get; set; } - public double vote_average { get; set; } - public int vote_count { get; set; } - public Images images { get; set; } - public ExternalIds external_ids { get; set; } - public Credits credits { get; set; } - public Videos videos { get; set; } - } } } diff --git a/MediaBrowser.Providers/TV/TheMovieDb/MovieDbSeasonProvider.cs b/MediaBrowser.Providers/Tmdb/TV/TmdbSeasonProvider.cs index 6be1b101d..2f2ac58e8 100644 --- a/MediaBrowser.Providers/TV/TheMovieDb/MovieDbSeasonProvider.cs +++ b/MediaBrowser.Providers/Tmdb/TV/TmdbSeasonProvider.cs @@ -7,7 +7,6 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; @@ -16,13 +15,16 @@ using MediaBrowser.Model.Net; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; using MediaBrowser.Providers.Movies; +using MediaBrowser.Providers.Tmdb.Models.TV; +using MediaBrowser.Providers.Tmdb.Movies; using Microsoft.Extensions.Logging; +using Season = MediaBrowser.Controller.Entities.TV.Season; -namespace MediaBrowser.Providers.TV.TheMovieDb +namespace MediaBrowser.Providers.Tmdb.TV { - public class MovieDbSeasonProvider : IRemoteMetadataProvider<Season, SeasonInfo> + public class TmdbSeasonProvider : IRemoteMetadataProvider<Season, SeasonInfo> { - private const string GetTvInfo3 = MovieDbProvider.BaseMovieDbUrl + @"3/tv/{0}/season/{1}?api_key={2}&append_to_response=images,keywords,external_ids,credits,videos"; + private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}/season/{1}?api_key={2}&append_to_response=images,keywords,external_ids,credits,videos"; private readonly IHttpClient _httpClient; private readonly IServerConfigurationManager _configurationManager; private readonly IJsonSerializer _jsonSerializer; @@ -30,7 +32,7 @@ namespace MediaBrowser.Providers.TV.TheMovieDb private readonly ILocalizationManager _localization; private readonly ILogger _logger; - public MovieDbSeasonProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization, IJsonSerializer jsonSerializer, ILoggerFactory loggerFactory) + public TmdbSeasonProvider(IHttpClient httpClient, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization, IJsonSerializer jsonSerializer, ILoggerFactory loggerFactory) { _httpClient = httpClient; _configurationManager = configurationManager; @@ -65,31 +67,31 @@ namespace MediaBrowser.Providers.TV.TheMovieDb result.Item.IndexNumber = seasonNumber; - result.Item.Overview = seasonInfo.overview; + result.Item.Overview = seasonInfo.Overview; - if (seasonInfo.external_ids.tvdb_id > 0) + if (seasonInfo.External_Ids.Tvdb_Id > 0) { - result.Item.SetProviderId(MetadataProviders.Tvdb, seasonInfo.external_ids.tvdb_id.ToString(CultureInfo.InvariantCulture)); + result.Item.SetProviderId(MetadataProviders.Tvdb, seasonInfo.External_Ids.Tvdb_Id.ToString(CultureInfo.InvariantCulture)); } - var credits = seasonInfo.credits; + var credits = seasonInfo.Credits; if (credits != null) { //Actors, Directors, Writers - all in People //actors come from cast - if (credits.cast != null) + if (credits.Cast != null) { //foreach (var actor in credits.cast.OrderBy(a => a.order)) result.Item.AddPerson(new PersonInfo { Name = actor.name.Trim(), Role = actor.character, Type = PersonType.Actor, SortOrder = actor.order }); } //and the rest from crew - if (credits.crew != null) + if (credits.Crew != null) { //foreach (var person in credits.crew) result.Item.AddPerson(new PersonInfo { Name = person.name.Trim(), Role = person.job, Type = person.department }); } } - result.Item.PremiereDate = seasonInfo.air_date; + result.Item.PremiereDate = seasonInfo.Air_Date; result.Item.ProductionYear = result.Item.PremiereDate.Value.Year; } catch (HttpException ex) @@ -108,7 +110,7 @@ namespace MediaBrowser.Providers.TV.TheMovieDb return result; } - public string Name => "TheMovieDb"; + public string Name => TmdbUtils.ProviderName; public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeasonInfo searchInfo, CancellationToken cancellationToken) { @@ -124,7 +126,7 @@ namespace MediaBrowser.Providers.TV.TheMovieDb }); } - private async Task<RootObject> GetSeasonInfo(string seriesTmdbId, int season, string preferredMetadataLanguage, + private async Task<SeasonResult> GetSeasonInfo(string seriesTmdbId, int season, string preferredMetadataLanguage, CancellationToken cancellationToken) { await EnsureSeasonInfo(seriesTmdbId, season, preferredMetadataLanguage, cancellationToken) @@ -132,7 +134,7 @@ namespace MediaBrowser.Providers.TV.TheMovieDb var dataFilePath = GetDataFilePath(seriesTmdbId, season, preferredMetadataLanguage); - return _jsonSerializer.DeserializeFromFile<RootObject>(dataFilePath); + return _jsonSerializer.DeserializeFromFile<SeasonResult>(dataFilePath); } internal Task EnsureSeasonInfo(string tmdbId, int seasonNumber, string language, CancellationToken cancellationToken) @@ -173,7 +175,7 @@ namespace MediaBrowser.Providers.TV.TheMovieDb throw new ArgumentNullException(nameof(preferredLanguage)); } - var path = MovieDbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId); + var path = TmdbSeriesProvider.GetSeriesDataPath(_configurationManager.ApplicationPaths, tmdbId); var filename = string.Format("season-{0}-{1}.json", seasonNumber.ToString(CultureInfo.InvariantCulture), @@ -192,117 +194,34 @@ namespace MediaBrowser.Providers.TV.TheMovieDb _jsonSerializer.SerializeToFile(mainResult, dataFilePath); } - internal async Task<RootObject> FetchMainResult(string id, int seasonNumber, string language, CancellationToken cancellationToken) + internal async Task<SeasonResult> FetchMainResult(string id, int seasonNumber, string language, CancellationToken cancellationToken) { - var url = string.Format(GetTvInfo3, id, seasonNumber.ToString(CultureInfo.InvariantCulture), MovieDbProvider.ApiKey); + var url = string.Format(GetTvInfo3, id, seasonNumber.ToString(CultureInfo.InvariantCulture), TmdbUtils.ApiKey); if (!string.IsNullOrEmpty(language)) { - url += string.Format("&language={0}", MovieDbProvider.NormalizeLanguage(language)); + url += string.Format("&language={0}", TmdbMovieProvider.NormalizeLanguage(language)); } - var includeImageLanguageParam = MovieDbProvider.GetImageLanguagesParam(language); + var includeImageLanguageParam = TmdbMovieProvider.GetImageLanguagesParam(language); // Get images in english and with no language url += "&include_image_language=" + includeImageLanguageParam; cancellationToken.ThrowIfCancellationRequested(); - using (var response = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions + using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions { Url = url, CancellationToken = cancellationToken, - AcceptHeader = MovieDbProvider.AcceptHeader + AcceptHeader = TmdbUtils.AcceptHeader }).ConfigureAwait(false)) { using (var json = response.Content) { - return await _jsonSerializer.DeserializeFromStreamAsync<RootObject>(json).ConfigureAwait(false); + return await _jsonSerializer.DeserializeFromStreamAsync<SeasonResult>(json).ConfigureAwait(false); } } } - - public class Episode - { - public string air_date { get; set; } - public int episode_number { get; set; } - public int id { get; set; } - public string name { get; set; } - public string overview { get; set; } - public string still_path { get; set; } - public double vote_average { get; set; } - public int vote_count { get; set; } - } - - public class Cast - { - public string character { get; set; } - public string credit_id { get; set; } - public int id { get; set; } - public string name { get; set; } - public string profile_path { get; set; } - public int order { get; set; } - } - - public class Crew - { - public string credit_id { get; set; } - public string department { get; set; } - public int id { get; set; } - public string name { get; set; } - public string job { get; set; } - public string profile_path { get; set; } - } - - public class Credits - { - public List<Cast> cast { get; set; } - public List<Crew> crew { get; set; } - } - - public class Poster - { - public double aspect_ratio { get; set; } - public string file_path { get; set; } - public int height { get; set; } - public string id { get; set; } - public string iso_639_1 { get; set; } - public double vote_average { get; set; } - public int vote_count { get; set; } - public int width { get; set; } - } - - public class Images - { - public List<Poster> posters { get; set; } - } - - public class ExternalIds - { - public string freebase_id { get; set; } - public string freebase_mid { get; set; } - public int tvdb_id { get; set; } - public object tvrage_id { get; set; } - } - - public class Videos - { - public List<object> results { get; set; } - } - - public class RootObject - { - public DateTime air_date { get; set; } - public List<Episode> episodes { get; set; } - public string name { get; set; } - public string overview { get; set; } - public int id { get; set; } - public string poster_path { get; set; } - public int season_number { get; set; } - public Credits credits { get; set; } - public Images images { get; set; } - public ExternalIds external_ids { get; set; } - public Videos videos { get; set; } - } } } diff --git a/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesExternalId.cs b/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesExternalId.cs new file mode 100644 index 000000000..524a3b05e --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesExternalId.cs @@ -0,0 +1,24 @@ +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Tmdb.TV +{ + public class TmdbSeriesExternalId : IExternalId + { + /// <inheritdoc /> + public string Name => TmdbUtils.ProviderName; + + /// <inheritdoc /> + public string Key => MetadataProviders.Tmdb.ToString(); + + /// <inheritdoc /> + public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "tv/{0}"; + + /// <inheritdoc /> + public bool Supports(IHasProviderIds item) + { + return item is Series; + } + } +} diff --git a/MediaBrowser.Providers/TV/TheMovieDb/MovieDbSeriesImageProvider.cs b/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesImageProvider.cs index 26686356f..882ec7574 100644 --- a/MediaBrowser.Providers/TV/TheMovieDb/MovieDbSeriesImageProvider.cs +++ b/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesImageProvider.cs @@ -13,16 +13,19 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; using MediaBrowser.Providers.Movies; +using MediaBrowser.Providers.Tmdb.Models.General; +using MediaBrowser.Providers.Tmdb.Models.TV; +using MediaBrowser.Providers.Tmdb.Movies; -namespace MediaBrowser.Providers.TV.TheMovieDb +namespace MediaBrowser.Providers.Tmdb.TV { - public class MovieDbSeriesImageProvider : IRemoteImageProvider, IHasOrder + public class TmdbSeriesImageProvider : IRemoteImageProvider, IHasOrder { private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClient _httpClient; private readonly IFileSystem _fileSystem; - public MovieDbSeriesImageProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, IFileSystem fileSystem) + public TmdbSeriesImageProvider(IJsonSerializer jsonSerializer, IHttpClient httpClient, IFileSystem fileSystem) { _jsonSerializer = jsonSerializer; _httpClient = httpClient; @@ -31,7 +34,7 @@ namespace MediaBrowser.Providers.TV.TheMovieDb public string Name => ProviderName; - public static string ProviderName => "TheMovieDb"; + public static string ProviderName => TmdbUtils.ProviderName; public bool Supports(BaseItem item) { @@ -58,7 +61,7 @@ namespace MediaBrowser.Providers.TV.TheMovieDb return list; } - var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); + var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); @@ -66,12 +69,12 @@ namespace MediaBrowser.Providers.TV.TheMovieDb list.AddRange(GetPosters(results).Select(i => new RemoteImageInfo { - Url = tmdbImageUrl + i.file_path, - CommunityRating = i.vote_average, - VoteCount = i.vote_count, - Width = i.width, - Height = i.height, - Language = MovieDbProvider.AdjustImageLanguage(i.iso_639_1, language), + Url = tmdbImageUrl + i.File_Path, + CommunityRating = i.Vote_Average, + VoteCount = i.Vote_Count, + Width = i.Width, + Height = i.Height, + Language = TmdbMovieProvider.AdjustImageLanguage(i.Iso_639_1, language), ProviderName = Name, Type = ImageType.Primary, RatingType = RatingType.Score @@ -79,11 +82,11 @@ namespace MediaBrowser.Providers.TV.TheMovieDb list.AddRange(GetBackdrops(results).Select(i => new RemoteImageInfo { - Url = tmdbImageUrl + i.file_path, - CommunityRating = i.vote_average, - VoteCount = i.vote_count, - Width = i.width, - Height = i.height, + Url = tmdbImageUrl + i.File_Path, + CommunityRating = i.Vote_Average, + VoteCount = i.Vote_Count, + Width = i.Width, + Height = i.Height, ProviderName = Name, Type = ImageType.Backdrop, RatingType = RatingType.Score @@ -118,22 +121,21 @@ namespace MediaBrowser.Providers.TV.TheMovieDb /// Gets the posters. /// </summary> /// <param name="images">The images.</param> - private IEnumerable<MovieDbSeriesProvider.Poster> GetPosters(MovieDbSeriesProvider.Images images) + private IEnumerable<Poster> GetPosters(Images images) { - return images.posters ?? new List<MovieDbSeriesProvider.Poster>(); + return images.Posters ?? new List<Poster>(); } /// <summary> /// Gets the backdrops. /// </summary> /// <param name="images">The images.</param> - private IEnumerable<MovieDbSeriesProvider.Backdrop> GetBackdrops(MovieDbSeriesProvider.Images images) + private IEnumerable<Backdrop> GetBackdrops(Images images) { - var eligibleBackdrops = images.backdrops == null ? new List<MovieDbSeriesProvider.Backdrop>() : - images.backdrops; + var eligibleBackdrops = images.Backdrops ?? new List<Backdrop>(); - return eligibleBackdrops.OrderByDescending(i => i.vote_average) - .ThenByDescending(i => i.vote_count); + return eligibleBackdrops.OrderByDescending(i => i.Vote_Average) + .ThenByDescending(i => i.Vote_Count); } /// <summary> @@ -144,7 +146,7 @@ namespace MediaBrowser.Providers.TV.TheMovieDb /// <param name="jsonSerializer">The json serializer.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task{MovieImages}.</returns> - private async Task<MovieDbSeriesProvider.Images> FetchImages(BaseItem item, string language, IJsonSerializer jsonSerializer, + private async Task<Images> FetchImages(BaseItem item, string language, IJsonSerializer jsonSerializer, CancellationToken cancellationToken) { var tmdbId = item.GetProviderId(MetadataProviders.Tmdb); @@ -154,9 +156,9 @@ namespace MediaBrowser.Providers.TV.TheMovieDb return null; } - await MovieDbSeriesProvider.Current.EnsureSeriesInfo(tmdbId, language, cancellationToken).ConfigureAwait(false); + await TmdbSeriesProvider.Current.EnsureSeriesInfo(tmdbId, language, cancellationToken).ConfigureAwait(false); - var path = MovieDbSeriesProvider.Current.GetDataFilePath(tmdbId, language); + var path = TmdbSeriesProvider.Current.GetDataFilePath(tmdbId, language); if (!string.IsNullOrEmpty(path)) { @@ -164,7 +166,7 @@ namespace MediaBrowser.Providers.TV.TheMovieDb if (fileInfo.Exists) { - return jsonSerializer.DeserializeFromFile<MovieDbSeriesProvider.RootObject>(path).images; + return jsonSerializer.DeserializeFromFile<SeriesResult>(path).Images; } } diff --git a/MediaBrowser.Providers/TV/TheMovieDb/MovieDbSeriesProvider.cs b/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs index b51fb6af8..304f34c25 100644 --- a/MediaBrowser.Providers/TV/TheMovieDb/MovieDbSeriesProvider.cs +++ b/MediaBrowser.Providers/Tmdb/TV/TmdbSeriesProvider.cs @@ -18,16 +18,19 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Providers; using MediaBrowser.Model.Serialization; using MediaBrowser.Providers.Movies; +using MediaBrowser.Providers.Tmdb.Models.Search; +using MediaBrowser.Providers.Tmdb.Models.TV; +using MediaBrowser.Providers.Tmdb.Movies; using Microsoft.Extensions.Logging; -namespace MediaBrowser.Providers.TV.TheMovieDb +namespace MediaBrowser.Providers.Tmdb.TV { - public class MovieDbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder + public class TmdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder { - private const string GetTvInfo3 = MovieDbProvider.BaseMovieDbUrl + @"3/tv/{0}?api_key={1}&append_to_response=credits,images,keywords,external_ids,videos,content_ratings"; + private const string GetTvInfo3 = TmdbUtils.BaseTmdbApiUrl + @"3/tv/{0}?api_key={1}&append_to_response=credits,images,keywords,external_ids,videos,content_ratings"; private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - internal static MovieDbSeriesProvider Current { get; private set; } + internal static TmdbSeriesProvider Current { get; private set; } private readonly IJsonSerializer _jsonSerializer; private readonly IFileSystem _fileSystem; @@ -37,7 +40,7 @@ namespace MediaBrowser.Providers.TV.TheMovieDb private readonly IHttpClient _httpClient; private readonly ILibraryManager _libraryManager; - public MovieDbSeriesProvider(IJsonSerializer jsonSerializer, IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILogger logger, ILocalizationManager localization, IHttpClient httpClient, ILibraryManager libraryManager) + public TmdbSeriesProvider(IJsonSerializer jsonSerializer, IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILogger logger, ILocalizationManager localization, IHttpClient httpClient, ILibraryManager libraryManager) { _jsonSerializer = jsonSerializer; _fileSystem = fileSystem; @@ -49,7 +52,7 @@ namespace MediaBrowser.Providers.TV.TheMovieDb Current = this; } - public string Name => "TheMovieDb"; + public string Name => TmdbUtils.ProviderName; public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken) { @@ -63,24 +66,24 @@ namespace MediaBrowser.Providers.TV.TheMovieDb var dataFilePath = GetDataFilePath(tmdbId, searchInfo.MetadataLanguage); - var obj = _jsonSerializer.DeserializeFromFile<RootObject>(dataFilePath); + var obj = _jsonSerializer.DeserializeFromFile<SeriesResult>(dataFilePath); - var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); + var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); var remoteResult = new RemoteSearchResult { - Name = obj.name, + Name = obj.Name, SearchProviderName = Name, - ImageUrl = string.IsNullOrWhiteSpace(obj.poster_path) ? null : tmdbImageUrl + obj.poster_path + ImageUrl = string.IsNullOrWhiteSpace(obj.Poster_Path) ? null : tmdbImageUrl + obj.Poster_Path }; - remoteResult.SetProviderId(MetadataProviders.Tmdb, obj.id.ToString(_usCulture)); - remoteResult.SetProviderId(MetadataProviders.Imdb, obj.external_ids.imdb_id); + remoteResult.SetProviderId(MetadataProviders.Tmdb, obj.Id.ToString(_usCulture)); + remoteResult.SetProviderId(MetadataProviders.Imdb, obj.External_Ids.Imdb_Id); - if (obj.external_ids.tvdb_id > 0) + if (obj.External_Ids.Tvdb_Id > 0) { - remoteResult.SetProviderId(MetadataProviders.Tvdb, obj.external_ids.tvdb_id.ToString(_usCulture)); + remoteResult.SetProviderId(MetadataProviders.Tvdb, obj.External_Ids.Tvdb_Id.ToString(_usCulture)); } return new[] { remoteResult }; @@ -110,7 +113,7 @@ namespace MediaBrowser.Providers.TV.TheMovieDb } } - return await new MovieDbSearch(_logger, _jsonSerializer, _libraryManager).GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false); + return await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false); } public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo info, CancellationToken cancellationToken) @@ -153,7 +156,7 @@ namespace MediaBrowser.Providers.TV.TheMovieDb if (string.IsNullOrEmpty(tmdbId)) { result.QueriedById = false; - var searchResults = await new MovieDbSearch(_logger, _jsonSerializer, _libraryManager).GetSearchResults(info, cancellationToken).ConfigureAwait(false); + var searchResults = await new TmdbSearch(_logger, _jsonSerializer, _libraryManager).GetSearchResults(info, cancellationToken).ConfigureAwait(false); var searchResult = searchResults.FirstOrDefault(); @@ -177,14 +180,14 @@ namespace MediaBrowser.Providers.TV.TheMovieDb private async Task<MetadataResult<Series>> FetchMovieData(string tmdbId, string language, string preferredCountryCode, CancellationToken cancellationToken) { - RootObject seriesInfo = await FetchMainResult(tmdbId, language, cancellationToken).ConfigureAwait(false); + SeriesResult seriesInfo = await FetchMainResult(tmdbId, language, cancellationToken).ConfigureAwait(false); if (seriesInfo == null) { return null; } - tmdbId = seriesInfo.id.ToString(_usCulture); + tmdbId = seriesInfo.Id.ToString(_usCulture); string dataFilePath = GetDataFilePath(tmdbId, language); Directory.CreateDirectory(Path.GetDirectoryName(dataFilePath)); @@ -196,102 +199,102 @@ namespace MediaBrowser.Providers.TV.TheMovieDb result.Item = new Series(); result.ResultLanguage = seriesInfo.ResultLanguage; - var settings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); + var settings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); ProcessMainInfo(result, seriesInfo, preferredCountryCode, settings); return result; } - private void ProcessMainInfo(MetadataResult<Series> seriesResult, RootObject seriesInfo, string preferredCountryCode, TmdbSettingsResult settings) + private void ProcessMainInfo(MetadataResult<Series> seriesResult, SeriesResult seriesInfo, string preferredCountryCode, TmdbSettingsResult settings) { var series = seriesResult.Item; - series.Name = seriesInfo.name; - series.SetProviderId(MetadataProviders.Tmdb, seriesInfo.id.ToString(_usCulture)); + series.Name = seriesInfo.Name; + series.SetProviderId(MetadataProviders.Tmdb, seriesInfo.Id.ToString(_usCulture)); //series.VoteCount = seriesInfo.vote_count; - string voteAvg = seriesInfo.vote_average.ToString(CultureInfo.InvariantCulture); + string voteAvg = seriesInfo.Vote_Average.ToString(CultureInfo.InvariantCulture); if (float.TryParse(voteAvg, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out float rating)) { series.CommunityRating = rating; } - series.Overview = seriesInfo.overview; + series.Overview = seriesInfo.Overview; - if (seriesInfo.networks != null) + if (seriesInfo.Networks != null) { - series.Studios = seriesInfo.networks.Select(i => i.name).ToArray(); + series.Studios = seriesInfo.Networks.Select(i => i.Name).ToArray(); } - if (seriesInfo.genres != null) + if (seriesInfo.Genres != null) { - series.Genres = seriesInfo.genres.Select(i => i.name).ToArray(); + series.Genres = seriesInfo.Genres.Select(i => i.Name).ToArray(); } //series.HomePageUrl = seriesInfo.homepage; - series.RunTimeTicks = seriesInfo.episode_run_time.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault(); + series.RunTimeTicks = seriesInfo.Episode_Run_Time.Select(i => TimeSpan.FromMinutes(i).Ticks).FirstOrDefault(); - if (string.Equals(seriesInfo.status, "Ended", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(seriesInfo.Status, "Ended", StringComparison.OrdinalIgnoreCase)) { series.Status = SeriesStatus.Ended; - series.EndDate = seriesInfo.last_air_date; + series.EndDate = seriesInfo.Last_Air_Date; } else { series.Status = SeriesStatus.Continuing; } - series.PremiereDate = seriesInfo.first_air_date; + series.PremiereDate = seriesInfo.First_Air_Date; - var ids = seriesInfo.external_ids; + var ids = seriesInfo.External_Ids; if (ids != null) { - if (!string.IsNullOrWhiteSpace(ids.imdb_id)) + if (!string.IsNullOrWhiteSpace(ids.Imdb_Id)) { - series.SetProviderId(MetadataProviders.Imdb, ids.imdb_id); + series.SetProviderId(MetadataProviders.Imdb, ids.Imdb_Id); } - if (ids.tvrage_id > 0) + if (ids.Tvrage_Id > 0) { - series.SetProviderId(MetadataProviders.TvRage, ids.tvrage_id.ToString(_usCulture)); + series.SetProviderId(MetadataProviders.TvRage, ids.Tvrage_Id.ToString(_usCulture)); } - if (ids.tvdb_id > 0) + if (ids.Tvdb_Id > 0) { - series.SetProviderId(MetadataProviders.Tvdb, ids.tvdb_id.ToString(_usCulture)); + series.SetProviderId(MetadataProviders.Tvdb, ids.Tvdb_Id.ToString(_usCulture)); } } - var contentRatings = (seriesInfo.content_ratings ?? new ContentRatings()).results ?? new List<ContentRating>(); + var contentRatings = (seriesInfo.Content_Ratings ?? new ContentRatings()).Results ?? new List<ContentRating>(); - var ourRelease = contentRatings.FirstOrDefault(c => string.Equals(c.iso_3166_1, preferredCountryCode, StringComparison.OrdinalIgnoreCase)); - var usRelease = contentRatings.FirstOrDefault(c => string.Equals(c.iso_3166_1, "US", StringComparison.OrdinalIgnoreCase)); + var ourRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, preferredCountryCode, StringComparison.OrdinalIgnoreCase)); + var usRelease = contentRatings.FirstOrDefault(c => string.Equals(c.Iso_3166_1, "US", StringComparison.OrdinalIgnoreCase)); var minimumRelease = contentRatings.FirstOrDefault(); if (ourRelease != null) { - series.OfficialRating = ourRelease.rating; + series.OfficialRating = ourRelease.Rating; } else if (usRelease != null) { - series.OfficialRating = usRelease.rating; + series.OfficialRating = usRelease.Rating; } else if (minimumRelease != null) { - series.OfficialRating = minimumRelease.rating; + series.OfficialRating = minimumRelease.Rating; } - if (seriesInfo.videos != null && seriesInfo.videos.results != null) + if (seriesInfo.Videos != null && seriesInfo.Videos.Results != null) { - foreach (var video in seriesInfo.videos.results) + foreach (var video in seriesInfo.Videos.Results) { - if ((video.type.Equals("trailer", StringComparison.OrdinalIgnoreCase) - || video.type.Equals("clip", StringComparison.OrdinalIgnoreCase)) - && video.site.Equals("youtube", StringComparison.OrdinalIgnoreCase)) + if ((video.Type.Equals("trailer", StringComparison.OrdinalIgnoreCase) + || video.Type.Equals("clip", StringComparison.OrdinalIgnoreCase)) + && video.Site.Equals("youtube", StringComparison.OrdinalIgnoreCase)) { - series.AddTrailerUrl($"http://www.youtube.com/watch?v={video.key}"); + series.AddTrailerUrl($"http://www.youtube.com/watch?v={video.Key}"); } } } @@ -299,26 +302,26 @@ namespace MediaBrowser.Providers.TV.TheMovieDb seriesResult.ResetPeople(); var tmdbImageUrl = settings.images.GetImageUrl("original"); - if (seriesInfo.credits != null && seriesInfo.credits.cast != null) + if (seriesInfo.Credits != null && seriesInfo.Credits.Cast != null) { - foreach (var actor in seriesInfo.credits.cast.OrderBy(a => a.order)) + foreach (var actor in seriesInfo.Credits.Cast.OrderBy(a => a.Order)) { var personInfo = new PersonInfo { - Name = actor.name.Trim(), - Role = actor.character, + Name = actor.Name.Trim(), + Role = actor.Character, Type = PersonType.Actor, - SortOrder = actor.order + SortOrder = actor.Order }; - if (!string.IsNullOrWhiteSpace(actor.profile_path)) + if (!string.IsNullOrWhiteSpace(actor.Profile_Path)) { - personInfo.ImageUrl = tmdbImageUrl + actor.profile_path; + personInfo.ImageUrl = tmdbImageUrl + actor.Profile_Path; } - if (actor.id > 0) + if (actor.Id > 0) { - personInfo.SetProviderId(MetadataProviders.Tmdb, actor.id.ToString(CultureInfo.InvariantCulture)); + personInfo.SetProviderId(MetadataProviders.Tmdb, actor.Id.ToString(CultureInfo.InvariantCulture)); } seriesResult.AddPerson(personInfo); @@ -342,7 +345,7 @@ namespace MediaBrowser.Providers.TV.TheMovieDb internal async Task DownloadSeriesInfo(string id, string preferredMetadataLanguage, CancellationToken cancellationToken) { - RootObject mainResult = await FetchMainResult(id, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); + SeriesResult mainResult = await FetchMainResult(id, preferredMetadataLanguage, cancellationToken).ConfigureAwait(false); if (mainResult == null) { @@ -356,31 +359,31 @@ namespace MediaBrowser.Providers.TV.TheMovieDb _jsonSerializer.SerializeToFile(mainResult, dataFilePath); } - internal async Task<RootObject> FetchMainResult(string id, string language, CancellationToken cancellationToken) + internal async Task<SeriesResult> FetchMainResult(string id, string language, CancellationToken cancellationToken) { - var url = string.Format(GetTvInfo3, id, MovieDbProvider.ApiKey); + var url = string.Format(GetTvInfo3, id, TmdbUtils.ApiKey); if (!string.IsNullOrEmpty(language)) { - url += "&language=" + MovieDbProvider.NormalizeLanguage(language) - + "&include_image_language=" + MovieDbProvider.GetImageLanguagesParam(language); // Get images in english and with no language + url += "&language=" + TmdbMovieProvider.NormalizeLanguage(language) + + "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language); // Get images in english and with no language } cancellationToken.ThrowIfCancellationRequested(); - RootObject mainResult; + SeriesResult mainResult; - using (var response = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions + using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions { Url = url, CancellationToken = cancellationToken, - AcceptHeader = MovieDbProvider.AcceptHeader + AcceptHeader = TmdbUtils.AcceptHeader }).ConfigureAwait(false)) { using (var json = response.Content) { - mainResult = await _jsonSerializer.DeserializeFromStreamAsync<RootObject>(json).ConfigureAwait(false); + mainResult = await _jsonSerializer.DeserializeFromStreamAsync<SeriesResult>(json).ConfigureAwait(false); if (!string.IsNullOrEmpty(language)) { @@ -393,33 +396,33 @@ namespace MediaBrowser.Providers.TV.TheMovieDb // If the language preference isn't english, then have the overview fallback to english if it's blank if (mainResult != null && - string.IsNullOrEmpty(mainResult.overview) && + string.IsNullOrEmpty(mainResult.Overview) && !string.IsNullOrEmpty(language) && !string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)) { _logger.LogInformation("MovieDbSeriesProvider couldn't find meta for language {Language}. Trying English...", language); - url = string.Format(GetTvInfo3, id, MovieDbProvider.ApiKey) + "&language=en"; + url = string.Format(GetTvInfo3, id, TmdbUtils.ApiKey) + "&language=en"; if (!string.IsNullOrEmpty(language)) { // Get images in english and with no language - url += "&include_image_language=" + MovieDbProvider.GetImageLanguagesParam(language); + url += "&include_image_language=" + TmdbMovieProvider.GetImageLanguagesParam(language); } - using (var response = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions + using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions { Url = url, CancellationToken = cancellationToken, - AcceptHeader = MovieDbProvider.AcceptHeader + AcceptHeader = TmdbUtils.AcceptHeader }).ConfigureAwait(false)) { using (var json = response.Content) { - var englishResult = await _jsonSerializer.DeserializeFromStreamAsync<RootObject>(json).ConfigureAwait(false); + var englishResult = await _jsonSerializer.DeserializeFromStreamAsync<SeriesResult>(json).ConfigureAwait(false); - mainResult.overview = englishResult.overview; + mainResult.Overview = englishResult.Overview; mainResult.ResultLanguage = "en"; } } @@ -467,40 +470,40 @@ namespace MediaBrowser.Providers.TV.TheMovieDb private async Task<RemoteSearchResult> FindByExternalId(string id, string externalSource, CancellationToken cancellationToken) { - var url = string.Format(MovieDbProvider.BaseMovieDbUrl + @"3/find/{0}?api_key={1}&external_source={2}", + var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/find/{0}?api_key={1}&external_source={2}", id, - MovieDbProvider.ApiKey, + TmdbUtils.ApiKey, externalSource); - using (var response = await MovieDbProvider.Current.GetMovieDbResponse(new HttpRequestOptions + using (var response = await TmdbMovieProvider.Current.GetMovieDbResponse(new HttpRequestOptions { Url = url, CancellationToken = cancellationToken, - AcceptHeader = MovieDbProvider.AcceptHeader + AcceptHeader = TmdbUtils.AcceptHeader }).ConfigureAwait(false)) { using (var json = response.Content) { - var result = await _jsonSerializer.DeserializeFromStreamAsync<MovieDbSearch.ExternalIdLookupResult>(json).ConfigureAwait(false); + var result = await _jsonSerializer.DeserializeFromStreamAsync<ExternalIdLookupResult>(json).ConfigureAwait(false); - if (result != null && result.tv_results != null) + if (result != null && result.Tv_Results != null) { - var tv = result.tv_results.FirstOrDefault(); + var tv = result.Tv_Results.FirstOrDefault(); if (tv != null) { - var tmdbSettings = await MovieDbProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); + var tmdbSettings = await TmdbMovieProvider.Current.GetTmdbSettings(cancellationToken).ConfigureAwait(false); var tmdbImageUrl = tmdbSettings.images.GetImageUrl("original"); var remoteResult = new RemoteSearchResult { - Name = tv.name, + Name = tv.Name, SearchProviderName = Name, - ImageUrl = string.IsNullOrWhiteSpace(tv.poster_path) ? null : tmdbImageUrl + tv.poster_path + ImageUrl = string.IsNullOrWhiteSpace(tv.Poster_Path) ? null : tmdbImageUrl + tv.Poster_Path }; - remoteResult.SetProviderId(MetadataProviders.Tmdb, tv.id.ToString(_usCulture)); + remoteResult.SetProviderId(MetadataProviders.Tmdb, tv.Id.ToString(_usCulture)); return remoteResult; } @@ -511,163 +514,6 @@ namespace MediaBrowser.Providers.TV.TheMovieDb return null; } - public class CreatedBy - { - public int id { get; set; } - public string name { get; set; } - public string profile_path { get; set; } - } - - public class Genre - { - public int id { get; set; } - public string name { get; set; } - } - - public class Network - { - public int id { get; set; } - public string name { get; set; } - } - - public class Season - { - public string air_date { get; set; } - public int episode_count { get; set; } - public int id { get; set; } - public string poster_path { get; set; } - public int season_number { get; set; } - } - - public class Cast - { - public string character { get; set; } - public string credit_id { get; set; } - public int id { get; set; } - public string name { get; set; } - public string profile_path { get; set; } - public int order { get; set; } - } - - public class Crew - { - public string credit_id { get; set; } - public string department { get; set; } - public int id { get; set; } - public string name { get; set; } - public string job { get; set; } - public string profile_path { get; set; } - } - - public class Credits - { - public List<Cast> cast { get; set; } - public List<Crew> crew { get; set; } - } - - public class Backdrop - { - public double aspect_ratio { get; set; } - public string file_path { get; set; } - public int height { get; set; } - public string iso_639_1 { get; set; } - public double vote_average { get; set; } - public int vote_count { get; set; } - public int width { get; set; } - } - - public class Poster - { - public double aspect_ratio { get; set; } - public string file_path { get; set; } - public int height { get; set; } - public string iso_639_1 { get; set; } - public double vote_average { get; set; } - public int vote_count { get; set; } - public int width { get; set; } - } - - public class Images - { - public List<Backdrop> backdrops { get; set; } - public List<Poster> posters { get; set; } - } - - public class Keywords - { - public List<object> results { get; set; } - } - - public class ExternalIds - { - public string imdb_id { get; set; } - public string freebase_id { get; set; } - public string freebase_mid { get; set; } - public int tvdb_id { get; set; } - public int tvrage_id { get; set; } - } - - public class Videos - { - public List<Video> results { get; set; } - } - - public class Video - { - public string id { get; set; } - public string iso_639_1 { get; set; } - public string iso_3166_1 { get; set; } - public string key { get; set; } - public string name { get; set; } - public string site { get; set; } - public string size { get; set; } - public string type { get; set; } - } - - public class ContentRating - { - public string iso_3166_1 { get; set; } - public string rating { get; set; } - } - - public class ContentRatings - { - public List<ContentRating> results { get; set; } - } - - public class RootObject - { - public string backdrop_path { get; set; } - public List<CreatedBy> created_by { get; set; } - public List<int> episode_run_time { get; set; } - public DateTime first_air_date { get; set; } - public List<Genre> genres { get; set; } - public string homepage { get; set; } - public int id { get; set; } - public bool in_production { get; set; } - public List<string> languages { get; set; } - public DateTime last_air_date { get; set; } - public string name { get; set; } - public List<Network> networks { get; set; } - public int number_of_episodes { get; set; } - public int number_of_seasons { get; set; } - public string original_name { get; set; } - public List<string> origin_country { get; set; } - public string overview { get; set; } - public string popularity { get; set; } - public string poster_path { get; set; } - public List<Season> seasons { get; set; } - public string status { get; set; } - public double vote_average { get; set; } - public int vote_count { get; set; } - public Credits credits { get; set; } - public Images images { get; set; } - public Keywords keywords { get; set; } - public ExternalIds external_ids { get; set; } - public Videos videos { get; set; } - public ContentRatings content_ratings { get; set; } - public string ResultLanguage { get; set; } - } // After TheTVDB public int Order => 1; diff --git a/MediaBrowser.Providers/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Tmdb/TmdbUtils.cs new file mode 100644 index 000000000..035b99c1a --- /dev/null +++ b/MediaBrowser.Providers/Tmdb/TmdbUtils.cs @@ -0,0 +1,31 @@ +using System; +using MediaBrowser.Model.Entities; +using MediaBrowser.Providers.Tmdb.Models.General; + +namespace MediaBrowser.Providers.Tmdb +{ + public static class TmdbUtils + { + public const string BaseTmdbUrl = "https://www.themoviedb.org/"; + public const string BaseTmdbApiUrl = "https://api.themoviedb.org/"; + public const string ProviderName = "TheMovieDb"; + public const string ApiKey = "4219e299c89411838049ab0dab19ebd5"; + public const string AcceptHeader = "application/json,image/*"; + + public static string MapCrewToPersonType(Crew crew) + { + if (crew.Department.Equals("production", StringComparison.InvariantCultureIgnoreCase) + && crew.Job.IndexOf("producer", StringComparison.InvariantCultureIgnoreCase) != -1) + { + return PersonType.Producer; + } + + if (crew.Department.Equals("writing", StringComparison.InvariantCultureIgnoreCase)) + { + return PersonType.Writer; + } + + return null; + } + } +} diff --git a/MediaBrowser.Providers/Movies/MovieDbTrailerProvider.cs b/MediaBrowser.Providers/Tmdb/Trailers/TmdbTrailerProvider.cs index 2a3cdf097..b0dec0245 100644 --- a/MediaBrowser.Providers/Movies/MovieDbTrailerProvider.cs +++ b/MediaBrowser.Providers/Tmdb/Trailers/TmdbTrailerProvider.cs @@ -5,29 +5,31 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Providers; +using MediaBrowser.Providers.Movies; +using MediaBrowser.Providers.Tmdb.Movies; -namespace MediaBrowser.Providers.Movies +namespace MediaBrowser.Providers.Tmdb.Trailers { - public class MovieDbTrailerProvider : IHasOrder, IRemoteMetadataProvider<Trailer, TrailerInfo> + public class TmdbTrailerProvider : IHasOrder, IRemoteMetadataProvider<Trailer, TrailerInfo> { private readonly IHttpClient _httpClient; - public MovieDbTrailerProvider(IHttpClient httpClient) + public TmdbTrailerProvider(IHttpClient httpClient) { _httpClient = httpClient; } public Task<IEnumerable<RemoteSearchResult>> GetSearchResults(TrailerInfo searchInfo, CancellationToken cancellationToken) { - return MovieDbProvider.Current.GetMovieSearchResults(searchInfo, cancellationToken); + return TmdbMovieProvider.Current.GetMovieSearchResults(searchInfo, cancellationToken); } public Task<MetadataResult<Trailer>> GetMetadata(TrailerInfo info, CancellationToken cancellationToken) { - return MovieDbProvider.Current.GetItemMetadata<Trailer>(info, cancellationToken); + return TmdbMovieProvider.Current.GetItemMetadata<Trailer>(info, cancellationToken); } - public string Name => MovieDbProvider.Current.Name; + public string Name => TmdbMovieProvider.Current.Name; public int Order => 0; diff --git a/MediaBrowser.Tests/ConsistencyTests/Resources/SampleTransformed.htm b/MediaBrowser.Tests/ConsistencyTests/Resources/SampleTransformed.htm deleted file mode 100644 index f36652468..000000000 --- a/MediaBrowser.Tests/ConsistencyTests/Resources/SampleTransformed.htm +++ /dev/null @@ -1,1277 +0,0 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml"> - <head> - <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> - <title>String Usage Report</title> - <style> -body { - background: #F3F3F4; - color: #1E1E1F; - font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; - padding: 0; - margin: 0; -} -h1 { - padding: 10px 0px 10px 10px; - font-size: 21pt; - background-color: #E2E2E2; - border-bottom: 1px #C1C1C2 solid; - color: #201F20; - margin: 0; - font-weight: normal; -} -h2 { - font-size: 18pt; - font-weight: normal; - padding: 15px 0 5px 0; - margin: 0; -} -h3 { - font-weight: normal; - font-size: 15pt; - margin: 0; - padding: 15px 0 5px 0; - background-color: transparent; -} -/* Color all hyperlinks one color */ -a { - color: #1382CE; -} -/* Table styles */ -table { - border-spacing: 0 0; - border-collapse: collapse; - font-size: 10pt; -} -table th { - background: #E7E7E8; - text-align: left; - text-decoration: none; - font-weight: normal; - padding: 3px 6px 3px 6px; - border: 1px solid #CBCBCB; -} -table td { - vertical-align: top; - padding: 3px 6px 5px 5px; - margin: 0px; - border: 1px solid #CBCBCB; - background: #F7F7F8; -} -/* Local link is a style for hyperlinks that link to file:/// content, there are lots so color them as 'normal' text until the user mouse overs */ -.localLink { - color: #1E1E1F; - background: #EEEEED; - text-decoration: none; -} -.localLink:hover { - color: #1382CE; - background: #FFFF99; - text-decoration: none; -} -.baseCell { - width: 100%; - color: #427A9F; -} -.stringCell { - display: table; -} -.tokenCell { - white-space: nowrap; -} -.occurrence { - padding-left: 40px; -} -.block { - display: table-cell; -} -/* Padding around the content after the h1 */ -#content { - padding: 0px 12px 12px 12px; -} -#messages table { - width: 97%; -} - </style> - </head> - <body> - <h1>String Usage Report</h1> - <div id="content"> - <h2>Strings</h2> - <div id="messages"> - <table> - <tbody> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelExit</strong>: "</div> - <div class="block">Exit"</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelVisitCommunity</strong>: "</div> - <div class="block">Visit Community"</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelGithub</strong>: "</div> - <div class="block">Github"</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelSwagger</strong>: "</div> - <div class="block">Swagger"</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelStandard</strong>: "</div> - <div class="block">Standard"</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelApiDocumentation</strong>: "</div> - <div class="block">Api Documentation"</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelDeveloperResources</strong>: "</div> - <div class="block">Developer Resources"</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelBrowseLibrary</strong>: "</div> - <div class="block">Browse Library"</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelConfigureServer</strong>: "</div> - <div class="block">Configure Emby"</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelOpenLibraryViewer</strong>: "</div> - <div class="block">Open Library Viewer"</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelRestartServer</strong>: "</div> - <div class="block">Restart Server"</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelShowLogWindow</strong>: "</div> - <div class="block">Show Log Window"</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelPrevious</strong>: "</div> - <div class="block">Previous"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardcomponents.html">\wizardcomponents.html:54</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardfinish.html">\wizardfinish.html:40</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlibrary.html">\wizardlibrary.html:19</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlivetvguide.html">\wizardlivetvguide.html:30</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlivetvtuner.html">\wizardlivetvtuner.html:31</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html">\wizardservice.html:17</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardsettings.html">\wizardsettings.html:32</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html">\wizarduser.html:27</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelFinish</strong>: "</div> - <div class="block">Finish"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardfinish.html">\wizardfinish.html:41</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelNext</strong>: "</div> - <div class="block">Next"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardcomponents.html">\wizardcomponents.html:55</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlibrary.html">\wizardlibrary.html:20</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlivetvguide.html">\wizardlivetvguide.html:31</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlivetvtuner.html">\wizardlivetvtuner.html:32</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html">\wizardservice.html:18</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardsettings.html">\wizardsettings.html:33</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardstart.html">\wizardstart.html:25</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html">\wizarduser.html:28</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelYoureDone</strong>: "</div> - <div class="block">You're Done!"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardfinish.html">\wizardfinish.html:7</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>WelcomeToProject</strong>: "</div> - <div class="block">Welcome to Emby!"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardstart.html">\wizardstart.html:10</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>ThisWizardWillGuideYou</strong>: "</div> - <div class="block">This wizard will help guide you through the setup process. To begin, please select your preferred language."</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardstart.html">\wizardstart.html:16</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>TellUsAboutYourself</strong>: "</div> - <div class="block">Tell us about yourself"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html">\wizarduser.html:8</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>ButtonQuickStartGuide</strong>: "</div> - <div class="block">Quick start guide"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardstart.html">\wizardstart.html:12</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelYourFirstName</strong>: "</div> - <div class="block">Your first name:"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html">\wizarduser.html:14</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>MoreUsersCanBeAddedLater</strong>: "</div> - <div class="block">More users can be added later within the Dashboard."</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html">\wizarduser.html:15</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>UserProfilesIntro</strong>: "</div> - <div class="block">Emby includes built-in support for user profiles, enabling each user to have their own display settings, playstate and parental controls."</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html">\wizarduser.html:11</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelWindowsService</strong>: "</div> - <div class="block">Windows Service"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html">\wizardservice.html:7</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>AWindowsServiceHasBeenInstalled</strong>: "</div> - <div class="block">A Windows Service has been installed."</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html">\wizardservice.html:10</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>WindowsServiceIntro1</strong>: "</div> - <div class="block">Emby Server normally runs as a desktop application with a tray icon, but if you prefer to run it as a background service, it can be started from the windows services control panel instead."</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html">\wizardservice.html:12</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>WindowsServiceIntro2</strong>: "</div> - <div class="block">If using the windows service, please note that it cannot be run at the same time as the tray icon, so you'll need to exit the tray in order to run the service. The service will also need to be configured with administrative privileges via the control panel. When running as a service, you will need to ensure that the service account has access to your media folders."</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html">\wizardservice.html:14</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>WizardCompleted</strong>: "</div> - <div class="block">That's all we need for now. Emby has begun collecting information about your media library. Check out some of our apps, and then click <b>Finish</b> to view the <b>Server Dashboard</b>."</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardfinish.html">\wizardfinish.html:10</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelConfigureSettings</strong>: "</div> - <div class="block">Configure settings"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardsettings.html">\wizardsettings.html:8</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelEnableVideoImageExtraction</strong>: "</div> - <div class="block">Enable video image extraction"</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>VideoImageExtractionHelp</strong>: "</div> - <div class="block">For videos that don't already have images, and that we're unable to find internet images for. This will add some additional time to the initial library scan but will result in a more pleasing presentation."</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelEnableChapterImageExtractionForMovies</strong>: "</div> - <div class="block">Extract chapter image extraction for Movies"</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelChapterImageExtractionForMoviesHelp</strong>: "</div> - <div class="block">Extracting chapter images will allow clients to display graphical scene selection menus. The process can be slow, cpu-intensive and may require several gigabytes of space. It runs as a nightly scheduled task, although this is configurable in the scheduled tasks area. It is not recommended to run this task during peak usage hours."</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelEnableAutomaticPortMapping</strong>: "</div> - <div class="block">Enable automatic port mapping"</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelEnableAutomaticPortMappingHelp</strong>: "</div> - <div class="block">UPnP allows automated router configuration for easy remote access. This may not work with some router models."</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>HeaderTermsOfService</strong>: "</div> - <div class="block">Emby Terms of Service"</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>HeaderDeveloperOptions</strong>: "</div> - <div class="block">Developer Options"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html">\dashboardgeneral.html:108</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>OptionEnableWebClientResponseCache</strong>: "</div> - <div class="block">Enable web response caching"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html">\dashboardgeneral.html:112</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>OptionDisableForDevelopmentHelp</strong>: "</div> - <div class="block">Configure these as needed for web development purposes."</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html">\dashboardgeneral.html:119</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>OptionEnableWebClientResourceMinification</strong>: "</div> - <div class="block">Enable web resource minification"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html">\dashboardgeneral.html:116</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelDashboardSourcePath</strong>: "</div> - <div class="block">Web client source path:"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html">\dashboardgeneral.html:124</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelDashboardSourcePathHelp</strong>: "</div> - <div class="block">If running the server from source, specify the path to the dashboard-ui folder. All web client files will be served from this location."</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html">\dashboardgeneral.html:126</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>ButtonConvertMedia</strong>: "</div> - <div class="block">Convert media"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\syncactivity.html">\syncactivity.html:22</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>ButtonOrganize</strong>: "</div> - <div class="block">Organize"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\autoorganizelog.html">\autoorganizelog.html:8</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\autoorganizelog.js">\scripts\autoorganizelog.js:293</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\autoorganizelog.js">\scripts\autoorganizelog.js:294</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\autoorganizelog.js">\scripts\autoorganizelog.js:296</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LinkedToEmbyConnect</strong>: "</div> - <div class="block">Linked to Emby Connect"</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>HeaderSupporterBenefits</strong>: "</div> - <div class="block">Emby Premiere Benefits"</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>HeaderAddUser</strong>: "</div> - <div class="block">Add User"</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelAddConnectSupporterHelp</strong>: "</div> - <div class="block">To add a user who isn't listed, you'll need to first link their account to Emby Connect from their user profile page."</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>LabelPinCode</strong>: "</div> - <div class="block">Pin code:"</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>OptionHideWatchedContentFromLatestMedia</strong>: "</div> - <div class="block">Hide watched content from latest media"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\mypreferenceshome.html">\mypreferenceshome.html:114</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>HeaderSync</strong>: "</div> - <div class="block">Sync"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\mysyncsettings.html">\mysyncsettings.html:7</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\registrationservices.js">\scripts\registrationservices.js:175</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\useredit.html">\useredit.html:82</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>ButtonOk</strong>: "</div> - <div class="block">Ok"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\directorybrowser\directorybrowser.js">\components\directorybrowser\directorybrowser.js:147</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\fileorganizer\fileorganizer.template.html">\components\fileorganizer\fileorganizer.template.html:45</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\medialibrarycreator\medialibrarycreator.template.html">\components\medialibrarycreator\medialibrarycreator.template.html:30</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\metadataeditor\personeditor.template.html">\components\metadataeditor\personeditor.template.html:33</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:372</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:453</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:504</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:542</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:590</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:630</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:661</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:706</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\nowplaying.html">\nowplaying.html:113</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\ratingdialog.js">\scripts\ratingdialog.js:42</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>ButtonCancel</strong>: "</div> - <div class="block">Cancel"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\tvproviders\schedulesdirect.template.html">\components\tvproviders\schedulesdirect.template.html:68</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\tvproviders\xmltv.template.html">\components\tvproviders\xmltv.template.html:48</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\connectlogin.html">\connectlogin.html:74</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\connectlogin.html">\connectlogin.html:108</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:325</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:375</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:456</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:507</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:545</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:593</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:633</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:664</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:709</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\forgotpassword.html">\forgotpassword.html:23</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\forgotpasswordpin.html">\forgotpasswordpin.html:22</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\livetvseriestimer.html">\livetvseriestimer.html:62</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\livetvtunerprovider-hdhomerun.html">\livetvtunerprovider-hdhomerun.html:35</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\livetvtunerprovider-m3u.html">\livetvtunerprovider-m3u.html:19</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\livetvtunerprovider-satip.html">\livetvtunerprovider-satip.html:65</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\login.html">\login.html:27</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\notificationsetting.html">\notificationsetting.html:64</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scheduledtask.html">\scheduledtask.html:85</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\librarylist.js">\scripts\librarylist.js:349</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\mediacontroller.js">\scripts\mediacontroller.js:167</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\mediacontroller.js">\scripts\mediacontroller.js:436</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\ratingdialog.js">\scripts\ratingdialog.js:43</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\site.js">\scripts\site.js:1025</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\userprofilespage.js">\scripts\userprofilespage.js:198</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\syncsettings.html">\syncsettings.html:43</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\useredit.html">\useredit.html:111</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\userlibraryaccess.html">\userlibraryaccess.html:57</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\usernew.html">\usernew.html:45</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\userparentalcontrol.html">\userparentalcontrol.html:101</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>ButtonExit</strong>: "</div> - <div class="block">Exit"</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>ButtonNew</strong>: "</div> - <div class="block">New"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\fileorganizer\fileorganizer.template.html">\components\fileorganizer\fileorganizer.template.html:18</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:107</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:278</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:290</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:296</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:302</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:308</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html">\dlnaprofile.html:314</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofiles.html">\dlnaprofiles.html:14</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\serversecurity.html">\serversecurity.html:8</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>HeaderTaskTriggers</strong>: "</div> - <div class="block">Task Triggers"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scheduledtask.html">\scheduledtask.html:11</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>HeaderTV</strong>: "</div> - <div class="block">TV"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\librarysettings.html">\librarysettings.html:113</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>HeaderAudio</strong>: "</div> - <div class="block">Audio"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\librarysettings.html">\librarysettings.html:39</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>HeaderVideo</strong>: "</div> - <div class="block">Video"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\librarysettings.html">\librarysettings.html:50</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>HeaderPaths</strong>: "</div> - <div class="block">Paths"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboard.html">\dashboard.html:92</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>CategorySync</strong>: "</div> - <div class="block">Sync"</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>TabPlaylist</strong>: "</div> - <div class="block">Playlist"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\nowplaying.html">\nowplaying.html:20</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>HeaderEasyPinCode</strong>: "</div> - <div class="block">Easy Pin Code"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\myprofile.html">\myprofile.html:69</a> - </td> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\userpassword.html">\userpassword.html:42</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>HeaderGrownupsOnly</strong>: "</div> - <div class="block">Grown-ups Only!"</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>DividerOr</strong>: "</div> - <div class="block">-- or --"</div> - </div> - </th> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>HeaderInstalledServices</strong>: "</div> - <div class="block">Installed Services"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\appservices.html">\appservices.html:6</a> - </td> - </tr> - <tr> - <th class="baseCell"> - <div class="stringCell"> - <div class="block tokenCell"> - <strong>HeaderAvailableServices</strong>: "</div> - <div class="block">Available Services"</div> - </div> - </th> - </tr> - <tr> - <td class="baseCell occurrence"> - <a href="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\appservices.html">\appservices.html:11</a> - </td> - </tr> - </tbody> - </table> - </div> - </div> - </body> -</html> diff --git a/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheck.xslt b/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheck.xslt deleted file mode 100644 index 39586022b..000000000 --- a/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheck.xslt +++ /dev/null @@ -1,145 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- DWXMLSource="StringCheckSample.xml" --> -<!DOCTYPE xsl:stylesheet [ - <!ENTITY nbsp " "> - <!ENTITY copy "©"> - <!ENTITY reg "®"> - <!ENTITY trade "™"> - <!ENTITY mdash "—"> - <!ENTITY ldquo "“"> - <!ENTITY rdquo "”"> - <!ENTITY pound "£"> - <!ENTITY yen "¥"> - <!ENTITY euro "€"> -]> -<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> - <xsl:output method="html" encoding="utf-8" doctype-public="-//W3C//DTD XHTML 1.0 Transitional//EN" doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"/> - <xsl:template match="/"> - <html xmlns="http://www.w3.org/1999/xhtml"> - <head> - <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> - <title> - <xsl:value-of select="StringUsages/@ReportTitle"/> - </title> - <style> -body { - background: #F3F3F4; - color: #1E1E1F; - font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; - padding: 0; - margin: 0; -} -h1 { - padding: 10px 0px 10px 10px; - font-size: 21pt; - background-color: #E2E2E2; - border-bottom: 1px #C1C1C2 solid; - color: #201F20; - margin: 0; - font-weight: normal; -} -h2 { - font-size: 18pt; - font-weight: normal; - padding: 15px 0 5px 0; - margin: 0; -} -h3 { - font-weight: normal; - font-size: 15pt; - margin: 0; - padding: 15px 0 5px 0; - background-color: transparent; -} -/* Color all hyperlinks one color */ -a { - color: #1382CE; -} -/* Table styles */ -table { - border-spacing: 0 0; - border-collapse: collapse; - font-size: 10pt; -} -table th { - background: #E7E7E8; - text-align: left; - text-decoration: none; - font-weight: normal; - padding: 3px 6px 3px 6px; - border: 1px solid #CBCBCB; -} -table td { - vertical-align: top; - padding: 3px 6px 5px 5px; - margin: 0px; - border: 1px solid #CBCBCB; - background: #F7F7F8; -} -/* Local link is a style for hyperlinks that link to file:/// content, there are lots so color them as 'normal' text until the user mouse overs */ -.localLink { - color: #1E1E1F; - background: #EEEEED; - text-decoration: none; -} -.localLink:hover { - color: #1382CE; - background: #FFFF99; - text-decoration: none; -} -.baseCell { - width: 100%; - color: #427A9F; -} -.stringCell { - display: table; -} -.tokenCell { - white-space: nowrap; -} -.occurrence { - padding-left: 40px; -} -.block { - display: table-cell; -} -/* Padding around the content after the h1 */ -#content { - padding: 0px 12px 12px 12px; -} -#messages table { - width: 97%; -} - </style> - </head> - <body> - <h1> - <xsl:value-of select="StringUsages/@ReportTitle"/> - </h1> - <div id="content"> - <h2>Strings</h2> - <div id="messages"> - <table> - <tbody> - <xsl:for-each select="StringUsages/Dictionary"> - <tr> - <th class="baseCell"> <div class="stringCell"> - <div class="block tokenCell"><strong><xsl:value-of select="@Token"/></strong>: "</div> - <div class="block"><xsl:value-of select="@Text"/>"</div> - </div></th> - </tr> - <xsl:for-each select="Occurence"> - <xsl:variable name="hyperlink"><xsl:value-of select="@FullPath" /></xsl:variable> - <tr> - <td class="baseCell occurrence"><a href="{@FullPath}"><xsl:value-of select="@FileName"/>:<xsl:value-of select="@LineNumber"/></a></td> - </tr> - </xsl:for-each> - </xsl:for-each> - </tbody> - </table> - </div> - </div> - </body> - </html> - </xsl:template> -</xsl:stylesheet>
\ No newline at end of file diff --git a/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheckSample.xml b/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheckSample.xml deleted file mode 100644 index 9c65bddcd..000000000 --- a/MediaBrowser.Tests/ConsistencyTests/Resources/StringCheckSample.xml +++ /dev/null @@ -1,222 +0,0 @@ -<?xml version="1.0" encoding="utf-8" standalone="yes"?> -<?xml-stylesheet type="text/xsl" href="StringCheck.xslt"?> -<StringUsages Mode="All"> - <Dictionary Token="LabelExit" Text="Exit" /> - <Dictionary Token="LabelVisitCommunity" Text="Visit Community" /> - <Dictionary Token="LabelGithub" Text="Github" /> - <Dictionary Token="LabelSwagger" Text="Swagger" /> - <Dictionary Token="LabelStandard" Text="Standard" /> - <Dictionary Token="LabelApiDocumentation" Text="Api Documentation" /> - <Dictionary Token="LabelDeveloperResources" Text="Developer Resources" /> - <Dictionary Token="LabelBrowseLibrary" Text="Browse Library" /> - <Dictionary Token="LabelConfigureServer" Text="Configure Emby" /> - <Dictionary Token="LabelOpenLibraryViewer" Text="Open Library Viewer" /> - <Dictionary Token="LabelRestartServer" Text="Restart Server" /> - <Dictionary Token="LabelShowLogWindow" Text="Show Log Window" /> - <Dictionary Token="LabelPrevious" Text="Previous"> - <Occurence FileName="\wizardcomponents.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardcomponents.html" LineNumber="54" /> - <Occurence FileName="\wizardfinish.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardfinish.html" LineNumber="40" /> - <Occurence FileName="\wizardlibrary.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlibrary.html" LineNumber="19" /> - <Occurence FileName="\wizardlivetvguide.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlivetvguide.html" LineNumber="30" /> - <Occurence FileName="\wizardlivetvtuner.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlivetvtuner.html" LineNumber="31" /> - <Occurence FileName="\wizardservice.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html" LineNumber="17" /> - <Occurence FileName="\wizardsettings.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardsettings.html" LineNumber="32" /> - <Occurence FileName="\wizarduser.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html" LineNumber="27" /> - </Dictionary> - <Dictionary Token="LabelFinish" Text="Finish"> - <Occurence FileName="\wizardfinish.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardfinish.html" LineNumber="41" /> - </Dictionary> - <Dictionary Token="LabelNext" Text="Next"> - <Occurence FileName="\wizardcomponents.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardcomponents.html" LineNumber="55" /> - <Occurence FileName="\wizardlibrary.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlibrary.html" LineNumber="20" /> - <Occurence FileName="\wizardlivetvguide.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlivetvguide.html" LineNumber="31" /> - <Occurence FileName="\wizardlivetvtuner.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardlivetvtuner.html" LineNumber="32" /> - <Occurence FileName="\wizardservice.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html" LineNumber="18" /> - <Occurence FileName="\wizardsettings.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardsettings.html" LineNumber="33" /> - <Occurence FileName="\wizardstart.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardstart.html" LineNumber="25" /> - <Occurence FileName="\wizarduser.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html" LineNumber="28" /> - </Dictionary> - <Dictionary Token="LabelYoureDone" Text="You're Done!"> - <Occurence FileName="\wizardfinish.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardfinish.html" LineNumber="7" /> - </Dictionary> - <Dictionary Token="WelcomeToProject" Text="Welcome to Emby!"> - <Occurence FileName="\wizardstart.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardstart.html" LineNumber="10" /> - </Dictionary> - <Dictionary Token="ThisWizardWillGuideYou" Text="This wizard will help guide you through the setup process. To begin, please select your preferred language."> - <Occurence FileName="\wizardstart.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardstart.html" LineNumber="16" /> - </Dictionary> - <Dictionary Token="TellUsAboutYourself" Text="Tell us about yourself"> - <Occurence FileName="\wizarduser.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html" LineNumber="8" /> - </Dictionary> - <Dictionary Token="ButtonQuickStartGuide" Text="Quick start guide"> - <Occurence FileName="\wizardstart.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardstart.html" LineNumber="12" /> - </Dictionary> - <Dictionary Token="LabelYourFirstName" Text="Your first name:"> - <Occurence FileName="\wizarduser.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html" LineNumber="14" /> - </Dictionary> - <Dictionary Token="MoreUsersCanBeAddedLater" Text="More users can be added later within the Dashboard."> - <Occurence FileName="\wizarduser.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html" LineNumber="15" /> - </Dictionary> - <Dictionary Token="UserProfilesIntro" Text="Emby includes built-in support for user profiles, enabling each user to have their own display settings, playstate and parental controls."> - <Occurence FileName="\wizarduser.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizarduser.html" LineNumber="11" /> - </Dictionary> - <Dictionary Token="LabelWindowsService" Text="Windows Service"> - <Occurence FileName="\wizardservice.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html" LineNumber="7" /> - </Dictionary> - <Dictionary Token="AWindowsServiceHasBeenInstalled" Text="A Windows Service has been installed."> - <Occurence FileName="\wizardservice.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html" LineNumber="10" /> - </Dictionary> - <Dictionary Token="WindowsServiceIntro1" Text="Emby Server normally runs as a desktop application with a tray icon, but if you prefer to run it as a background service, it can be started from the windows services control panel instead."> - <Occurence FileName="\wizardservice.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html" LineNumber="12" /> - </Dictionary> - <Dictionary Token="WindowsServiceIntro2" Text="If using the windows service, please note that it cannot be run at the same time as the tray icon, so you'll need to exit the tray in order to run the service. The service will also need to be configured with administrative privileges via the control panel. When running as a service, you will need to ensure that the service account has access to your media folders."> - <Occurence FileName="\wizardservice.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardservice.html" LineNumber="14" /> - </Dictionary> - <Dictionary Token="WizardCompleted" Text="That's all we need for now. Emby has begun collecting information about your media library. Check out some of our apps, and then click <b>Finish</b> to view the <b>Server Dashboard</b>."> - <Occurence FileName="\wizardfinish.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardfinish.html" LineNumber="10" /> - </Dictionary> - <Dictionary Token="LabelConfigureSettings" Text="Configure settings"> - <Occurence FileName="\wizardsettings.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\wizardsettings.html" LineNumber="8" /> - </Dictionary> - <Dictionary Token="LabelEnableVideoImageExtraction" Text="Enable video image extraction" /> - <Dictionary Token="VideoImageExtractionHelp" Text="For videos that don't already have images, and that we're unable to find internet images for. This will add some additional time to the initial library scan but will result in a more pleasing presentation." /> - <Dictionary Token="LabelEnableChapterImageExtractionForMovies" Text="Extract chapter image extraction for Movies" /> - <Dictionary Token="LabelChapterImageExtractionForMoviesHelp" Text="Extracting chapter images will allow clients to display graphical scene selection menus. The process can be slow, cpu-intensive and may require several gigabytes of space. It runs as a nightly scheduled task, although this is configurable in the scheduled tasks area. It is not recommended to run this task during peak usage hours." /> - <Dictionary Token="LabelEnableAutomaticPortMapping" Text="Enable automatic port mapping" /> - <Dictionary Token="LabelEnableAutomaticPortMappingHelp" Text="UPnP allows automated router configuration for easy remote access. This may not work with some router models." /> - <Dictionary Token="HeaderDeveloperOptions" Text="Developer Options"> - <Occurence FileName="\dashboardgeneral.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html" LineNumber="108" /> - </Dictionary> - <Dictionary Token="OptionEnableWebClientResponseCache" Text="Enable web response caching"> - <Occurence FileName="\dashboardgeneral.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html" LineNumber="112" /> - </Dictionary> - <Dictionary Token="OptionDisableForDevelopmentHelp" Text="Configure these as needed for web development purposes."> - <Occurence FileName="\dashboardgeneral.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html" LineNumber="119" /> - </Dictionary> - <Dictionary Token="OptionEnableWebClientResourceMinification" Text="Enable web resource minification"> - <Occurence FileName="\dashboardgeneral.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html" LineNumber="116" /> - </Dictionary> - <Dictionary Token="LabelDashboardSourcePath" Text="Web client source path:"> - <Occurence FileName="\dashboardgeneral.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html" LineNumber="124" /> - </Dictionary> - <Dictionary Token="LabelDashboardSourcePathHelp" Text="If running the server from source, specify the path to the dashboard-ui folder. All web client files will be served from this location."> - <Occurence FileName="\dashboardgeneral.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboardgeneral.html" LineNumber="126" /> - </Dictionary> - <Dictionary Token="ButtonConvertMedia" Text="Convert media"> - <Occurence FileName="\syncactivity.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\syncactivity.html" LineNumber="22" /> - </Dictionary> - <Dictionary Token="ButtonOrganize" Text="Organize"> - <Occurence FileName="\autoorganizelog.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\autoorganizelog.html" LineNumber="8" /> - <Occurence FileName="\scripts\autoorganizelog.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\autoorganizelog.js" LineNumber="293" /> - <Occurence FileName="\scripts\autoorganizelog.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\autoorganizelog.js" LineNumber="294" /> - <Occurence FileName="\scripts\autoorganizelog.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\autoorganizelog.js" LineNumber="296" /> - </Dictionary> - <Dictionary Token="LinkedToEmbyConnect" Text="Linked to Emby Connect" /> - <Dictionary Token="HeaderSupporterBenefits" Text="Emby Premiere Benefits" /> - <Dictionary Token="HeaderAddUser" Text="Add User" /> - <Dictionary Token="LabelAddConnectSupporterHelp" Text="To add a user who isn't listed, you'll need to first link their account to Emby Connect from their user profile page." /> - <Dictionary Token="LabelPinCode" Text="Pin code:" /> - <Dictionary Token="OptionHideWatchedContentFromLatestMedia" Text="Hide watched content from latest media"> - <Occurence FileName="\mypreferenceshome.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\mypreferenceshome.html" LineNumber="114" /> - </Dictionary> - <Dictionary Token="HeaderSync" Text="Sync"> - <Occurence FileName="\mysyncsettings.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\mysyncsettings.html" LineNumber="7" /> - <Occurence FileName="\scripts\registrationservices.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\registrationservices.js" LineNumber="175" /> - <Occurence FileName="\useredit.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\useredit.html" LineNumber="82" /> - </Dictionary> - <Dictionary Token="ButtonOk" Text="Ok"> - <Occurence FileName="\components\directorybrowser\directorybrowser.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\directorybrowser\directorybrowser.js" LineNumber="147" /> - <Occurence FileName="\components\fileorganizer\fileorganizer.template.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\fileorganizer\fileorganizer.template.html" LineNumber="45" /> - <Occurence FileName="\components\medialibrarycreator\medialibrarycreator.template.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\medialibrarycreator\medialibrarycreator.template.html" LineNumber="30" /> - <Occurence FileName="\components\metadataeditor\personeditor.template.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\metadataeditor\personeditor.template.html" LineNumber="33" /> - <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="372" /> - <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="453" /> - <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="504" /> - <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="542" /> - <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="590" /> - <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="630" /> - <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="661" /> - <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="706" /> - <Occurence FileName="\nowplaying.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\nowplaying.html" LineNumber="113" /> - <Occurence FileName="\scripts\ratingdialog.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\ratingdialog.js" LineNumber="42" /> - </Dictionary> - <Dictionary Token="ButtonCancel" Text="Cancel"> - <Occurence FileName="\components\tvproviders\schedulesdirect.template.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\tvproviders\schedulesdirect.template.html" LineNumber="68" /> - <Occurence FileName="\components\tvproviders\xmltv.template.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\tvproviders\xmltv.template.html" LineNumber="48" /> - <Occurence FileName="\connectlogin.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\connectlogin.html" LineNumber="74" /> - <Occurence FileName="\connectlogin.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\connectlogin.html" LineNumber="108" /> - <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="325" /> - <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="375" /> - <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="456" /> - <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="507" /> - <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="545" /> - <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="593" /> - <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="633" /> - <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="664" /> - <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="709" /> - <Occurence FileName="\forgotpassword.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\forgotpassword.html" LineNumber="23" /> - <Occurence FileName="\forgotpasswordpin.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\forgotpasswordpin.html" LineNumber="22" /> - <Occurence FileName="\livetvseriestimer.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\livetvseriestimer.html" LineNumber="62" /> - <Occurence FileName="\livetvtunerprovider-hdhomerun.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\livetvtunerprovider-hdhomerun.html" LineNumber="35" /> - <Occurence FileName="\livetvtunerprovider-m3u.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\livetvtunerprovider-m3u.html" LineNumber="19" /> - <Occurence FileName="\livetvtunerprovider-satip.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\livetvtunerprovider-satip.html" LineNumber="65" /> - <Occurence FileName="\login.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\login.html" LineNumber="27" /> - <Occurence FileName="\notificationsetting.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\notificationsetting.html" LineNumber="64" /> - <Occurence FileName="\scheduledtask.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scheduledtask.html" LineNumber="85" /> - <Occurence FileName="\scripts\librarylist.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\librarylist.js" LineNumber="349" /> - <Occurence FileName="\scripts\mediacontroller.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\mediacontroller.js" LineNumber="167" /> - <Occurence FileName="\scripts\mediacontroller.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\mediacontroller.js" LineNumber="436" /> - <Occurence FileName="\scripts\ratingdialog.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\ratingdialog.js" LineNumber="43" /> - <Occurence FileName="\scripts\site.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\site.js" LineNumber="1025" /> - <Occurence FileName="\scripts\userprofilespage.js" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scripts\userprofilespage.js" LineNumber="198" /> - <Occurence FileName="\syncsettings.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\syncsettings.html" LineNumber="43" /> - <Occurence FileName="\useredit.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\useredit.html" LineNumber="111" /> - <Occurence FileName="\userlibraryaccess.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\userlibraryaccess.html" LineNumber="57" /> - <Occurence FileName="\usernew.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\usernew.html" LineNumber="45" /> - <Occurence FileName="\userparentalcontrol.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\userparentalcontrol.html" LineNumber="101" /> - </Dictionary> - <Dictionary Token="ButtonExit" Text="Exit" /> - <Dictionary Token="ButtonNew" Text="New"> - <Occurence FileName="\components\fileorganizer\fileorganizer.template.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\components\fileorganizer\fileorganizer.template.html" LineNumber="18" /> - <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="107" /> - <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="278" /> - <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="290" /> - <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="296" /> - <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="302" /> - <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="308" /> - <Occurence FileName="\dlnaprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofile.html" LineNumber="314" /> - <Occurence FileName="\dlnaprofiles.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dlnaprofiles.html" LineNumber="14" /> - <Occurence FileName="\serversecurity.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\serversecurity.html" LineNumber="8" /> - </Dictionary> - <Dictionary Token="HeaderTaskTriggers" Text="Task Triggers"> - <Occurence FileName="\scheduledtask.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\scheduledtask.html" LineNumber="11" /> - </Dictionary> - <Dictionary Token="HeaderTV" Text="TV"> - <Occurence FileName="\librarysettings.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\librarysettings.html" LineNumber="113" /> - </Dictionary> - <Dictionary Token="HeaderAudio" Text="Audio"> - <Occurence FileName="\librarysettings.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\librarysettings.html" LineNumber="39" /> - </Dictionary> - <Dictionary Token="HeaderVideo" Text="Video"> - <Occurence FileName="\librarysettings.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\librarysettings.html" LineNumber="50" /> - </Dictionary> - <Dictionary Token="HeaderPaths" Text="Paths"> - <Occurence FileName="\dashboard.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\dashboard.html" LineNumber="92" /> - </Dictionary> - <Dictionary Token="CategorySync" Text="Sync" /> - <Dictionary Token="TabPlaylist" Text="Playlist"> - <Occurence FileName="\nowplaying.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\nowplaying.html" LineNumber="20" /> - </Dictionary> - <Dictionary Token="HeaderEasyPinCode" Text="Easy Pin Code"> - <Occurence FileName="\myprofile.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\myprofile.html" LineNumber="69" /> - <Occurence FileName="\userpassword.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\userpassword.html" LineNumber="42" /> - </Dictionary> - <Dictionary Token="HeaderGrownupsOnly" Text="Grown-ups Only!" /> - <Dictionary Token="DividerOr" Text="-- or --" /> - <Dictionary Token="HeaderInstalledServices" Text="Installed Services"> - <Occurence FileName="\appservices.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\appservices.html" LineNumber="6" /> - </Dictionary> - <Dictionary Token="HeaderAvailableServices" Text="Available Services"> - <Occurence FileName="\appservices.html" FullPath="F:\Projects\Softworkz_Emby\Emby\MediaBrowser.WebDashboard\dashboard-ui\appservices.html" LineNumber="11" /> - </Dictionary> -</StringUsages> diff --git a/MediaBrowser.Tests/ConsistencyTests/StringUsageReporter.cs b/MediaBrowser.Tests/ConsistencyTests/StringUsageReporter.cs deleted file mode 100644 index 1fd511e86..000000000 --- a/MediaBrowser.Tests/ConsistencyTests/StringUsageReporter.cs +++ /dev/null @@ -1,259 +0,0 @@ -using MediaBrowser.Tests.ConsistencyTests.TextIndexing; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Xml; - -namespace MediaBrowser.Tests.ConsistencyTests -{ - /// <summary> - /// This class contains tests for reporting the usage of localization string tokens - /// in the dashboard-ui or similar. - /// </summary> - /// <remarks> - /// <para>Run one of the two tests using Visual Studio's "Test Explorer":</para> - /// <para> - /// <list type="bullet"> - /// <item><see cref="ReportStringUsage"/></item> - /// <item><see cref="ReportUnusedStrings"/></item> - /// </list> - /// </para> - /// <para> - /// On successful run, the bottom section of the test explorer will contain a link "Output". - /// This link will open the test results, displaying the trace and two attachment links. - /// One link will open the output folder, the other link will open the output xml file. - /// </para> - /// <para> - /// The output xml file contains a stylesheet link to render the results as html. - /// How that works depends on the default application configured for XML files: - /// </para> - /// <para><list type="bullet"> - /// <item><term>Visual Studio</term> - /// <description>Will open in XML source view. To view the html result, click menu - /// 'XML' => 'Start XSLT without debugging'</description></item> - /// <item><term>Internet Explorer</term> - /// <description>XSL transform will be applied automatically.</description></item> - /// <item><term>Firefox</term> - /// <description>XSL transform will be applied automatically.</description></item> - /// <item><term>Chrome</term> - /// <description>Does not work. Chrome is unable/unwilling to apply xslt transforms from local files.</description></item> - /// </list></para> - /// </remarks> - [TestClass] - public class StringUsageReporter - { - /// <summary> - /// Root path of the web application - /// </summary> - /// <remarks> - /// Can be an absolute path or a path relative to the binaries folder (bin\Debug). - /// </remarks> - public const string WebFolder = @"..\..\..\MediaBrowser.WebDashboard\dashboard-ui"; - - /// <summary> - /// Path to the strings file, relative to <see cref="WebFolder"/>. - /// </summary> - public const string StringsFile = @"strings\en-US.json"; - - /// <summary> - /// Path to the output folder - /// </summary> - /// <remarks> - /// Can be an absolute path or a path relative to the binaries folder (bin\Debug). - /// Important: When changing the output path, make sure that "StringCheck.xslt" is present - /// to make the XML transform work. - /// </remarks> - public const string OutputPath = @"."; - - /// <summary> - /// List of file extension to search. - /// </summary> - public static string[] TargetExtensions = new[] { ".js", ".html" }; - - /// <summary> - /// List of paths to exclude from search. - /// </summary> - public static string[] ExcludePaths = new[] { @"\bower_components\", @"\thirdparty\" }; - - private TestContext testContextInstance; - - /// <summary> - ///Gets or sets the test context which provides - ///information about and functionality for the current test run. - ///</summary> - public TestContext TestContext - { - get - { - return testContextInstance; - } - set - { - testContextInstance = value; - } - } - - //[TestMethod] - //public void ReportStringUsage() - //{ - // this.CheckDashboardStrings(false); - //} - - [TestMethod] - public void ReportUnusedStrings() - { - this.CheckDashboardStrings(true); - } - - private void CheckDashboardStrings(Boolean unusedOnly) - { - // Init Folders - var currentDir = System.IO.Directory.GetCurrentDirectory(); - Trace("CurrentDir: {0}", currentDir); - - var rootFolderInfo = ResolveFolder(currentDir, WebFolder); - Trace("Web Root: {0}", rootFolderInfo.FullName); - - var outputFolderInfo = ResolveFolder(currentDir, OutputPath); - Trace("Output Path: {0}", outputFolderInfo.FullName); - - // Load Strings - var stringsFileName = Path.Combine(rootFolderInfo.FullName, StringsFile); - - if (!File.Exists(stringsFileName)) - { - throw new Exception(string.Format("Strings file not found: {0}", stringsFileName)); - } - - int lineNumbers; - var stringsDic = this.CreateStringsDictionary(new FileInfo(stringsFileName), out lineNumbers); - - Trace("Loaded {0} strings from strings file containing {1} lines", stringsDic.Count, lineNumbers); - - var allFiles = rootFolderInfo.GetFiles("*", SearchOption.AllDirectories); - - var filteredFiles1 = allFiles.Where(f => TargetExtensions.Any(e => string.Equals(e, f.Extension, StringComparison.OrdinalIgnoreCase))); - var filteredFiles2 = filteredFiles1.Where(f => !ExcludePaths.Any(p => f.FullName.Contains(p))); - - var selectedFiles = filteredFiles2.OrderBy(f => f.FullName).ToList(); - - var wordIndex = IndexBuilder.BuildIndexFromFiles(selectedFiles, rootFolderInfo.FullName); - - Trace("Created word index from {0} files containing {1} individual words", selectedFiles.Count, wordIndex.Keys.Count); - - var outputFileName = Path.Combine(outputFolderInfo.FullName, string.Format("StringCheck_{0:yyyyMMddHHmmss}.xml", DateTime.Now)); - var settings = new XmlWriterSettings - { - Indent = true, - Encoding = Encoding.UTF8, - WriteEndDocumentOnClose = true - }; - - Trace("Output file: {0}", outputFileName); - - using (XmlWriter writer = XmlWriter.Create(outputFileName, settings)) - { - writer.WriteStartDocument(true); - - // Write the Processing Instruction node. - string xslText = "type=\"text/xsl\" href=\"StringCheck.xslt\""; - writer.WriteProcessingInstruction("xml-stylesheet", xslText); - - writer.WriteStartElement("StringUsages"); - writer.WriteAttributeString("ReportTitle", unusedOnly ? "Unused Strings Report" : "String Usage Report"); - writer.WriteAttributeString("Mode", unusedOnly ? "UnusedOnly" : "All"); - - foreach (var kvp in stringsDic) - { - var occurences = wordIndex.Find(kvp.Key); - - if (occurences == null || !unusedOnly) - { - ////Trace("{0}: {1}", kvp.Key, kvp.Value); - writer.WriteStartElement("Dictionary"); - writer.WriteAttributeString("Token", kvp.Key); - writer.WriteAttributeString("Text", kvp.Value); - - if (occurences != null && !unusedOnly) - { - foreach (var occurence in occurences) - { - writer.WriteStartElement("Occurence"); - writer.WriteAttributeString("FileName", occurence.FileName); - writer.WriteAttributeString("FullPath", occurence.FullPath); - writer.WriteAttributeString("LineNumber", occurence.LineNumber.ToString()); - writer.WriteEndElement(); - ////Trace(" {0}:{1}", occurence.FileName, occurence.LineNumber); - } - } - - writer.WriteEndElement(); - } - } - } - - TestContext.AddResultFile(outputFileName); - TestContext.AddResultFile(outputFolderInfo.FullName); - } - - private SortedDictionary<string, string> CreateStringsDictionary(FileInfo file, out int lineNumbers) - { - var dic = new SortedDictionary<string, string>(); - lineNumbers = 0; - - using (var reader = file.OpenText()) - { - while (!reader.EndOfStream) - { - lineNumbers++; - var words = reader - .ReadLine() - .Split(new[] { "\":" }, StringSplitOptions.RemoveEmptyEntries); - - - if (words.Length == 2) - { - var token = words[0].Replace("\"", string.Empty).Trim(); - var text = words[1].Replace("\",", string.Empty).Replace("\"", string.Empty).Trim(); - - if (dic.Keys.Contains(token)) - { - throw new Exception(string.Format("Double string entry found: {0}", token)); - } - - dic.Add(token, text); - } - } - } - - return dic; - } - - private DirectoryInfo ResolveFolder(string currentDir, string folderPath) - { - if (folderPath.IndexOf(@"\:") != 1) - { - folderPath = Path.Combine(currentDir, folderPath); - } - - var folderInfo = new DirectoryInfo(folderPath); - - if (!folderInfo.Exists) - { - throw new Exception(string.Format("Folder not found: {0}", folderInfo.FullName)); - } - - return folderInfo; - } - - - private void Trace(string message, params object[] parameters) - { - var formatted = string.Format(message, parameters); - System.Diagnostics.Trace.WriteLine(formatted); - } - } -} diff --git a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/IndexBuilder.cs b/MediaBrowser.Tests/ConsistencyTests/TextIndexing/IndexBuilder.cs deleted file mode 100644 index 4c46f4793..000000000 --- a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/IndexBuilder.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace MediaBrowser.Tests.ConsistencyTests.TextIndexing -{ - public class IndexBuilder - { - public const int MinumumWordLength = 4; - - public static char[] WordChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray(); - - public static WordIndex BuildIndexFromFiles(IEnumerable<FileInfo> wordFiles, string rootFolderPath) - { - var index = new WordIndex(); - - var wordSeparators = Enumerable.Range(32, 127).Select(e => Convert.ToChar(e)).Where(c => !WordChars.Contains(c)).ToArray(); - wordSeparators = wordSeparators.Concat(new[] { '\t' }).ToArray(); // add tab - - foreach (var file in wordFiles) - { - var lineNumber = 1; - var displayFileName = file.FullName.Replace(rootFolderPath, string.Empty); - using (var reader = file.OpenText()) - { - while (!reader.EndOfStream) - { - var words = reader - .ReadLine() - .Split(wordSeparators, StringSplitOptions.RemoveEmptyEntries); - ////.Select(f => f.Trim()); - - var wordIndex = 1; - foreach (var word in words) - { - if (word.Length >= MinumumWordLength) - { - index.AddWordOccurrence(word, displayFileName, file.FullName, lineNumber, wordIndex++); - } - } - - lineNumber++; - } - } - } - - return index; - } - - } -} diff --git a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordIndex.cs b/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordIndex.cs deleted file mode 100644 index e0af08792..000000000 --- a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordIndex.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace MediaBrowser.Tests.ConsistencyTests.TextIndexing -{ - public class WordIndex : Dictionary<string, WordOccurrences> - { - public WordIndex() : base(StringComparer.InvariantCultureIgnoreCase) - { - } - - public void AddWordOccurrence(string word, string fileName, string fullPath, int lineNumber, int wordIndex) - { - WordOccurrences current; - if (!this.TryGetValue(word, out current)) - { - current = new WordOccurrences(); - this[word] = current; - } - - current.AddOccurrence(fileName, fullPath, lineNumber, wordIndex); - } - - public WordOccurrences Find(string word) - { - WordOccurrences found; - if (this.TryGetValue(word, out found)) - { - return found; - } - - return null; - } - - } -} diff --git a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrence.cs b/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrence.cs deleted file mode 100644 index b30e58624..000000000 --- a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrence.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace MediaBrowser.Tests.ConsistencyTests.TextIndexing -{ - public struct WordOccurrence - { - public readonly string FileName; // file containing the word. - public readonly string FullPath; // file containing the word. - public readonly int LineNumber; // line within the file. - public readonly int WordIndex; // index within the line. - - public WordOccurrence(string fileName, string fullPath, int lineNumber, int wordIndex) - { - FileName = fileName; - FullPath = fullPath; - LineNumber = lineNumber; - WordIndex = wordIndex; - } - } -} diff --git a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrences.cs b/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrences.cs deleted file mode 100644 index a6388ab54..000000000 --- a/MediaBrowser.Tests/ConsistencyTests/TextIndexing/WordOccurrences.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; - -namespace MediaBrowser.Tests.ConsistencyTests.TextIndexing -{ - public class WordOccurrences : List<WordOccurrence> - { - public void AddOccurrence(string fileName, string fullPath, int lineNumber, int wordIndex) - { - this.Add(new WordOccurrence(fileName, fullPath, lineNumber, wordIndex)); - } - - } -} diff --git a/MediaBrowser.Tests/M3uParserTest.cs b/MediaBrowser.Tests/M3uParserTest.cs deleted file mode 100644 index 583f5f5f0..000000000 --- a/MediaBrowser.Tests/M3uParserTest.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Emby.Server.Implementations.Cryptography; -using Emby.Server.Implementations.LiveTv.TunerHosts; -using MediaBrowser.Common.Extensions; -using Microsoft.Extensions.Logging; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace MediaBrowser.Tests -{ - [TestClass] - public class M3uParserTest - { - [TestMethod] - public void TestFormat1() - { - BaseExtensions.CryptographyProvider = new CryptographyProvider(); - - var result = new M3uParser(new NullLogger(), null, null, null).ParseString("#EXTINF:0,84. VOX Schweiz\nhttp://mystream", "-", "-"); - Assert.AreEqual(1, result.Count); - - Assert.AreEqual("VOX Schweiz", result[0].Name); - Assert.AreEqual("84", result[0].Number); - } - [TestMethod] - public void TestFormat2() - { - BaseExtensions.CryptographyProvider = new CryptographyProvider(); - - var input = "#EXTINF:-1 tvg-id=\"\" tvg-name=\"ABC News 04\" tvg-logo=\"\" group-title=\"ABC Group\",ABC News 04"; - input += "\n"; - input += "http://mystream"; - - var result = new M3uParser(new NullLogger(), null, null, null).ParseString(input, "-", "-"); - Assert.AreEqual(1, result.Count); - - Assert.AreEqual("ABC News 04", result[0].Name); - Assert.IsNull(result[0].Number); - } - - [TestMethod] - public void TestFormat3() - { - BaseExtensions.CryptographyProvider = new CryptographyProvider(); - - var result = new M3uParser(new NullLogger(), null, null, null).ParseString("#EXTINF:0, 3.2 - Movies!\nhttp://mystream", "-", "-"); - Assert.AreEqual(1, result.Count); - - Assert.AreEqual("Movies!", result[0].Name); - Assert.AreEqual("3.2", result[0].Number); - } - - [TestMethod] - public void TestFormat4() - { - BaseExtensions.CryptographyProvider = new CryptographyProvider(); - - var result = new M3uParser(new NullLogger(), null, null, null).ParseString("#EXTINF:0 tvg-id=\"abckabclosangeles.path.to\" tvg-logo=\"path.to / channel_logos / abckabclosangeles.png\", ABC KABC Los Angeles\nhttp://mystream", "-", "-"); - Assert.AreEqual(1, result.Count); - - Assert.IsNull(result[0].Number); - Assert.AreEqual("ABC KABC Los Angeles", result[0].Name); - } - - [TestMethod] - public void TestFormat5() - { - BaseExtensions.CryptographyProvider = new CryptographyProvider(); - - var result = new M3uParser(new NullLogger(), null, null, null).ParseString("#EXTINF:-1 channel-id=\"2101\" tvg-id=\"I69387.json.schedulesdirect.org\" group-title=\"Entertainment\",BBC 1 HD\nhttp://mystream", "-", "-"); - Assert.AreEqual(1, result.Count); - - Assert.AreEqual("BBC 1 HD", result[0].Name); - Assert.AreEqual("2101", result[0].Number); - } - - [TestMethod] - public void TestFormat6() - { - BaseExtensions.CryptographyProvider = new CryptographyProvider(); - - var result = new M3uParser(new NullLogger(), null, null, null).ParseString("#EXTINF:-1 tvg-id=\"2101\" group-title=\"Entertainment\",BBC 1 HD\nhttp://mystream", "-", "-"); - Assert.AreEqual(1, result.Count); - - Assert.AreEqual("BBC 1 HD", result[0].Name); - Assert.AreEqual("2101", result[0].Number); - } - } -} diff --git a/MediaBrowser.Tests/MediaBrowser.Tests.csproj b/MediaBrowser.Tests/MediaBrowser.Tests.csproj deleted file mode 100644 index 6415d4211..000000000 --- a/MediaBrowser.Tests/MediaBrowser.Tests.csproj +++ /dev/null @@ -1,139 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="12.0" DefaultTargets="Build" - xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> - <PropertyGroup> - <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> - <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> - <ProjectGuid>{E22BFD35-0FCD-4A85-978A-C22DCD73A081}</ProjectGuid> - <OutputType>Library</OutputType> - <AppDesignerFolder>Properties</AppDesignerFolder> - <RootNamespace>MediaBrowser.Tests</RootNamespace> - <AssemblyName>MediaBrowser.Tests</AssemblyName> - <TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion> - <FileAlignment>512</FileAlignment> - <ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids> - <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion> - <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath> - <ReferencePath>$(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages</ReferencePath> - <IsCodedUITest>False</IsCodedUITest> - <TestProjectType>UnitTest</TestProjectType> - <TargetFrameworkProfile /> - </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> - <DebugSymbols>true</DebugSymbols> - <DebugType>full</DebugType> - <Optimize>false</Optimize> - <OutputPath>bin\Debug\</OutputPath> - <DefineConstants>DEBUG;TRACE</DefineConstants> - <ErrorReport>prompt</ErrorReport> - <WarningLevel>4</WarningLevel> - <DocumentationFile>bin\Debug\MediaBrowser.Tests.XML</DocumentationFile> - </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> - <DebugType>none</DebugType> - <Optimize>true</Optimize> - <OutputPath>bin\Release\</OutputPath> - <DefineConstants>TRACE</DefineConstants> - <ErrorReport>prompt</ErrorReport> - <WarningLevel>4</WarningLevel> - </PropertyGroup> - <ItemGroup> - <Reference Include="Emby.Server.MediaEncoding"> - <HintPath>..\ThirdParty\emby\Emby.Server.MediaEncoding.dll</HintPath> - </Reference> - <Reference Include="System" /> - <Reference Include="System.XML" /> - </ItemGroup> - <Choose> - <When Condition="('$(VisualStudioVersion)' == '10.0' or '$(VisualStudioVersion)' == '') and '$(TargetFrameworkVersion)' == 'v3.5'"> - <ItemGroup> - <Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" /> - </ItemGroup> - </When> - <Otherwise> - <ItemGroup> - <Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework" /> - </ItemGroup> - </Otherwise> - </Choose> - <ItemGroup> - <Compile Include="ConsistencyTests\StringUsageReporter.cs" /> - <Compile Include="ConsistencyTests\TextIndexing\IndexBuilder.cs" /> - <Compile Include="ConsistencyTests\TextIndexing\WordIndex.cs" /> - <Compile Include="ConsistencyTests\TextIndexing\WordOccurrence.cs" /> - <Compile Include="ConsistencyTests\TextIndexing\WordOccurrences.cs" /> - <Compile Include="M3uParserTest.cs" /> - <Compile Include="MediaEncoding\Subtitles\AssParserTests.cs" /> - <Compile Include="MediaEncoding\Subtitles\SrtParserTests.cs" /> - <Compile Include="MediaEncoding\Subtitles\VttWriterTest.cs" /> - <Compile Include="Properties\AssemblyInfo.cs" /> - </ItemGroup> - <ItemGroup> - <ProjectReference Include="..\Emby.Server.Implementations\Emby.Server.Implementations.csproj"> - <Project>{e383961b-9356-4d5d-8233-9a1079d03055}</Project> - <Name>Emby.Server.Implementations</Name> - </ProjectReference> - <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj"> - <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project> - <Name>MediaBrowser.Common</Name> - </ProjectReference> - <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj"> - <Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project> - <Name>MediaBrowser.Controller</Name> - </ProjectReference> - <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj"> - <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project> - <Name>MediaBrowser.Model</Name> - </ProjectReference> - <ProjectReference Include="..\MediaBrowser.Providers\MediaBrowser.Providers.csproj"> - <Project>{442B5058-DCAF-4263-BB6A-F21E31120A1B}</Project> - <Name>MediaBrowser.Providers</Name> - </ProjectReference> - <ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj"> - <Project>{23499896-b135-4527-8574-c26e926ea99e}</Project> - <Name>MediaBrowser.XbmcMetadata</Name> - </ProjectReference> - </ItemGroup> - <ItemGroup> - <None Include="app.config" /> - <None Include="MediaEncoding\Subtitles\TestSubtitles\data.ass" /> - <None Include="MediaEncoding\Subtitles\TestSubtitles\data2.ass" /> - <None Include="MediaEncoding\Subtitles\TestSubtitles\expected.vtt" /> - <None Include="MediaEncoding\Subtitles\TestSubtitles\unit.srt" /> - </ItemGroup> - <ItemGroup> - <ContentWithTargetPath Include="ConsistencyTests\Resources\StringCheck.xslt"> - <CopyToOutputDirectory>Always</CopyToOutputDirectory> - <TargetPath>StringCheck.xslt</TargetPath> - </ContentWithTargetPath> - <None Include="ConsistencyTests\Resources\SampleTransformed.htm" /> - <None Include="ConsistencyTests\Resources\StringCheckSample.xml" /> - </ItemGroup> - <Choose> - <When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'"> - <ItemGroup> - <Reference Include="Microsoft.VisualStudio.QualityTools.CodedUITestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> - <Private>False</Private> - </Reference> - <Reference Include="Microsoft.VisualStudio.TestTools.UITest.Common, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> - <Private>False</Private> - </Reference> - <Reference Include="Microsoft.VisualStudio.TestTools.UITest.Extension, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> - <Private>False</Private> - </Reference> - <Reference Include="Microsoft.VisualStudio.TestTools.UITesting, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> - <Private>False</Private> - </Reference> - </ItemGroup> - </When> - </Choose> - <Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" /> - <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> - <!-- To modify your build process, add your task inside one of the targets below and uncomment it. - Other similar extension points exist, see Microsoft.Common.targets. - <Target Name="BeforeBuild"> - </Target> - <Target Name="AfterBuild"> - </Target> - --> -</Project> diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/AssParserTests.cs b/MediaBrowser.Tests/MediaEncoding/Subtitles/AssParserTests.cs deleted file mode 100644 index b69faab11..000000000 --- a/MediaBrowser.Tests/MediaEncoding/Subtitles/AssParserTests.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.Text; -using MediaBrowser.MediaEncoding.Subtitles; -using MediaBrowser.Model.MediaInfo; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using Emby.Server.MediaEncoding.Subtitles; - -namespace MediaBrowser.Tests.MediaEncoding.Subtitles { - - [TestClass] - public class AssParserTests { - - [TestMethod] - public void TestParse() { - - var expectedSubs = - new SubtitleTrackInfo { - TrackEvents = new SubtitleTrackEvent[] { - new SubtitleTrackEvent { - Id = "1", - StartPositionTicks = 24000000, - EndPositionTicks = 72000000, - Text = - "Senator, we're "+ParserValues.NewLine+"making our final "+ParserValues.NewLine+"approach into Coruscant." - }, - new SubtitleTrackEvent { - Id = "2", - StartPositionTicks = 97100000, - EndPositionTicks = 133900000, - Text = - "Very good, Lieutenant." - }, - new SubtitleTrackEvent { - Id = "3", - StartPositionTicks = 150400000, - EndPositionTicks = 180400000, - Text = "It's "+ParserValues.NewLine+"a "+ParserValues.NewLine+"trap!" - } - } - }; - - var sut = new AssParser(); - - var stream = File.OpenRead(@"MediaEncoding\Subtitles\TestSubtitles\data.ass"); - - var result = sut.Parse(stream, CancellationToken.None); - - Assert.IsNotNull(result); - Assert.AreEqual(expectedSubs.TrackEvents.Length,result.TrackEvents.Length); - for (int i = 0; i < expectedSubs.TrackEvents.Length; i++) - { - Assert.AreEqual(expectedSubs.TrackEvents[i].Id, result.TrackEvents[i].Id); - Assert.AreEqual(expectedSubs.TrackEvents[i].StartPositionTicks, result.TrackEvents[i].StartPositionTicks); - Assert.AreEqual(expectedSubs.TrackEvents[i].EndPositionTicks, result.TrackEvents[i].EndPositionTicks); - Assert.AreEqual(expectedSubs.TrackEvents[i].Text, result.TrackEvents[i].Text); - } - - } - - [TestMethod] - public void TestParse2() - { - - var sut = new AssParser(); - - var stream = File.OpenRead(@"MediaEncoding\Subtitles\TestSubtitles\data2.ass"); - - var result = sut.Parse(stream, CancellationToken.None); - - Assert.IsNotNull(result); - - using (var ms = new MemoryStream()) - { - var writer = new SrtWriter(); - writer.Write(result, ms, CancellationToken.None); - - ms.Position = 0; - var text = Encoding.UTF8.GetString(ms.ToArray()); - var b = text; - } - - } - } -} diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/SrtParserTests.cs b/MediaBrowser.Tests/MediaEncoding/Subtitles/SrtParserTests.cs deleted file mode 100644 index aae96b382..000000000 --- a/MediaBrowser.Tests/MediaEncoding/Subtitles/SrtParserTests.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading; -using Emby.Server.MediaEncoding.Subtitles; -using Microsoft.Extensions.Logging; -using MediaBrowser.Model.MediaInfo; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace MediaBrowser.Tests.MediaEncoding.Subtitles -{ - - [TestClass] - public class SrtParserTests - { - - [TestMethod] - public void TestParse() - { - - var expectedSubs = - new SubtitleTrackInfo - { - TrackEvents = new SubtitleTrackEvent[] { - new SubtitleTrackEvent { - Id = "1", - StartPositionTicks = 24000000, - EndPositionTicks = 52000000, - Text = - "[Background Music Playing]" - }, - new SubtitleTrackEvent { - Id = "2", - StartPositionTicks = 157120000, - EndPositionTicks = 173990000, - Text = - "Oh my god, Watch out!"+ParserValues.NewLine+"It's coming!!" - }, - new SubtitleTrackEvent { - Id = "3", - StartPositionTicks = 257120000, - EndPositionTicks = 303990000, - Text = "[Bird noises]" - }, - new SubtitleTrackEvent { - Id = "4", - StartPositionTicks = 310000000, - EndPositionTicks = 319990000, - Text = - "This text is <font color=\"red\">RED</font> and has not been positioned." - }, - new SubtitleTrackEvent { - Id = "5", - StartPositionTicks = 320000000, - EndPositionTicks = 329990000, - Text = - "This is a"+ParserValues.NewLine+"new line, as is"+ParserValues.NewLine+"this" - }, - new SubtitleTrackEvent { - Id = "6", - StartPositionTicks = 330000000, - EndPositionTicks = 339990000, - Text = - "This contains nested <b>bold, <i>italic, <u>underline</u> and <s>strike-through</s></u></i></b> HTML tags" - }, - new SubtitleTrackEvent { - Id = "7", - StartPositionTicks = 340000000, - EndPositionTicks = 349990000, - Text = - "Unclosed but <b>supported HTML tags are left in, SSA italics aren't" - }, - new SubtitleTrackEvent { - Id = "8", - StartPositionTicks = 350000000, - EndPositionTicks = 359990000, - Text = - "<ggg>Unsupported</ggg> HTML tags are escaped and left in, even if <hhh>not closed." - }, - new SubtitleTrackEvent { - Id = "9", - StartPositionTicks = 360000000, - EndPositionTicks = 369990000, - Text = - "Multiple SSA tags are stripped" - }, - new SubtitleTrackEvent { - Id = "10", - StartPositionTicks = 370000000, - EndPositionTicks = 379990000, - Text = - "Greater than (<) and less than (>) are shown" - } - } - }; - - var sut = new SrtParser(new NullLogger()); - - var stream = File.OpenRead(@"MediaEncoding\Subtitles\TestSubtitles\unit.srt"); - - var result = sut.Parse(stream, CancellationToken.None); - - Assert.IsNotNull(result); - Assert.AreEqual(expectedSubs.TrackEvents.Length, result.TrackEvents.Length); - for (int i = 0; i < expectedSubs.TrackEvents.Length; i++) - { - Assert.AreEqual(expectedSubs.TrackEvents[i].Id, result.TrackEvents[i].Id); - Assert.AreEqual(expectedSubs.TrackEvents[i].StartPositionTicks, result.TrackEvents[i].StartPositionTicks); - Assert.AreEqual(expectedSubs.TrackEvents[i].EndPositionTicks, result.TrackEvents[i].EndPositionTicks); - Assert.AreEqual(expectedSubs.TrackEvents[i].Text, result.TrackEvents[i].Text); - } - - } - } -} diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data.ass b/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data.ass deleted file mode 100644 index 3114a844a..000000000 --- a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data.ass +++ /dev/null @@ -1,23 +0,0 @@ -[Script Info] -Title: Testing subtitles for the SSA Format - -[V4 Styles] -Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, Encoding -Style: Default,Arial,20,65535,65535,65535,-2147483640,-1,0,1,3,0,2,30,30,30,0,0 -Style: Titre_episode,Akbar,140,15724527,65535,65535,986895,-1,0,1,1,0,3,30,30,30,0,0 -Style: Wolf main,Wolf_Rain,56,15724527,15724527,15724527,4144959,0,0,1,1,2,2,5,5,30,0,0 - - - -[Events] -Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text -Dialogue: 0,0:00:02.40,0:00:07.20,Default,,0000,0000,0000,,Senator, {\kf89}we're \Nmaking our final \napproach into Coruscant. -Dialogue: 0,0:00:09.71,0:00:13.39,Default,,0000,0000,0000,,{\pos(400,570)}Very good, Lieutenant. -Dialogue: 0,0:00:15.04,0:00:18.04,Default,,0000,0000,0000,,It's \Na \ntrap! - - -[Pictures] -This section will be ignored - -[Fonts] -This section will be ignored
\ No newline at end of file diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data2.ass b/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data2.ass deleted file mode 100644 index 98585f636..000000000 --- a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/data2.ass +++ /dev/null @@ -1,391 +0,0 @@ -[Script Info] -Title: English (US) -ScriptType: v4.00+ -WrapStyle: 0 -PlayResX: 640 -PlayResY: 360 - -[V4+ Styles] -Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding -Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,0010,0010,0010,1 -Style: para-main,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,1,2,0020,0020,0015,0 -Style: para-main-top,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H001E0200,&H00000000,0,0,0,0,100,100,0,0,1,2,1,8,0010,0010,0017,0 -Style: para-internal,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H001E0200,&H00000000,0,1,0,0,100,100,0,0,1,2,1,2,0010,0010,0015,0 -Style: para-internal-top,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H001E0200,&H00000000,0,1,0,0,100,100,0,0,1,2,1,8,0010,0010,0017,0 -Style: para-overlap,Trebuchet MS,25,&H00BAFCF3,&H000000FF,&H001E0200,&H00000000,0,0,0,0,100,100,0,0,1,2,1,2,0010,0010,0015,0 -Style: para-narration,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H00000137,&H00000137,0,1,0,0,100,100,0,0,1,2,1,8,0020,0020,0015,0 -Style: para-internaloverlap,Trebuchet MS,25,&H00BAFCF3,&H000000FF,&H001E0200,&H00000000,0,1,0,0,100,100,0,0,1,2,1,2,0010,0010,0015,0 -Style: para-flashback,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H004D0000,&H00000000,0,0,0,0,100,100,0,0,1,2,1,2,0010,0010,0015,0 -Style: para-flashbackinternal,Trebuchet MS,25,&H00FFFFFF,&H000000FF,&H004D0701,&H00000000,0,1,0,0,100,100,0,0,1,2,1,2,0010,0010,0015,0 -Style: para-flashbackoverlap,Trebuchet MS,25,&H00BAFCF3,&H000000FF,&H004D0701,&H00000000,0,0,0,0,100,100,0,0,1,2,1,2,0010,0010,0015,0 -Style: para-title,arial,35,&H001F00C1,&H000000FF,&H00050058,&H00000137,1,0,0,0,100,100,0,0,1,1,0,7,0050,0020,0050,0 -Style: para-title-maxim,Times New Roman,25,&H00FFF3F3,&H000000FF,&H003B264A,&H00000137,0,0,0,0,100,100,0,0,1,1,0,4,0050,0020,0050,0 -Style: para-ep-title,Times New Roman,25,&H00F8FDFF,&H000000FF,&H005C5C5C,&H00273024,0,0,0,0,100,100,0,0,1,0,1,1,0056,0058,0060,0 -Style: para-next-ep,Trebuchet MS,22,&H009A8D94,&H000000FF,&H00000000,&H00273024,0,0,0,0,100,100,0,0,1,0,0,8,0000,0000,0135,0 -Style: tiny sign,Times New Roman,14,&H002C2F23,&H000000FF,&H00060600,&H00000000,1,0,0,0,100,100,0,0,1,2,0,8,0140,0010,0015,1 -Style: writing1,Verdana,16,&H00292C29,&H000000FF,&H002D241D,&H00000000,0,0,0,0,100,100,0,0,1,0,0,8,0080,0010,0025,1 -Style: writing2,Verdana,12,&H00292C29,&H000000FF,&H002D241D,&H00000000,0,0,0,0,100,100,0,0,1,0,0,3,0080,0090,0085,1 -Style: writing3,Verdana,16,&H00292C29,&H000000FF,&H002D241D,&H00000000,0,0,0,0,100,100,0,0,1,0,0,8,0010,0130,0080,1 -Style: recept,Trebuchet MS,12,&H00AFB2AC,&H000000FF,&H004C4D49,&H00000000,1,0,0,0,100,100,0,0,1,4,0,8,0010,0010,0020,1 -Style: food,Times New Roman,23,&H0056886C,&H000000FF,&H0083E5F9,&H00000000,1,0,0,0,100,100,0,0,1,4,0,7,0020,0010,0070,1 -Style: pad,Times New Roman,12,&H00445F6A,&H000000FF,&H007D6A4F,&H00000000,0,0,0,0,100,100,0,25,1,0,0,2,0040,0010,0105,1 -Style: chalk,Times New Roman,24,&H007B867F,&H000000FF,&H008EE3E9,&H00000000,0,0,0,0,100,100,0,0,1,0,0,7,0050,0050,0055,1 -Style: fortune,Times New Roman,18,&H00153249,&H000000FF,&H00727FA4,&H00000000,0,0,0,0,100,100,0,0,1,4,0,7,0060,0010,0030,1 -Style: fortune2,Times New Roman,24,&H003277AB,&H000000FF,&H00D0FFFF,&H00000000,1,0,0,0,100,100,0,0,1,4,0,8,0080,0000,0020,1 - -[Events] -Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text - -Dialogue: 0,0:00:06.89,0:00:10.62,para-main,M,0000,0000,0000,,I'm sorry to sour the mood, Shinichi, but... -Dialogue: 0,0:00:10.62,0:00:11.80,para-main,S,0000,0000,0000,,No way. -Dialogue: 0,0:00:11.80,0:00:12.49,para-main,S,0000,0000,0000,,You must be kidding. -Dialogue: 0,0:00:13.00,0:00:14.61,para-main,M,0000,0000,0000,,We need to start running right now. -Dialogue: 0,0:00:15.20,0:00:16.74,para-main,S,0000,0000,0000,,Are you sure it's him? -Dialogue: 0,0:00:17.25,0:00:20.06,para-main,M,0000,0000,0000,,These wavelengths are too \Npowerful to come from some lackey. -Dialogue: 0,0:00:20.06,0:00:22.81,para-main,M,0000,0000,0000,,And given the speed of his \Napproach, I'd say he's in a car. -Dialogue: 0,0:00:23.49,0:00:25.29,para-main,M,0000,0000,0000,,Take a right at that corner. -Dialogue: 0,0:00:25.76,0:00:26.72,para-main,S,0000,0000,0000,,Shit! -Dialogue: 0,0:00:26.72,0:00:28.43,para-main,S,0000,0000,0000,,Now I'm a thief. -Dialogue: 0,0:00:28.43,0:00:30.17,para-main,M,0000,0000,0000,,Is this the time to whine about it? -Dialogue: 0,0:00:31.10,0:00:34.25,para-main,S,0000,0000,0000,,When'd you learn to drive, anyway? -Dialogue: 0,0:00:34.70,0:00:37.80,para-main,M,0000,0000,0000,,I mastered Japanese in \Na single day, you know. -Dialogue: 0,0:00:39.72,0:00:41.46,para-main,S,0000,0000,0000,,Migi, I have a favor to ask. -Dialogue: 0,0:00:42.68,0:00:45.23,para-main,S,0000,0000,0000,,Please go somewhere with \Nas few people as possible. -Dialogue: 0,0:00:45.23,0:00:47.94,para-main,S,0000,0000,0000,,If we fight him in a city, \Nmany people will die. -Dialogue: 0,0:00:49.63,0:00:50.94,para-main,M,0000,0000,0000,,Very well. -Dialogue: 0,0:00:52.20,0:00:55.81,para-main,M,0000,0000,0000,,I've thought of something \Nthat's worth a gamble. -Dialogue: 0,0:01:35.53,0:01:42.66,para-title,,0000,0000,0000,,Parasyte -Dialogue: 0,0:01:37.74,0:01:42.66,para-title-maxim,,0000,0000,0000,,{\fad(2000,1)}The Maxim -Dialogue: 0,0:02:42.01,0:02:46.54,para-ep-title,Sign 0245,0000,0000,0000,,{\fad(350,500)\an3}Quiescence and Awakening -Dialogue: 0,0:02:48.95,0:02:49.69,para-main,S,0000,0000,0000,,Well? -Dialogue: 0,0:02:50.56,0:02:51.55,para-main,M,0000,0000,0000,,It didn't work. -Dialogue: 0,0:02:52.05,0:02:52.99,para-main,M,0000,0000,0000,,He's alive. -Dialogue: 0,0:02:53.89,0:02:55.55,para-main,M,0000,0000,0000,,He's tough. -Dialogue: 0,0:02:56.76,0:02:57.48,para-main,M,0000,0000,0000,,Let's go. -Dialogue: 0,0:02:58.00,0:02:58.82,para-main,S,0000,0000,0000,,Go where? -Dialogue: 0,0:03:00.01,0:03:00.87,para-main,M,0000,0000,0000,,Let's run. -Dialogue: 0,0:03:11.71,0:03:13.13,para-main,M,0000,0000,0000,,All right, stop. -Dialogue: 0,0:03:16.59,0:03:18.09,para-main,S,0000,0000,0000,,Why are we stopping? -Dialogue: 0,0:03:18.09,0:03:20.38,para-main,S,0000,0000,0000,,We can't afford to waste time here! -Dialogue: 0,0:03:20.38,0:03:21.47,para-main,M,0000,0000,0000,,Calm down. -Dialogue: 0,0:03:21.47,0:03:23.97,para-main,M,0000,0000,0000,,Let's strategize until he shows up. -Dialogue: 0,0:03:24.33,0:03:25.85,para-main,S,0000,0000,0000,,Strategize?! -Dialogue: 0,0:03:25.85,0:03:28.21,para-main,S,0000,0000,0000,,We might be minutes away \Nfrom being chopped up! -Dialogue: 0,0:03:28.21,0:03:29.24,para-main,M,0000,0000,0000,,Shinichi. -Dialogue: 0,0:03:29.24,0:03:30.69,para-main,M,0000,0000,0000,,I understand how you feel. -Dialogue: 0,0:03:30.69,0:03:32.48,para-main,M,0000,0000,0000,,Anyone would fear death. -Dialogue: 0,0:03:32.83,0:03:34.48,para-main,M,0000,0000,0000,,I'm afraid, as well. -Dialogue: 0,0:03:34.95,0:03:37.51,para-main,M,0000,0000,0000,,However, this is our moment of truth! -Dialogue: 0,0:03:39.54,0:03:43.25,para-main,M,0000,0000,0000,,You have a strength normal \Nhumans don't have. -Dialogue: 0,0:03:43.25,0:03:46.23,para-main,M,0000,0000,0000,,You can be calm, no matter \Nwhat the circumstance. -Dialogue: 0,0:03:46.85,0:03:48.89,para-main,M,0000,0000,0000,,Now, put your hand on your chest -Dialogue: 0,0:03:48.89,0:03:51.02,para-main,M,0000,0000,0000,,and breathe deeply like you always do. -Dialogue: 0,0:04:00.56,0:04:02.84,para-main,M,0000,0000,0000,,Good, well done. -Dialogue: 0,0:04:03.54,0:04:04.44,para-main,M,0000,0000,0000,,Listen. -Dialogue: 0,0:04:04.44,0:04:08.50,para-main,M,0000,0000,0000,,When it comes to ability, \NGotou surpasses us in every way. -Dialogue: 0,0:04:08.93,0:04:12.85,para-main,M,0000,0000,0000,,By simple calculations, our odds of \Nvictory might be zero percent. -Dialogue: 0,0:04:12.85,0:04:16.22,para-main,M,0000,0000,0000,,But that just means we should approach \Nthis from a different angle. -Dialogue: 0,0:04:16.79,0:04:20.18,para-main,M,0000,0000,0000,,If we can't win even by working together, -Dialogue: 0,0:04:20.85,0:04:23.36,para-main,M,0000,0000,0000,,maybe we should try {\i1}not{\i0} working together. -Dialogue: 0,0:04:23.63,0:04:24.24,para-main,S,0000,0000,0000,,What? -Dialogue: 0,0:04:25.01,0:04:28.12,para-main,M,0000,0000,0000,,In war, what matters is \Nopportunity, not numbers. -Dialogue: 0,0:04:28.12,0:04:29.27,para-main,S,0000,0000,0000,,Opportunity? -Dialogue: 0,0:04:29.86,0:04:33.16,para-main,M,0000,0000,0000,,In your pocket is a lighter I found in the car. -Dialogue: 0,0:04:42.79,0:04:44.01,para-main,Goto,0000,0000,0000,,They're above... -Dialogue: 0,0:04:44.31,0:04:48.01,para-main,Goto,0000,0000,0000,,They've spread out among \Nthe tree branches to hide. -Dialogue: 0,0:04:48.29,0:04:49.89,para-main,Goto,0000,0000,0000,,How unoriginal. -Dialogue: 0,0:04:50.90,0:04:52.68,para-flashbackinternal,M,0000,0000,0000,,This will be a race against time. -Dialogue: 0,0:04:53.27,0:04:57.40,para-flashbackinternal,M,0000,0000,0000,,To a parasite, the body is our lifeline \Nas well as our greatest weakness. -Dialogue: 0,0:04:58.18,0:05:01.39,para-flashbackinternal,M,0000,0000,0000,,My cells that have dispersed in your body -Dialogue: 0,0:05:01.39,0:05:05.21,para-flashbackinternal,M,0000,0000,0000,,have been completely integrated, \Nand are altered, -Dialogue: 0,0:05:05.21,0:05:06.91,para-flashbackinternal,M,0000,0000,0000,,so Gotou can't detect them. -Dialogue: 0,0:05:07.43,0:05:09.72,para-flashbackinternal,M,0000,0000,0000,,He will come straight for me -Dialogue: 0,0:05:09.72,0:05:11.66,para-flashbackinternal,M,0000,0000,0000,,without noticing your presence. -Dialogue: 0,0:05:12.17,0:05:15.50,para-flashbackinternal,M,0000,0000,0000,,If there is a protracted fight, \NI will shrivel up and die. -Dialogue: 0,0:05:15.50,0:05:17.67,para-flashbackinternal,M,0000,0000,0000,,This is an extremely reckless strategy. -Dialogue: 0,0:05:17.67,0:05:21.67,para-flashbackinternal,M,0000,0000,0000,,But that means even Gotou is \Nunlikely to anticipate our strategy. -Dialogue: 0,0:05:39.54,0:05:43.16,para-internal,Gotou,0000,0000,0000,,What, no counterattack? -Dialogue: 0,0:05:46.10,0:05:48.69,para-internal,Gotou,0000,0000,0000,,Where is the human boy? -Dialogue: 0,0:05:48.69,0:05:51.95,para-internal,Gotou,0000,0000,0000,,If only I can find and destroy the body, I'll win. -Dialogue: 0,0:05:53.56,0:05:54.58,para-flashbackinternal,M,0000,0000,0000,,His body -Dialogue: 0,0:05:55.03,0:05:58.58,para-flashbackinternal,M,0000,0000,0000,,is protected by semi-hardened parasite cells. -Dialogue: 0,0:05:59.12,0:06:02.59,para-flashbackinternal,M,0000,0000,0000,,It's unlikely that his entire body is armored, -Dialogue: 0,0:06:02.59,0:06:06.28,para-flashbackinternal,M,0000,0000,0000,,but there's no time to find where \Nthe chinks are in his armor. -Dialogue: 0,0:06:06.28,0:06:08.95,para-flashbackinternal,M,0000,0000,0000,,The part least likely to be armored -Dialogue: 0,0:06:08.95,0:06:12.47,para-flashbackinternal,M,0000,0000,0000,,and thus most suitable as a target... -Dialogue: 0,0:06:13.05,0:06:14.12,para-flashbackinternal,M,0000,0000,0000,,is his head. -Dialogue: 0,0:06:16.41,0:06:19.93,para-flashbackinternal,M,0000,0000,0000,,Unifying the multiple parasites \Nin his torso and limbs must -Dialogue: 0,0:06:19.93,0:06:22.95,para-flashbackinternal,M,0000,0000,0000,,require a tremendous amount of energy. -Dialogue: 0,0:06:22.95,0:06:25.96,para-flashbackinternal,M,0000,0000,0000,,Thus, the "head" has its hands \Nfull acting as the control tower. -Dialogue: 0,0:06:26.53,0:06:29.63,para-flashbackinternal,M,0000,0000,0000,,If we lop the head off, -Dialogue: 0,0:06:29.63,0:06:31.93,para-flashbackinternal,M,0000,0000,0000,,unity will be lost along with his armor, -Dialogue: 0,0:06:31.93,0:06:34.12,para-flashbackinternal,M,0000,0000,0000,,which should allow us to destroy his body. -Dialogue: 0,0:06:34.76,0:06:36.28,para-internal,S,0000,0000,0000,,Any time now, Migi! -Dialogue: 0,0:06:36.74,0:06:39.12,para-internal,S,0000,0000,0000,,If you don't hurry, you'll... -Dialogue: 0,0:06:39.64,0:06:41.33,para-internal,M,0000,0000,0000,,I will only have one chance! -Dialogue: 0,0:06:41.33,0:06:45.88,para-internal,M,0000,0000,0000,,If I am to decapitate Gotou when his \Npower and speed far surpasses my own... -Dialogue: 0,0:06:47.02,0:06:48.51,para-internal,M,0000,0000,0000,,What is this? -Dialogue: 0,0:06:48.51,0:06:50.73,para-internal,M,0000,0000,0000,,My consciousness is already fading... -Dialogue: 0,0:06:51.25,0:06:52.74,para-internal,M,0000,0000,0000,,I must hurry! -Dialogue: 0,0:06:52.74,0:06:54.89,para-internal,M,0000,0000,0000,,But the angle of attack is still poor. -Dialogue: 0,0:06:55.51,0:06:57.09,para-main,Gotou,0000,0000,0000,,Hey! Listen up! -Dialogue: 0,0:06:57.09,0:06:59.12,para-main,Gotou,0000,0000,0000,,Are you that scared of me?! -Dialogue: 0,0:06:59.12,0:07:03.19,para-main,Gotou,0000,0000,0000,,Spreading out in all directions \Nisn't much of a camouflage! -Dialogue: 0,0:07:03.51,0:07:06.69,para-main,Gotou,0000,0000,0000,,Use your brains to fight, not run! -Dialogue: 0,0:07:06.69,0:07:08.11,para-main,M,0000,0000,0000,,Now! Do it! -Dialogue: 0,0:07:08.62,0:07:09.49,para-internal,S,0000,0000,0000,,Was that my voice? -Dialogue: 0,0:07:09.88,0:07:11.24,para-main,G,0000,0000,0000,,There! -Dialogue: 0,0:07:17.41,0:07:20.29,para-flashbackinternal,M,0000,0000,0000,,The surface cells will instinctively disengage -Dialogue: 0,0:07:20.29,0:07:22.58,para-flashbackinternal,M,0000,0000,0000,,from Gotou's command upon exposure to fire, -Dialogue: 0,0:07:22.92,0:07:23.72,para-flashbackinternal,M,0000,0000,0000,,and as a result... -Dialogue: 0,0:07:28.94,0:07:29.67,para-internal,M,0000,0000,0000,,Damn! -Dialogue: 0,0:07:29.67,0:07:30.38,para-internal,M,0000,0000,0000,,Not deep enough! -Dialogue: 0,0:07:34.70,0:07:35.55,para-main,M,0000,0000,0000,,Did we fail? -Dialogue: 0,0:07:35.55,0:07:36.84,para-main,S,0000,0000,0000,,Migi! -Dialogue: 0,0:07:36.84,0:07:38.08,para-main,M,0000,0000,0000,,Stay back, Shinichi! -Dialogue: 0,0:07:39.79,0:07:40.77,para-main,M,0000,0000,0000,,We failed! -Dialogue: 0,0:07:41.31,0:07:43.43,para-main,Gotou,0000,0000,0000,,Well, this is a surprise. -Dialogue: 0,0:07:43.43,0:07:44.31,para-main,Gotou,0000,0000,0000,,Well done. -Dialogue: 0,0:07:44.52,0:07:45.52,para-main,M,0000,0000,0000,,Run! Now! -Dialogue: 0,0:07:45.52,0:07:46.33,para-main,S,0000,0000,0000,,But... -Dialogue: 0,0:07:46.33,0:07:47.36,para-main,M,0000,0000,0000,,Don't come any closer! -Dialogue: 0,0:07:47.61,0:07:48.98,para-main,M,0000,0000,0000,,We don't both need to die! -Dialogue: 0,0:07:52.43,0:07:54.11,para-main,S,0000,0000,0000,,But, Migi... -Dialogue: 0,0:07:59.77,0:08:00.83,para-main,M,0000,0000,0000,,What are you doing?! -Dialogue: 0,0:08:00.83,0:08:01.62,para-main,M,0000,0000,0000,,Hurry up and go! -Dialogue: 0,0:08:10.76,0:08:12.38,para-internal,M,0000,0000,0000,,Goodbye, Shinichi. -Dialogue: 0,0:08:13.06,0:08:15.48,para-internal,M,0000,0000,0000,,This is farewell, Shinichi. -Dialogue: 0,0:08:16.52,0:08:21.88,para-internal,M,0000,0000,0000,,I'm glad I didn't take over \Nyour brain when we first met. -Dialogue: 0,0:08:22.86,0:08:27.24,para-internal,M,0000,0000,0000,,Thanks to that, we made many \Ngood memories as friends... -Dialogue: 0,0:08:33.43,0:08:35.48,para-internal,M,0000,0000,0000,,I'm fading... -Dialogue: 0,0:08:35.91,0:08:37.32,para-internal,M,0000,0000,0000,,I feel oddly sleepy, -Dialogue: 0,0:08:38.47,0:08:42.85,para-internal,M,0000,0000,0000,,yet it's all eclipsed by the \Nfeeling that I'm so alone. -Dialogue: 0,0:08:45.37,0:08:46.44,para-internal,M,0000,0000,0000,,So this... -Dialogue: 0,0:08:47.45,0:08:48.37,para-internal,M,0000,0000,0000,,is death... -Dialogue: 0,0:09:48.98,0:09:49.91,para-main,Mitsu,0000,0000,0000,,Who's there?! -Dialogue: 0,0:09:53.90,0:09:55.91,para-main,S,0000,0000,0000,,Oh, sorry. -Dialogue: 0,0:09:55.91,0:09:57.35,para-main,Mitsu,0000,0000,0000,,A... A burglar?! -Dialogue: 0,0:09:57.35,0:09:58.41,para-main,S,0000,0000,0000,,No! -Dialogue: 0,0:09:59.03,0:10:00.66,para-main,S,0000,0000,0000,,I'm not... But... -Dialogue: 0,0:10:00.66,0:10:02.82,para-main,S,0000,0000,0000,,Sure. You can call me that. -Dialogue: 0,0:10:02.82,0:10:03.70,para-main,Mitsu,0000,0000,0000,,Huh? -Dialogue: 0,0:10:04.52,0:10:07.71,para-main,S,0000,0000,0000,,I did try to drink some water \Nwithout permission, after all. -Dialogue: 0,0:10:07.71,0:10:09.55,para-main,Mitsu,0000,0000,0000,,I see. -Dialogue: 0,0:10:09.55,0:10:11.97,para-main,Mitsu,0000,0000,0000,,Water isn't free, either. -Dialogue: 0,0:10:12.55,0:10:14.19,para-main,S,0000,0000,0000,,S-Sorry... -Dialogue: 0,0:10:14.82,0:10:16.39,para-main,S,0000,0000,0000,,Well, uh... -Dialogue: 0,0:10:16.87,0:10:18.19,para-main,S,0000,0000,0000,,I should go. -Dialogue: 0,0:10:18.19,0:10:19.75,para-main,S,0000,0000,0000,,I'm sorry for the trouble. -Dialogue: 0,0:10:23.35,0:10:24.99,para-main,Mitsu,0000,0000,0000,,Hang on a second. -Dialogue: 0,0:10:24.99,0:10:25.87,para-main,S,0000,0000,0000,,Yes? -Dialogue: 0,0:10:25.87,0:10:27.39,para-main,Mitsu,0000,0000,0000,,You're hurt. -Dialogue: 0,0:10:27.67,0:10:28.31,para-main,S,0000,0000,0000,,Oh... -Dialogue: 0,0:10:28.88,0:10:30.54,para-main,S,0000,0000,0000,,Well, no, uh... -Dialogue: 0,0:10:30.54,0:10:33.07,para-main,Mitsu,0000,0000,0000,,No, your head. -Dialogue: 0,0:10:33.07,0:10:36.03,para-main,Mitsu,0000,0000,0000,,You lost your right arm a long \Ntime ago, from the looks of it. -Dialogue: 0,0:10:37.39,0:10:38.44,para-main,S,0000,0000,0000,,I'm fine. -Dialogue: 0,0:10:38.93,0:10:40.77,para-main,S,0000,0000,0000,,I think the bleeding's already stopped. -Dialogue: 0,0:10:40.77,0:10:43.40,para-main,Mitsu,0000,0000,0000,,Just come in and let me take a look. -Dialogue: 0,0:10:43.81,0:10:44.41,para-main,S,0000,0000,0000,,But... -Dialogue: 0,0:10:44.41,0:10:45.74,para-main,Mitsu,0000,0000,0000,,Hurry up! -Dialogue: 0,0:10:45.74,0:10:48.69,para-main,Mitsu,0000,0000,0000,,A burglar wouldn't be this polite. -Dialogue: 0,0:10:48.69,0:10:51.29,para-main,Mitsu,0000,0000,0000,,Besides, you look like you've been crying. -Dialogue: 0,0:10:54.13,0:10:57.55,para-main,Mitsu,0000,0000,0000,,I worked in retail for a long time. -Dialogue: 0,0:10:57.55,0:11:01.89,para-main,Mitsu,0000,0000,0000,,I can tell a lot about a person from just one look. -Dialogue: 0,0:11:03.49,0:11:09.26,para-main,Mitsu,0000,0000,0000,,This injury wasn't from a fair fight, \Nwas it? You were bullied. -Dialogue: 0,0:11:09.26,0:11:10.39,para-main,S,0000,0000,0000,,Uh... -Dialogue: 0,0:11:11.56,0:11:14.15,para-main,Mitsu,0000,0000,0000,,The cut's pretty deep. -Dialogue: 0,0:11:14.70,0:11:18.14,para-main,Mitsu,0000,0000,0000,,Some people in this world do terrible things. -Dialogue: 0,0:11:30.20,0:11:33.22,para-main,S,0000,0000,0000,,I didn't expect so much kindness \Nfrom a complete stranger. -Dialogue: 0,0:11:34.82,0:11:36.92,para-main,S,0000,0000,0000,,Thank you for everything. -Dialogue: 0,0:11:37.35,0:11:39.76,para-main,Mitsu,0000,0000,0000,,Stay the night. -Dialogue: 0,0:11:39.76,0:11:40.48,para-main,S,0000,0000,0000,,What? -Dialogue: 0,0:11:40.48,0:11:42.34,para-main,S,0000,0000,0000,,I couldn't possibly... -Dialogue: 0,0:11:43.32,0:11:45.73,para-main,Mitsu,0000,0000,0000,,Where do you expect to go this late at night? -Dialogue: 0,0:11:45.73,0:11:48.05,para-main,Mitsu,0000,0000,0000,,There are no hotels around here! -Dialogue: 0,0:11:48.79,0:11:52.32,para-main,S,0000,0000,0000,,Um, do you live here by yourself, Granny? -Dialogue: 0,0:11:52.74,0:11:54.89,para-main,Mitsu,0000,0000,0000,,I don't have any grandchildren your age. -Dialogue: 0,0:11:55.41,0:11:56.91,para-main,S,0000,0000,0000,,Erm, Auntie? -Dialogue: 0,0:11:56.91,0:11:58.40,para-main,Mitsu,0000,0000,0000,,I don't have any nephews, either. -Dialogue: 0,0:11:59.38,0:12:01.40,para-main,Mitsu,0000,0000,0000,,My name is Mitsuyo. -Dialogue: 0,0:12:02.65,0:12:03.80,para-main,S,0000,0000,0000,,I'm sorry. -Dialogue: 0,0:12:04.31,0:12:06.91,para-main,S,0000,0000,0000,,I'm Izumi Shinichi. -Dialogue: 0,0:12:07.72,0:12:09.24,para-main,Mitsu,0000,0000,0000,,Shinichi, eh? -Dialogue: 0,0:12:09.86,0:12:11.22,para-main,Mitsu,0000,0000,0000,,Shin-chan, then. -Dialogue: 0,0:12:13.27,0:12:16.75,para-main,Mitsu,0000,0000,0000,,Sorry for making you help with the shopping. -Dialogue: 0,0:12:16.75,0:12:18.20,para-main,S,0000,0000,0000,,Oh, no problem. -Dialogue: 0,0:12:18.75,0:12:20.10,para-main,S,0000,0000,0000,,I can at least do that much. -Dialogue: 0,0:12:22.76,0:12:26.09,para-main,Mitsu,0000,0000,0000,,Let's just say you're my nephew. -Dialogue: 0,0:12:26.09,0:12:26.84,para-main,S,0000,0000,0000,,Nephew? -Dialogue: 0,0:12:26.84,0:12:29.39,para-main,Mitsu,0000,0000,0000,,Strange things have been \Nhappening around here lately. -Dialogue: 0,0:12:29.39,0:12:31.48,para-main,Mitsu,0000,0000,0000,,They're suspicious of outsiders. -Dialogue: 0,0:12:31.87,0:12:33.17,para-main,S,0000,0000,0000,,Strange things? -Dialogue: 0,0:12:36.59,0:12:38.12,para-main,Mitsu,0000,0000,0000,,This is what I was talking about. -Dialogue: 0,0:12:38.98,0:12:43.54,para-main,Mitsu,0000,0000,0000,,Someone keeps dumping truckloads \Nof garbage without permission. -Dialogue: 0,0:12:43.54,0:12:48.49,para-main,Mitsu,0000,0000,0000,,One time, it caught fire and nearly \Nset the entire mountain ablaze. -Dialogue: 0,0:12:48.85,0:12:52.74,para-main,Mitsu,0000,0000,0000,,I know they say big cities \Nare running out of landfills, -Dialogue: 0,0:12:52.74,0:12:55.11,para-main,Mitsu,0000,0000,0000,,but this is a bit much, don't you think? -Dialogue: 0,0:12:55.11,0:12:55.93,para-main,S,0000,0000,0000,,Yes... -Dialogue: 0,0:12:56.36,0:12:58.27,para-main,Mitsu,0000,0000,0000,,Those of us who live around here -Dialogue: 0,0:12:58.27,0:13:00.84,para-main,Mitsu,0000,0000,0000,,have been keeping watch day and night, -Dialogue: 0,0:13:01.16,0:13:03.50,para-main,Mitsu,0000,0000,0000,,but they're no-shows when we do keep watch -Dialogue: 0,0:13:03.50,0:13:06.49,para-main,Mitsu,0000,0000,0000,,and come the one day we sleep. -Dialogue: 0,0:13:06.49,0:13:09.38,para-internal,S,0000,0000,0000,,Mitsuyo-san had a sharp tongue, -Dialogue: 0,0:13:09.38,0:13:10.86,para-internal,S,0000,0000,0000,,but she was kindhearted. -Dialogue: 0,0:13:11.78,0:13:14.98,para-internal,S,0000,0000,0000,,Whenever I tried to thank her and leave, -Dialogue: 0,0:13:14.98,0:13:18.73,para-internal,S,0000,0000,0000,,she'd stop me with a machine gun \Nbarrage of conversation. -Dialogue: 0,0:13:19.43,0:13:20.86,para-internal,S,0000,0000,0000,,With Migi gone, -Dialogue: 0,0:13:20.86,0:13:23.55,para-internal,S,0000,0000,0000,,I had no idea what to do next. -Dialogue: 0,0:13:23.55,0:13:27.06,para-internal,S,0000,0000,0000,,I ended up staying several days. -Dialogue: 0,0:13:28.68,0:13:31.83,para-internal,S,0000,0000,0000,,But I can't impose on her forever. -Dialogue: 0,0:13:32.36,0:13:36.87,para-internal,S,0000,0000,0000,,I should go home tomorrow \Nand tell Dad everything. -Dialogue: 0,0:13:37.52,0:13:39.30,para-internal,S,0000,0000,0000,,About why I lost my right arm... -Dialogue: 0,0:13:39.93,0:13:42.39,para-internal,S,0000,0000,0000,,About how I had a friend named Migi... -Dialogue: 0,0:13:43.11,0:13:45.54,para-internal,S,0000,0000,0000,,About the day Migi first showed up in my life. -Dialogue: 0,0:13:46.48,0:13:48.39,para-internal,S,0000,0000,0000,,About the days we spent together. -Dialogue: 0,0:13:49.08,0:13:52.73,para-internal,S,0000,0000,0000,,And about how great a guy he was. -Dialogue: 0,0:13:53.94,0:13:55.77,para-internal,S,0000,0000,0000,,To save my life, he... -Dialogue: 0,0:13:55.77,0:13:58.14,para-internal,S,0000,0000,0000,,His intelligence, his courage... -Dialogue: 0,0:13:58.87,0:14:01.43,para-internal,S,0000,0000,0000,,I can't even hope to come \Nclose to him in any way. -Dialogue: 0,0:14:03.01,0:14:06.65,para-internal,S,0000,0000,0000,,He is a true hero! -Dialogue: 0,0:14:11.44,0:14:12.87,para-internal,S,0000,0000,0000,,Where am I? -Dialogue: 0,0:14:12.87,0:14:15.31,para-internal,S,0000,0000,0000,,I think I've been here before. -Dialogue: 0,0:14:16.02,0:14:17.22,para-internal,S,0000,0000,0000,,What's that? -Dialogue: 0,0:14:17.67,0:14:19.70,para-internal,S,0000,0000,0000,,Uh, who're you? -Dialogue: 0,0:14:19.70,0:14:20.63,para-internal,M,0000,0000,0000,,What is it? -Dialogue: 0,0:14:20.63,0:14:22.21,para-internal,M,0000,0000,0000,,Are you looking for something? -Dialogue: 0,0:14:22.21,0:14:24.08,para-internal,S,0000,0000,0000,,Looking? -Dialogue: 0,0:14:24.08,0:14:26.42,para-internal,S,0000,0000,0000,,Yeah, I'm looking for a friend. -Dialogue: 0,0:14:26.42,0:14:27.75,para-internal,M,0000,0000,0000,,A friend? -Dialogue: 0,0:14:28.17,0:14:30.27,para-internal,M,0000,0000,0000,,What does this friend look like? -Dialogue: 0,0:14:30.27,0:14:31.09,para-internal,S,0000,0000,0000,,Look like? -Dialogue: 0,0:14:31.72,0:14:34.12,para-internal,S,0000,0000,0000,,I don't really remember. -Dialogue: 0,0:14:34.12,0:14:35.43,para-internal,M,0000,0000,0000,,Then I can't help you. -Dialogue: 0,0:14:35.43,0:14:37.58,para-internal,S,0000,0000,0000,,Hey, wait. -Dialogue: 0,0:14:37.58,0:14:39.22,para-internal,S,0000,0000,0000,,I think he looked like you... -Dialogue: 0,0:14:39.76,0:14:41.81,para-internal,S,0000,0000,0000,,Right! I remember now! -Dialogue: 0,0:14:41.81,0:14:42.95,para-internal,S,0000,0000,0000,,He... -Dialogue: 0,0:14:42.95,0:14:44.86,para-internal,S,0000,0000,0000,,He died. -Dialogue: 0,0:14:44.86,0:14:46.83,para-internal,M,0000,0000,0000,,What? He's dead? -Dialogue: 0,0:14:46.83,0:14:47.69,para-internal,S,0000,0000,0000,,Yeah... -Dialogue: 0,0:14:48.28,0:14:51.43,para-internal,M,0000,0000,0000,,No, he's alive. -Dialogue: 0,0:14:51.43,0:14:52.07,para-internal,S,0000,0000,0000,,What?! -Dialogue: 0,0:14:52.16,0:14:53.97,para-internal,M,0000,0000,0000,,I can tell. -Dialogue: 0,0:14:53.97,0:14:56.60,para-internal,M,0000,0000,0000,,I actually know his name, too. -Dialogue: 0,0:14:56.60,0:14:58.55,para-internal,S,0000,0000,0000,,His... name? -Dialogue: 0,0:14:58.55,0:15:00.30,para-internal,S,0000,0000,0000,,His name... -Dialogue: 0,0:15:01.27,0:15:02.26,para-main,S,0000,0000,0000,,Migi?! -Dialogue: 0,0:15:07.49,0:15:08.26,para-main,S,0000,0000,0000,,Huh?! -Dialogue: 0,0:15:09.76,0:15:10.46,para-main,S,0000,0000,0000,,M... -Dialogue: 0,0:15:10.88,0:15:11.64,para-main,S,0000,0000,0000,,Migi! -Dialogue: 0,0:15:12.60,0:15:14.76,para-internal,S,0000,0000,0000,,Some of his cells are still here! -Dialogue: 0,0:15:15.68,0:15:17.37,para-main,S,0000,0000,0000,,Hey! It's me! -Dialogue: 0,0:15:17.37,0:15:18.50,para-main,S,0000,0000,0000,,Do you recognize me?! -Dialogue: 0,0:15:18.50,0:15:20.85,para-main,Mitsu,0000,0000,0000,,Keep it down. -Dialogue: 0,0:15:21.30,0:15:23.62,para-main,Mitsu,0000,0000,0000,,Go back to sleep. -Dialogue: 0,0:15:28.12,0:15:29.25,para-internal,S,0000,0000,0000,,It won't work. -Dialogue: 0,0:15:29.74,0:15:31.75,para-internal,S,0000,0000,0000,,Even if he can make a small eye, -Dialogue: 0,0:15:31.75,0:15:34.21,para-internal,S,0000,0000,0000,,it's not enough to be capable \Nof thought or speech. -Dialogue: 0,0:15:36.08,0:15:36.93,para-internal,S,0000,0000,0000,,Migi... -Dialogue: 0,0:15:38.42,0:15:39.24,para-internal,S,0000,0000,0000,,Migi! -Dialogue: 0,0:15:54.23,0:15:55.45,para-main,Mitsu,0000,0000,0000,,I see. -Dialogue: 0,0:15:55.45,0:15:57.42,para-main,Mitsu,0000,0000,0000,,I guess I don't have a choice. -Dialogue: 0,0:15:57.42,0:16:00.75,para-main,Mitsu,0000,0000,0000,,I can't keep you here forever. -Dialogue: 0,0:16:00.75,0:16:03.48,para-main,S,0000,0000,0000,,I don't know how I can ever repay you. -Dialogue: 0,0:16:08.17,0:16:09.58,para-main,Mitsu,0000,0000,0000,,What's the matter? -Dialogue: 0,0:16:09.58,0:16:11.49,para-main,Mitsu,0000,0000,0000,,Why're you here so early in the morning? -Dialogue: 0,0:16:11.49,0:16:13.00,para-main,Taoka,0000,0000,0000,,Hey, you! -Dialogue: 0,0:16:13.00,0:16:15.28,para-main,Taoka,0000,0000,0000,,Are you really Mitsuyo-san's nephew? -Dialogue: 0,0:16:15.89,0:16:18.16,para-main,Mitsu,0000,0000,0000,,What does it matter? -Dialogue: 0,0:16:18.16,0:16:20.54,para-main,Mitsu,0000,0000,0000,,He's about to leave. -Dialogue: 0,0:16:20.54,0:16:22.05,para-main,Taoka,0000,0000,0000,,Not so fast. -Dialogue: 0,0:16:22.05,0:16:24.79,para-main,Taoka,0000,0000,0000,,There are way too many strange \Nthings happening lately. -Dialogue: 0,0:16:24.79,0:16:26.70,para-main,Taoka,0000,0000,0000,,The illegal trash dumping, -Dialogue: 0,0:16:26.70,0:16:29.05,para-main,Taoka,0000,0000,0000,,the car crash between two \Ndriver-less vehicles, -Dialogue: 0,0:16:30.29,0:16:32.30,para-main,Taoka,0000,0000,0000,,and now, murder. -Dialogue: 0,0:16:32.65,0:16:33.88,para-main,Mitsu,0000,0000,0000,,What?! -Dialogue: 0,0:16:34.09,0:16:37.24,para-main,Taoka,0000,0000,0000,,Isn't it always outsiders who commit crimes? -Dialogue: 0,0:16:37.24,0:16:38.85,para-main,Taoka,0000,0000,0000,,Outsiders like him? -Dialogue: 0,0:16:41.62,0:16:45.50,para-main,Nakano,0000,0000,0000,,I told you, that thing was \Nbeyond being an outsider. -Dialogue: 0,0:16:45.50,0:16:47.74,para-main,Mitsu,0000,0000,0000,,He's been with me the entire time— -Dialogue: 0,0:16:47.15,0:16:49.19,para-overlap,Nakano,0000,0000,0000,,It wasn't human! -Dialogue: 0,0:16:49.59,0:16:51.40,para-main,Nakano,0000,0000,0000,,I wasn't just seeing things! -Dialogue: 0,0:16:51.40,0:16:53.24,para-main,Nakano,0000,0000,0000,,It was at least three meters tall! -Dialogue: 0,0:16:53.78,0:16:54.45,para-main,Nakano,0000,0000,0000,,And its legs! -Dialogue: 0,0:16:54.45,0:16:56.70,para-main,Nakano,0000,0000,0000,,Yeah, it had four front legs alone! -Dialogue: 0,0:16:56.98,0:16:59.49,para-main,Nakano,0000,0000,0000,,It had more than three eyes, too! -Dialogue: 0,0:17:00.45,0:17:01.79,para-internal,S,0000,0000,0000,,It's Gotou... -Dialogue: 0,0:17:01.79,0:17:03.20,para-internal,S,0000,0000,0000,,It has to be! -Dialogue: 0,0:17:03.20,0:17:06.23,para-main,Nakano,0000,0000,0000,,Yeah, laugh at me all you want. -Dialogue: 0,0:17:06.23,0:17:09.75,para-main,Nakano,0000,0000,0000,,But see this blood? It's all Kitayama's! -Dialogue: 0,0:17:10.19,0:17:14.85,para-main,Nakano,0000,0000,0000,,Kitayama was killed and eaten \Nby a monster right close by! -Dialogue: 0,0:17:19.80,0:17:22.29,para-main,Det,0000,0000,0000,,So it happened around here? -Dialogue: 0,0:17:22.29,0:17:24.43,para-main,Nakano,0000,0000,0000,,Th-That's right. -Dialogue: 0,0:17:29.90,0:17:31.64,para-main,Naitou,0000,0000,0000,,This is horrible. -Dialogue: 0,0:17:31.64,0:17:34.65,para-main,Cop,0000,0000,0000,,Wait, something similar's happened before... -Dialogue: 0,0:17:35.46,0:17:37.08,para-main,Det,0000,0000,0000,,The Mincemeat Murders? -Dialogue: 0,0:17:38.54,0:17:40.10,para-main,Mitsu,0000,0000,0000,,I see. -Dialogue: 0,0:17:40.10,0:17:42.04,para-main,Mitsu,0000,0000,0000,,Okay, got it. -Dialogue: 0,0:17:46.32,0:17:49.76,para-main,Mitsu,0000,0000,0000,,A bunch of hunters are going \Nto get together tomorrow, -Dialogue: 0,0:17:49.76,0:17:53.05,para-main,Mitsu,0000,0000,0000,,so they should be able to take \Ndown this "monster" then. -Dialogue: 0,0:17:55.04,0:17:58.12,para-main,S,0000,0000,0000,,Hunting rifles won't be \Nenough to take him down. -Dialogue: 0,0:17:58.12,0:17:58.90,para-main,Mitsu,0000,0000,0000,,Huh? -Dialogue: 0,0:17:58.90,0:18:00.05,para-main,Mitsu,0000,0000,0000,,"Him"? -Dialogue: 0,0:18:00.73,0:18:03.31,para-main,Mitsu,0000,0000,0000,,You know the monster? -Dialogue: 0,0:18:04.05,0:18:06.09,para-main,S,0000,0000,0000,,He's here because he's after me. -Dialogue: 0,0:18:06.57,0:18:08.01,para-main,S,0000,0000,0000,,To kill me. -Dialogue: 0,0:18:08.42,0:18:09.39,para-main,Mitsu,0000,0000,0000,,Huh? -Dialogue: 0,0:18:09.39,0:18:10.94,para-main,Mitsu,0000,0000,0000,,Stop talking nonsense. -Dialogue: 0,0:18:11.26,0:18:13.24,para-main,S,0000,0000,0000,,This is my fault! -Dialogue: 0,0:18:13.24,0:18:14.73,para-main,S,0000,0000,0000,,I brought him here! -Dialogue: 0,0:18:14.73,0:18:16.36,para-main,S,0000,0000,0000,,And someone was killed! -Dialogue: 0,0:18:17.15,0:18:20.18,para-main,S,0000,0000,0000,,Many more will die tomorrow \Nif I don't do something! -Dialogue: 0,0:18:21.02,0:18:22.41,para-main,Mitsu,0000,0000,0000,,Shin-chan... -Dialogue: 0,0:18:23.25,0:18:24.87,para-main,S,0000,0000,0000,,I just wanted to keep myself alive. -Dialogue: 0,0:18:25.38,0:18:26.85,para-main,S,0000,0000,0000,,Whatever it took. -Dialogue: 0,0:18:27.54,0:18:30.15,para-main,S,0000,0000,0000,,Friends have died for me, too. -Dialogue: 0,0:18:30.71,0:18:35.20,para-main,S,0000,0000,0000,,But I can't just keep running away on my own. -Dialogue: 0,0:18:36.07,0:18:37.43,para-main,Mitsu,0000,0000,0000,,Why not? -Dialogue: 0,0:18:41.97,0:18:43.90,para-main,Mitsu,0000,0000,0000,,Why not live? -Dialogue: 0,0:18:43.90,0:18:45.81,para-main,Mitsu,0000,0000,0000,,Why not run? -Dialogue: 0,0:18:45.81,0:18:48.98,para-main,Mitsu,0000,0000,0000,,Run if it's only to save your own life. -Dialogue: 0,0:18:49.27,0:18:51.68,para-main,Mitsu,0000,0000,0000,,It's nothing to be ashamed of. -Dialogue: 0,0:18:52.39,0:18:57.15,para-main,S,0000,0000,0000,,Mitsuyo-san, I haven't done \Neverything I can just yet! -Dialogue: 0,0:18:57.80,0:19:00.79,para-main,S,0000,0000,0000,,I have to make use of my life \Nbefore a group of people -Dialogue: 0,0:19:00.79,0:19:03.28,para-main,S,0000,0000,0000,,come face-to-face with \Nthat monster tomorrow! -Dialogue: 0,0:19:04.33,0:19:05.22,para-main,Mitsu,0000,0000,0000,,You idiot! -Dialogue: 0,0:19:05.22,0:19:06.45,para-main,Mitsu,0000,0000,0000,,Cut the bullshit! -Dialogue: 0,0:19:06.45,0:19:08.99,para-main,Mitsu,0000,0000,0000,,Make use of your life? -Dialogue: 0,0:19:08.99,0:19:10.96,para-main,Mitsu,0000,0000,0000,,How dare you speak so \Nlightly of your own life?! -Dialogue: 0,0:19:11.33,0:19:13.14,para-main,Mitsu,0000,0000,0000,,Who do you think you are?! -Dialogue: 0,0:19:13.14,0:19:14.88,para-main,Mitsu,0000,0000,0000,,Use your life? -Dialogue: 0,0:19:14.88,0:19:16.40,para-main,Mitsu,0000,0000,0000,,Don't make me laugh! -Dialogue: 0,0:19:16.40,0:19:19.47,para-main,Mitsu,0000,0000,0000,,What does a snot-nosed brat \Nlike you expect to do?! -Dialogue: 0,0:19:24.99,0:19:27.65,para-main,Mitsu,0000,0000,0000,,Look, I don't know your story, -Dialogue: 0,0:19:27.65,0:19:31.42,para-main,Mitsu,0000,0000,0000,,but you should just let adults \Nhandle this sort of thing. -Dialogue: 0,0:19:40.56,0:19:42.61,para-main,Mitsu,0000,0000,0000,,You're still leaving? -Dialogue: 0,0:19:43.78,0:19:48.70,para-main,Mitsu,0000,0000,0000,,Don't you have someone \Nin your life you care about? -Dialogue: 0,0:19:48.70,0:19:50.98,para-main,Mitsu,0000,0000,0000,,Even if it's a stranger, -Dialogue: 0,0:19:50.98,0:19:53.08,para-main,Mitsu,0000,0000,0000,,once I come to know them, -Dialogue: 0,0:19:53.08,0:19:54.66,para-main,Mitsu,0000,0000,0000,,I can't just abandon them. -Dialogue: 0,0:19:54.66,0:19:57.20,para-main,Mitsu,0000,0000,0000,,That's what it means to be human. -Dialogue: 0,0:19:57.20,0:19:58.73,para-main,Mitsu,0000,0000,0000,,But you... -Dialogue: 0,0:20:00.87,0:20:05.52,para-main,Mitsu,0000,0000,0000,,I don't know how much time you have left, -Dialogue: 0,0:20:05.52,0:20:11.89,para-main,Mitsu,0000,0000,0000,,but give some thought to as many things, \Nas many ideas, as you can come up with. -Dialogue: 0,0:20:12.80,0:20:16.47,para-main,Mitsu,0000,0000,0000,,If you throw everything away, that's the end. -Dialogue: 0,0:20:17.05,0:20:20.10,para-main,Mitsu,0000,0000,0000,,Don't give up, no matter what, -Dialogue: 0,0:20:20.10,0:20:21.78,para-main,Mitsu,0000,0000,0000,,and be flexible. -Dialogue: 0,0:20:31.45,0:20:32.36,para-main,Mitsu,0000,0000,0000,,Wait. -Dialogue: 0,0:20:32.90,0:20:35.81,para-main,Mitsu,0000,0000,0000,,Isn't there something useful \Nyou can take with you? -Dialogue: 0,0:20:35.81,0:20:37.09,para-main,Mitsu,0000,0000,0000,,Like a weapon? -Dialogue: 0,0:20:41.43,0:20:42.84,para-main,S,0000,0000,0000,,This, then. -Dialogue: 0,0:20:42.84,0:20:46.42,para-main,Mitsu,0000,0000,0000,,What, that? It's all rusty. -Dialogue: 0,0:20:46.42,0:20:48.60,para-main,Mitsu,0000,0000,0000,,But I guess it's better than nothing. -Dialogue: 0,0:21:02.69,0:21:03.88,para-internal,Mitsu,0000,0000,0000,,Dear... -Dialogue: 0,0:21:04.62,0:21:06.32,para-internal,Mitsu,0000,0000,0000,,Please keep him safe. -Dialogue: 0,0:21:15.06,0:21:19.51,para-internal,S,0000,0000,0000,,If Gotou's adopted a totally \Ndifferent human appearance, -Dialogue: 0,0:21:19.51,0:21:21.48,para-internal,S,0000,0000,0000,,there'll be no way for me to recognize him. -Dialogue: 0,0:21:21.96,0:21:25.53,para-internal,S,0000,0000,0000,,But I'll cross that bridge when I come to it! -Dialogue: 0,0:22:46.84,0:22:51.03,para-next-ep,Sign 2248,0000,0000,0000,,{\fad(700,1)}Life and Oath -Dialogue: 0,0:22:46.93,0:22:47.67,para-main,S,0000,0000,0000,,Next time: -Dialogue: 0,0:22:48.64,0:22:49.81,para-main,S,0000,0000,0000,,"Life and Oath." diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/expected.vtt b/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/expected.vtt deleted file mode 100644 index b6352e7b5..000000000 --- a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/expected.vtt +++ /dev/null @@ -1,32 +0,0 @@ -WEBVTT - -00:00:02.400 --> 00:00:05.200 -[Background Music Playing] - -00:00:15.712 --> 00:00:17.399 -Oh my god, Watch out!<br />It's coming!! - -00:00:25.712 --> 00:00:30.399 -[Bird noises] - -00:00:31.000 --> 00:00:31.999 -This text is <font color="red">RED</font> and has not been positioned. - -00:00:32.000 --> 00:00:32.999 -This is a<br />new line, as is<br />this - -00:00:33.000 --> 00:00:33.999 -This contains nested <b>bold, <i>italic, <u>underline</u> and <s>strike-through</s></u></i></b> HTML tags - -00:00:34.000 --> 00:00:34.999 -Unclosed but <b>supported HTML tags are left in, SSA italics aren't - -00:00:35.000 --> 00:00:35.999 -<ggg>Unsupported</ggg> HTML tags are escaped and left in, even if <hhh>not closed. - -00:00:36.000 --> 00:00:36.999 -Multiple SSA tags are stripped - -00:00:37.000 --> 00:00:37.999 -Greater than (<) and less than (>) are shown - diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/unit.srt b/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/unit.srt deleted file mode 100644 index 1ce811bcb..000000000 --- a/MediaBrowser.Tests/MediaEncoding/Subtitles/TestSubtitles/unit.srt +++ /dev/null @@ -1,44 +0,0 @@ - - -1 -00:00:02.400 --> 00:00:05.200 -[Background Music Playing] - -2 -00:00:15,712 --> 00:00:17,399 X1:000 X2:000 Y1:050 Y2:100 -Oh my god, Watch out! -It's coming!! - -3 -00:00:25,712 --> 00:00:30,399 -[Bird noises] - -4 -00:00:31,000 --> 00:00:31,999 -This text is <font color="red">RED</font> and has not been {\pos(142,120)}positioned. - -5 -00:00:32,000 --> 00:00:32,999 -This is a\nnew line, as is\Nthis - -6 -00:00:33,000 --> 00:00:33,999 -This contains nested <b>bold, <i>italic, <u>underline</u> and <s>strike-through</s></u></i></b> HTML tags - -7 -00:00:34,000 --> 00:00:34,999 -Unclosed but <b>supported HTML tags are left in, {\i1} SSA italics aren't - -8 -00:00:35,000 --> 00:00:35,999 -<ggg>Unsupported</ggg> HTML tags are escaped and left in, even if <hhh>not closed. - -9 -00:00:36,000 --> 00:00:36,999 -Multiple {\bord-3.7\clip(1,m 50 0 b 100 0 100 100 50 100 b 0 100 0 0 50 0)\pos(142,120)\t(0,500,\fscx100\fscy100)\b1\c&H000000&}SSA tags are stripped - -10 -00:00:37,000 --> 00:00:37,999 -Greater than (<) and less than (>) are shown - - diff --git a/MediaBrowser.Tests/MediaEncoding/Subtitles/VttWriterTest.cs b/MediaBrowser.Tests/MediaEncoding/Subtitles/VttWriterTest.cs deleted file mode 100644 index 2d25bcb14..000000000 --- a/MediaBrowser.Tests/MediaEncoding/Subtitles/VttWriterTest.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading; -using Emby.Server.MediaEncoding.Subtitles; -using MediaBrowser.Model.MediaInfo; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace MediaBrowser.Tests.MediaEncoding.Subtitles { - - [TestClass] - public class VttWriterTest { - [TestMethod] - public void TestWrite() { - var infoSubs = - new SubtitleTrackInfo - { - TrackEvents = new SubtitleTrackEvent[] { - new SubtitleTrackEvent { - Id = "1", - StartPositionTicks = 24000000, - EndPositionTicks = 52000000, - Text = - "[Background Music Playing]" - }, - new SubtitleTrackEvent { - Id = "2", - StartPositionTicks = 157120000, - EndPositionTicks = 173990000, - Text = - "Oh my god, Watch out!<br />It's coming!!" - }, - new SubtitleTrackEvent { - Id = "3", - StartPositionTicks = 257120000, - EndPositionTicks = 303990000, - Text = "[Bird noises]" - }, - new SubtitleTrackEvent { - Id = "4", - StartPositionTicks = 310000000, - EndPositionTicks = 319990000, - Text = - "This text is <font color=\"red\">RED</font> and has not been positioned." - }, - new SubtitleTrackEvent { - Id = "5", - StartPositionTicks = 320000000, - EndPositionTicks = 329990000, - Text = - "This is a<br />new line, as is<br />this" - }, - new SubtitleTrackEvent { - Id = "6", - StartPositionTicks = 330000000, - EndPositionTicks = 339990000, - Text = - "This contains nested <b>bold, <i>italic, <u>underline</u> and <s>strike-through</s></u></i></b> HTML tags" - }, - new SubtitleTrackEvent { - Id = "7", - StartPositionTicks = 340000000, - EndPositionTicks = 349990000, - Text = - "Unclosed but <b>supported HTML tags are left in, SSA italics aren't" - }, - new SubtitleTrackEvent { - Id = "8", - StartPositionTicks = 350000000, - EndPositionTicks = 359990000, - Text = - "<ggg>Unsupported</ggg> HTML tags are escaped and left in, even if <hhh>not closed." - }, - new SubtitleTrackEvent { - Id = "9", - StartPositionTicks = 360000000, - EndPositionTicks = 369990000, - Text = - "Multiple SSA tags are stripped" - }, - new SubtitleTrackEvent { - Id = "10", - StartPositionTicks = 370000000, - EndPositionTicks = 379990000, - Text = - "Greater than (<) and less than (>) are shown" - } - } - }; - - var sut = new VttWriter(); - - if(File.Exists("testVTT.vtt")) - File.Delete("testVTT.vtt"); - using (var file = File.OpenWrite("testVTT.vtt")) - { - sut.Write(infoSubs, file, CancellationToken.None); - } - - var result = File.ReadAllText("testVTT.vtt"); - var expectedText = File.ReadAllText(@"MediaEncoding\Subtitles\TestSubtitles\expected.vtt"); - - Assert.AreEqual(expectedText, result); - } - } -} diff --git a/MediaBrowser.Tests/Properties/AssemblyInfo.cs b/MediaBrowser.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index 1bd3ef5d6..000000000 --- a/MediaBrowser.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Reflection; -using System.Resources; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("MediaBrowser.Tests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Jellyfin Project")] -[assembly: AssemblyProduct("Jellyfin System")] -[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License Version 2")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: NeutralResourcesLanguage("en")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/MediaBrowser.Tests/app.config b/MediaBrowser.Tests/app.config deleted file mode 100644 index 5c79b167f..000000000 --- a/MediaBrowser.Tests/app.config +++ /dev/null @@ -1,11 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<configuration> - <runtime> - <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> - <dependentAssembly> - <assemblyIdentity name="System.Data.SQLite" publicKeyToken="db937bc2d44ff139" culture="neutral"/> - <bindingRedirect oldVersion="0.0.0.0-1.0.94.0" newVersion="1.0.94.0"/> - </dependentAssembly> - </assemblyBinding> - </runtime> -<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2"/></startup></configuration> diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index c099e77d6..a43949367 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -10,7 +10,7 @@ </ItemGroup> <ItemGroup> - <None Include="jellyfin-web\src\**\*.*"> + <None Include="jellyfin-web\**\*.*"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> </ItemGroup> @@ -18,6 +18,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> </Project> diff --git a/MediaBrowser.WebDashboard/jellyfin-web b/MediaBrowser.WebDashboard/jellyfin-web deleted file mode 160000 -Subproject 1d0fd79eb1e4d0bf6a9f62f769a951970383bcf diff --git a/MediaBrowser.XbmcMetadata/Configuration/NfoOptions.cs b/MediaBrowser.XbmcMetadata/Configuration/NfoOptions.cs index f631439de..60dcde4db 100644 --- a/MediaBrowser.XbmcMetadata/Configuration/NfoOptions.cs +++ b/MediaBrowser.XbmcMetadata/Configuration/NfoOptions.cs @@ -6,6 +6,7 @@ namespace MediaBrowser.XbmcMetadata.Configuration { public class ConfigurationFactory : IConfigurationFactory { + /// <inheritdoc /> public IEnumerable<ConfigurationStore> GetConfigurations() { return new[] diff --git a/MediaBrowser.XbmcMetadata/EntryPoint.cs b/MediaBrowser.XbmcMetadata/EntryPoint.cs index 992991a7e..fe4d50efa 100644 --- a/MediaBrowser.XbmcMetadata/EntryPoint.cs +++ b/MediaBrowser.XbmcMetadata/EntryPoint.cs @@ -16,27 +16,30 @@ namespace MediaBrowser.XbmcMetadata { private readonly IUserDataManager _userDataManager; private readonly ILogger _logger; - private readonly ILibraryManager _libraryManager; private readonly IProviderManager _providerManager; private readonly IConfigurationManager _config; - public EntryPoint(IUserDataManager userDataManager, ILibraryManager libraryManager, ILogger logger, IProviderManager providerManager, IConfigurationManager config) + public EntryPoint( + IUserDataManager userDataManager, + ILogger logger, + IProviderManager providerManager, + IConfigurationManager config) { _userDataManager = userDataManager; - _libraryManager = libraryManager; _logger = logger; _providerManager = providerManager; _config = config; } + /// <inheritdoc /> public Task RunAsync() { - _userDataManager.UserDataSaved += _userDataManager_UserDataSaved; + _userDataManager.UserDataSaved += OnUserDataSaved; return Task.CompletedTask; } - void _userDataManager_UserDataSaved(object sender, UserDataSaveEventArgs e) + private void OnUserDataSaved(object sender, UserDataSaveEventArgs e) { if (e.SaveReason == UserDataSaveReason.PlaybackFinished || e.SaveReason == UserDataSaveReason.TogglePlayed || e.SaveReason == UserDataSaveReason.UpdateUserRating) { @@ -47,9 +50,10 @@ namespace MediaBrowser.XbmcMetadata } } + /// <inheritdoc /> public void Dispose() { - _userDataManager.UserDataSaved -= _userDataManager_UserDataSaved; + _userDataManager.UserDataSaved -= OnUserDataSaved; } private void SaveMetadataForItem(BaseItem item, ItemUpdateType updateReason) diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj index ba29c656b..1ca9e43bb 100644 --- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj +++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj @@ -12,6 +12,11 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + </PropertyGroup> + + <PropertyGroup> + <LangVersion>latest</LangVersion> </PropertyGroup> </Project> diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index 5896497ab..b8d0e6560 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -22,13 +22,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers public class BaseNfoParser<T> where T : BaseItem { - /// <summary> - /// The logger - /// </summary> - protected ILogger Logger { get; private set; } - protected IProviderManager ProviderManager { get; private set; } - - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly IConfigurationManager _config; private Dictionary<string, string> _validProviderIds; @@ -42,6 +35,19 @@ namespace MediaBrowser.XbmcMetadata.Parsers ProviderManager = providerManager; } + protected CultureInfo UsCulture { get; } = new CultureInfo("en-US"); + + /// <summary> + /// Gets the logger. + /// </summary> + protected ILogger Logger { get; } + + protected IProviderManager ProviderManager { get; } + + protected virtual bool SupportsUrlAfterClosingXmlTag => false; + + protected virtual string MovieDbParserSearchString => "themoviedb.org/movie/"; + /// <summary> /// Fetches metadata for an item from one xml file /// </summary> @@ -83,8 +89,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers Fetch(item, metadataFile, GetXmlReaderSettings(), cancellationToken); } - protected virtual bool SupportsUrlAfterClosingXmlTag => false; - /// <summary> /// Fetches the specified item. /// </summary> @@ -198,8 +202,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers } } - protected virtual string MovieDbParserSearchString => "themoviedb.org/movie/"; - protected void ParseProviderLinks(T item, string xml) { //Look for a match for the Regex pattern "tt" followed by 7 digits @@ -219,7 +221,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers var tmdbId = xml.Substring(index + srch.Length).TrimEnd('/').Split('-')[0]; if (!string.IsNullOrWhiteSpace(tmdbId) && int.TryParse(tmdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) { - item.SetProviderId(MetadataProviders.Tmdb, value.ToString(_usCulture)); + item.SetProviderId(MetadataProviders.Tmdb, value.ToString(UsCulture)); } } @@ -234,7 +236,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers var tvdbId = xml.Substring(index + srch.Length).TrimEnd('/'); if (!string.IsNullOrWhiteSpace(tvdbId) && int.TryParse(tvdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) { - item.SetProviderId(MetadataProviders.Tvdb, value.ToString(_usCulture)); + item.SetProviderId(MetadataProviders.Tvdb, value.ToString(UsCulture)); } } } @@ -291,7 +293,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers if (!string.IsNullOrEmpty(text)) { - if (float.TryParse(text, NumberStyles.Any, _usCulture, out var value)) + if (float.TryParse(text, NumberStyles.Any, UsCulture, out var value)) { item.CriticRating = value; } @@ -417,7 +419,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers if (!string.IsNullOrWhiteSpace(text)) { - if (int.TryParse(text.Split(' ')[0], NumberStyles.Integer, _usCulture, out var runtime)) + if (int.TryParse(text.Split(' ')[0], NumberStyles.Integer, UsCulture, out var runtime)) { item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks; } @@ -870,7 +872,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers if (!string.IsNullOrWhiteSpace(val)) { - if (int.TryParse(val, NumberStyles.Integer, _usCulture, out var intVal)) + if (int.TryParse(val, NumberStyles.Integer, UsCulture, out var intVal)) { sortOrder = intVal; } diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs index 7f4224076..82ac6c548 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; @@ -14,16 +13,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers { public class EpisodeNfoParser : BaseNfoParser<Episode> { - public void Fetch(MetadataResult<Episode> item, - List<LocalImageInfo> images, - string metadataFile, - CancellationToken cancellationToken) + public EpisodeNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager) + : base(logger, config, providerManager) { - Fetch(item, metadataFile, cancellationToken); } - private readonly CultureInfo UsCulture = new CultureInfo("en-US"); - + /// <inheritdoc /> protected override void Fetch(MetadataResult<Episode> item, string metadataFile, XmlReaderSettings settings, CancellationToken cancellationToken) { using (var fileStream = File.OpenRead(metadataFile)) @@ -73,11 +68,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers } } - /// <summary> - /// Fetches the data from XML node. - /// </summary> - /// <param name="reader">The reader.</param> - /// <param name="itemResult">The item result.</param> + /// <inheritdoc /> protected override void FetchDataFromXmlNode(XmlReader reader, MetadataResult<Episode> itemResult) { var item = itemResult.Item; @@ -212,10 +203,5 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; } } - - public EpisodeNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager) - : base(logger, config, providerManager) - { - } } } diff --git a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs index 0c4de9f33..79d9111fe 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs @@ -13,13 +13,15 @@ namespace MediaBrowser.XbmcMetadata.Parsers { public class MovieNfoParser : BaseNfoParser<Video> { + public MovieNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager) + : base(logger, config, providerManager) + { + } + + /// <inheritdoc /> protected override bool SupportsUrlAfterClosingXmlTag => true; - /// <summary> - /// Fetches the data from XML node. - /// </summary> - /// <param name="reader">The reader.</param> - /// <param name="itemResult">The item result.</param> + /// <inheritdoc /> protected override void FetchDataFromXmlNode(XmlReader reader, MetadataResult<Video> itemResult) { var item = itemResult.Item; @@ -35,14 +37,17 @@ namespace MediaBrowser.XbmcMetadata.Parsers { imdbId = reader.ReadElementContentAsString(); } + if (!string.IsNullOrWhiteSpace(imdbId)) { item.SetProviderId(MetadataProviders.Imdb, imdbId); } + if (!string.IsNullOrWhiteSpace(tmdbId)) { item.SetProviderId(MetadataProviders.Tmdb, tmdbId); } + break; } case "set": @@ -83,9 +88,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers case "artist": { var val = reader.ReadElementContentAsString(); - var movie = item as MusicVideo; - if (!string.IsNullOrWhiteSpace(val) && movie != null) + if (!string.IsNullOrWhiteSpace(val) && item is MusicVideo movie) { var list = movie.Artists.ToList(); list.Add(val); @@ -98,9 +102,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers case "album": { var val = reader.ReadElementContentAsString(); - var movie = item as MusicVideo; - if (!string.IsNullOrWhiteSpace(val) && movie != null) + if (!string.IsNullOrWhiteSpace(val) && item is MusicVideo movie) { movie.Album = val; } @@ -119,48 +122,41 @@ namespace MediaBrowser.XbmcMetadata.Parsers //xml = xml.Substring(xml.IndexOf('<')); //xml = xml.Substring(0, xml.LastIndexOf('>')); - using (var stringReader = new StringReader("<set>" + xml + "</set>")) + // These are not going to be valid xml so no sense in causing the provider to fail and spamming the log with exceptions + try { - // These are not going to be valid xml so no sense in causing the provider to fail and spamming the log with exceptions - try + using (var stringReader = new StringReader("<set>" + xml + "</set>")) + using (var reader = XmlReader.Create(stringReader, GetXmlReaderSettings())) { - using (var reader = XmlReader.Create(stringReader, GetXmlReaderSettings())) - { - reader.MoveToContent(); - reader.Read(); + reader.MoveToContent(); + reader.Read(); - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) + // Loop through each element + while (!reader.EOF && reader.ReadState == ReadState.Interactive) + { + if (reader.NodeType == XmlNodeType.Element) { - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "name": - movie.CollectionName = reader.ReadElementContentAsString(); - break; - default: - reader.Skip(); - break; - } - } - else + switch (reader.Name) { - reader.Read(); + case "name": + movie.CollectionName = reader.ReadElementContentAsString(); + break; + default: + reader.Skip(); + break; } } + else + { + reader.Read(); + } } } - catch (XmlException) - { - - } } - } + catch (XmlException) + { - public MovieNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager) - : base(logger, config, providerManager) - { + } } } } diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs index 882f3a9d3..d6c06f982 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs @@ -9,11 +9,12 @@ namespace MediaBrowser.XbmcMetadata.Parsers { public class SeasonNfoParser : BaseNfoParser<Season> { - /// <summary> - /// Fetches the data from XML node. - /// </summary> - /// <param name="reader">The reader.</param> - /// <param name="itemResult">The item result.</param> + public SeasonNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager) + : base(logger, config, providerManager) + { + } + + /// <inheritdoc /> protected override void FetchDataFromXmlNode(XmlReader reader, MetadataResult<Season> itemResult) { var item = itemResult.Item; @@ -39,10 +40,5 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; } } - - public SeasonNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager) - : base(logger, config, providerManager) - { - } } } diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs index b0f25ae64..278858b4a 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs @@ -11,15 +11,18 @@ namespace MediaBrowser.XbmcMetadata.Parsers { public class SeriesNfoParser : BaseNfoParser<Series> { + public SeriesNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager) + : base(logger, config, providerManager) + { + } + + /// <inheritdoc /> protected override bool SupportsUrlAfterClosingXmlTag => true; + /// <inheritdoc /> protected override string MovieDbParserSearchString => "themoviedb.org/tv/"; - /// <summary> - /// Fetches the data from XML node. - /// </summary> - /// <param name="reader">The reader.</param> - /// <param name="itemResult">The item result.</param> + /// <inheritdoc /> protected override void FetchDataFromXmlNode(XmlReader reader, MetadataResult<Series> itemResult) { var item = itemResult.Item; @@ -91,10 +94,5 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; } } - - public SeriesNfoParser(ILogger logger, IConfigurationManager config, IProviderManager providerManager) - : base(logger, config, providerManager) - { - } } } diff --git a/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs index 6e6a22794..3517bc32c 100644 --- a/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/AlbumNfoProvider.cs @@ -23,14 +23,14 @@ namespace MediaBrowser.XbmcMetadata.Providers _providerManager = providerManager; } + /// <inheritdoc /> protected override void Fetch(MetadataResult<MusicAlbum> result, string path, CancellationToken cancellationToken) { new BaseNfoParser<MusicAlbum>(_logger, _config, _providerManager).Fetch(result, path, cancellationToken); } + /// <inheritdoc /> protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService) - { - return directoryService.GetFile(Path.Combine(info.Path, "album.nfo")); - } + => directoryService.GetFile(Path.Combine(info.Path, "album.nfo")); } } diff --git a/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs index 20abfc7f3..03d8dbc7e 100644 --- a/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/ArtistNfoProvider.cs @@ -23,14 +23,14 @@ namespace MediaBrowser.XbmcMetadata.Providers _providerManager = providerManager; } + /// <inheritdoc /> protected override void Fetch(MetadataResult<MusicArtist> result, string path, CancellationToken cancellationToken) { new BaseNfoParser<MusicArtist>(_logger, _config, _providerManager).Fetch(result, path, cancellationToken); } + /// <inheritdoc /> protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService) - { - return directoryService.GetFile(Path.Combine(info.Path, "artist.nfo")); - } + => directoryService.GetFile(Path.Combine(info.Path, "artist.nfo")); } } diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs index 0a47ac8e1..ff3368bb9 100644 --- a/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/BaseNfoProvider.cs @@ -11,9 +11,16 @@ namespace MediaBrowser.XbmcMetadata.Providers public abstract class BaseNfoProvider<T> : ILocalMetadataProvider<T>, IHasItemChangeMonitor where T : BaseItem, new() { - protected IFileSystem FileSystem; + private IFileSystem _fileSystem; - public Task<MetadataResult<T>> GetMetadata(ItemInfo info, + protected BaseNfoProvider(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + } + + /// <inheritdoc /> + public Task<MetadataResult<T>> GetMetadata( + ItemInfo info, IDirectoryService directoryService, CancellationToken cancellationToken) { @@ -47,15 +54,13 @@ namespace MediaBrowser.XbmcMetadata.Providers return Task.FromResult(result); } + /// <inheritdoc /> protected abstract void Fetch(MetadataResult<T> result, string path, CancellationToken cancellationToken); - protected BaseNfoProvider(IFileSystem fileSystem) - { - FileSystem = fileSystem; - } - + /// <inheritdoc /> protected abstract FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService); + /// <inheritdoc /> public bool HasChanged(BaseItem item, IDirectoryService directoryService) { var file = GetXmlFile(new ItemInfo(item), directoryService); @@ -65,7 +70,7 @@ namespace MediaBrowser.XbmcMetadata.Providers return false; } - return file.Exists && FileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved; + return file.Exists && _fileSystem.GetLastWriteTimeUtc(file) > item.DateLastSaved; } public string Name => BaseNfoSaver.SaverName; diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs index 28a0514d5..7410b97e0 100644 --- a/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs @@ -25,6 +25,7 @@ namespace MediaBrowser.XbmcMetadata.Providers _providerManager = providerManager; } + /// <inheritdoc /> protected override void Fetch(MetadataResult<T> result, string path, CancellationToken cancellationToken) { var tmpItem = new MetadataResult<Video> @@ -42,9 +43,10 @@ namespace MediaBrowser.XbmcMetadata.Providers } } + /// <inheritdoc /> protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService) { - return MovieNfoSaver.GetMovieSavePaths(info, FileSystem) + return MovieNfoSaver.GetMovieSavePaths(info) .Select(directoryService.GetFile) .FirstOrDefault(i => i != null); } diff --git a/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs index f90f283cf..b2278ba4a 100644 --- a/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/EpisodeNfoProvider.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.IO; using System.Threading; using MediaBrowser.Common.Configuration; @@ -24,15 +23,13 @@ namespace MediaBrowser.XbmcMetadata.Providers _providerManager = providerManager; } + /// <inheritdoc /> protected override void Fetch(MetadataResult<Episode> result, string path, CancellationToken cancellationToken) { - var images = new List<LocalImageInfo>(); - - new EpisodeNfoParser(_logger, _config, _providerManager).Fetch(result, images, path, cancellationToken); - - result.Images = images; + new EpisodeNfoParser(_logger, _config, _providerManager).Fetch(result, path, cancellationToken); } + /// <inheritdoc /> protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService) { var path = Path.ChangeExtension(info.Path, ".nfo"); diff --git a/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs index 0ebc30293..2cf542054 100644 --- a/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/SeasonNfoProvider.cs @@ -23,11 +23,13 @@ namespace MediaBrowser.XbmcMetadata.Providers _providerManager = providerManager; } + /// <inheritdoc /> protected override void Fetch(MetadataResult<Season> result, string path, CancellationToken cancellationToken) { new SeasonNfoParser(_logger, _config, _providerManager).Fetch(result, path, cancellationToken); } + /// <inheritdoc /> protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService) => directoryService.GetFile(Path.Combine(info.Path, "season.nfo")); } diff --git a/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs index 19ac3dc97..25c8e0faf 100644 --- a/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs +++ b/MediaBrowser.XbmcMetadata/Providers/SeriesNfoProvider.cs @@ -23,11 +23,13 @@ namespace MediaBrowser.XbmcMetadata.Providers _providerManager = providerManager; } + /// <inheritdoc /> protected override void Fetch(MetadataResult<Series> result, string path, CancellationToken cancellationToken) { new SeriesNfoParser(_logger, _config, _providerManager).Fetch(result, path, cancellationToken); } + /// <inheritdoc /> protected override FileSystemMetadata GetXmlFile(ItemInfo info, IDirectoryService directoryService) => directoryService.GetFile(Path.Combine(info.Path, "tvshow.nfo")); } diff --git a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs index 430b93199..233b3cb89 100644 --- a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs @@ -15,26 +15,30 @@ namespace MediaBrowser.XbmcMetadata.Savers { public class AlbumNfoSaver : BaseNfoSaver { - protected override string GetLocalSavePath(BaseItem item) + public AlbumNfoSaver( + IFileSystem fileSystem, + IServerConfigurationManager configurationManager, + ILibraryManager libraryManager, + IUserManager userManager, + IUserDataManager userDataManager, + ILogger logger) + : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger) { - return Path.Combine(item.Path, "album.nfo"); } + /// <inheritdoc /> + protected override string GetLocalSavePath(BaseItem item) + => Path.Combine(item.Path, "album.nfo"); + + /// <inheritdoc /> protected override string GetRootElementName(BaseItem item) - { - return "album"; - } + => "album"; + /// <inheritdoc /> public override bool IsEnabledFor(BaseItem item, ItemUpdateType updateType) - { - if (!item.SupportsLocalMetadata) - { - return false; - } - - return item is MusicAlbum && updateType >= MinimumUpdateType; - } + => item.SupportsLocalMetadata && item is MusicAlbum && updateType >= MinimumUpdateType; + /// <inheritdoc /> protected override void WriteCustomElements(BaseItem item, XmlWriter writer) { var album = (MusicAlbum)item; @@ -52,8 +56,6 @@ namespace MediaBrowser.XbmcMetadata.Savers AddTracks(album.Tracks, writer); } - private readonly CultureInfo UsCulture = new CultureInfo("en-US"); - private void AddTracks(IEnumerable<BaseItem> tracks, XmlWriter writer) { foreach (var track in tracks.OrderBy(i => i.ParentIndexNumber ?? 0).ThenBy(i => i.IndexNumber ?? 0)) @@ -62,7 +64,7 @@ namespace MediaBrowser.XbmcMetadata.Savers if (track.IndexNumber.HasValue) { - writer.WriteElementString("position", track.IndexNumber.Value.ToString(UsCulture)); + writer.WriteElementString("position", track.IndexNumber.Value.ToString(CultureInfo.InvariantCulture)); } if (!string.IsNullOrEmpty(track.Name)) @@ -81,21 +83,19 @@ namespace MediaBrowser.XbmcMetadata.Savers } } + /// <inheritdoc /> protected override List<string> GetTagsUsed(BaseItem item) { var list = base.GetTagsUsed(item); - list.AddRange(new string[] - { - "track", - "artist", - "albumartist" - }); - return list; - } + list.AddRange( + new string[] + { + "track", + "artist", + "albumartist" + }); - public AlbumNfoSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger) - : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger) - { + return list; } } } diff --git a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs index 0876db5c1..04565ff7e 100644 --- a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs @@ -14,26 +14,24 @@ namespace MediaBrowser.XbmcMetadata.Savers { public class ArtistNfoSaver : BaseNfoSaver { - protected override string GetLocalSavePath(BaseItem item) + public ArtistNfoSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger) + : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger) { - return Path.Combine(item.Path, "artist.nfo"); } + /// <inheritdoc /> + protected override string GetLocalSavePath(BaseItem item) + => Path.Combine(item.Path, "artist.nfo"); + + /// <inheritdoc /> protected override string GetRootElementName(BaseItem item) - { - return "artist"; - } + => "artist"; + /// <inheritdoc /> public override bool IsEnabledFor(BaseItem item, ItemUpdateType updateType) - { - if (!item.SupportsLocalMetadata) - { - return false; - } - - return item is MusicArtist && updateType >= MinimumUpdateType; - } + => item.SupportsLocalMetadata && item is MusicArtist && updateType >= MinimumUpdateType; + /// <inheritdoc /> protected override void WriteCustomElements(BaseItem item, XmlWriter writer) { var artist = (MusicArtist)item; @@ -51,8 +49,6 @@ namespace MediaBrowser.XbmcMetadata.Savers AddAlbums(albums, writer); } - private readonly CultureInfo UsCulture = new CultureInfo("en-US"); - private void AddAlbums(IList<BaseItem> albums, XmlWriter writer) { foreach (var album in albums) @@ -66,13 +62,14 @@ namespace MediaBrowser.XbmcMetadata.Savers if (album.ProductionYear.HasValue) { - writer.WriteElementString("year", album.ProductionYear.Value.ToString(UsCulture)); + writer.WriteElementString("year", album.ProductionYear.Value.ToString(CultureInfo.InvariantCulture)); } writer.WriteEndElement(); } } + /// <inheritdoc /> protected override List<string> GetTagsUsed(BaseItem item) { var list = base.GetTagsUsed(item); @@ -81,12 +78,8 @@ namespace MediaBrowser.XbmcMetadata.Savers "album", "disbanded" }); - return list; - } - public ArtistNfoSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger) - : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger) - { + return list; } } } diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index 3ae72c472..d84bc2abb 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -25,76 +25,78 @@ namespace MediaBrowser.XbmcMetadata.Savers { public abstract class BaseNfoSaver : IMetadataFileSaver { - public static readonly string YouTubeWatchUrl = "https://www.youtube.com/watch?v="; - - private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - - private static readonly Dictionary<string, string> CommonTags = new[] { - - "plot", - "customrating", - "lockdata", - "dateadded", - "title", - "rating", - "year", - "sorttitle", - "mpaa", - "aspectratio", - "collectionnumber", - "tmdbid", - "rottentomatoesid", - "language", - "tvcomid", - "tagline", - "studio", - "genre", - "tag", - "runtime", - "actor", - "criticrating", - "fileinfo", - "director", - "writer", - "trailer", - "premiered", - "releasedate", - "outline", - "id", - "credits", - "originaltitle", - "watched", - "playcount", - "lastplayed", - "art", - "resume", - "biography", - "formed", - "review", - "style", - "imdbid", - "imdb_id", - "country", - "audiodbalbumid", - "audiodbartistid", - "enddate", - "lockedfields", - "zap2itid", - "tvrageid", - - "musicbrainzartistid", - "musicbrainzalbumartistid", - "musicbrainzalbumid", - "musicbrainzreleasegroupid", - "tvdbid", - "collectionitem", - - "isuserfavorite", - "userrating", + public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss"; + + public const string YouTubeWatchUrl = "https://www.youtube.com/watch?v="; - "countrycode" + private static readonly HashSet<string> _commonTags = new HashSet<string>(StringComparer.OrdinalIgnoreCase) + { + "plot", + "customrating", + "lockdata", + "dateadded", + "title", + "rating", + "year", + "sorttitle", + "mpaa", + "aspectratio", + "collectionnumber", + "tmdbid", + "rottentomatoesid", + "language", + "tvcomid", + "tagline", + "studio", + "genre", + "tag", + "runtime", + "actor", + "criticrating", + "fileinfo", + "director", + "writer", + "trailer", + "premiered", + "releasedate", + "outline", + "id", + "credits", + "originaltitle", + "watched", + "playcount", + "lastplayed", + "art", + "resume", + "biography", + "formed", + "review", + "style", + "imdbid", + "imdb_id", + "country", + "audiodbalbumid", + "audiodbartistid", + "enddate", + "lockedfields", + "zap2itid", + "tvrageid", + + "musicbrainzartistid", + "musicbrainzalbumartistid", + "musicbrainzalbumid", + "musicbrainzreleasegroupid", + "tvdbid", + "collectionitem", + + "isuserfavorite", + "userrating", + + "countrycode" + }; - }.ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); + // filters control characters but allows only properly-formed surrogate sequences + private const string _invalidXMLCharsRegex = @"(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFEFF\uFFFE\uFFFF]"; protected BaseNfoSaver( IFileSystem fileSystem, @@ -112,12 +114,17 @@ namespace MediaBrowser.XbmcMetadata.Savers FileSystem = fileSystem; } - protected IFileSystem FileSystem { get; private set; } - protected IServerConfigurationManager ConfigurationManager { get; private set; } - protected ILibraryManager LibraryManager { get; private set; } - protected IUserManager UserManager { get; private set; } - protected IUserDataManager UserDataManager { get; private set; } - protected ILogger Logger { get; private set; } + protected IFileSystem FileSystem { get; } + + protected IServerConfigurationManager ConfigurationManager { get; } + + protected ILibraryManager LibraryManager { get; } + + protected IUserManager UserManager { get; } + + protected IUserDataManager UserDataManager { get; } + + protected ILogger Logger { get; } protected ItemUpdateType MinimumUpdateType { @@ -132,35 +139,30 @@ namespace MediaBrowser.XbmcMetadata.Savers } } + /// <inheritdoc /> public string Name => SaverName; public static string SaverName => "Nfo"; + /// <inheritdoc /> public string GetSavePath(BaseItem item) - { - return GetLocalSavePath(item); - } + => GetLocalSavePath(item); /// <summary> /// Gets the save path. /// </summary> /// <param name="item">The item.</param> - /// <returns>System.String.</returns> + /// <returns><see cref="string" />.</returns> protected abstract string GetLocalSavePath(BaseItem item); /// <summary> /// Gets the name of the root element. /// </summary> /// <param name="item">The item.</param> - /// <returns>System.String.</returns> + /// <returns><see cref="string" />.</returns> protected abstract string GetRootElementName(BaseItem item); - /// <summary> - /// Determines whether [is enabled for] [the specified item]. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="updateType">Type of the update.</param> - /// <returns><c>true</c> if [is enabled for] [the specified item]; otherwise, <c>false</c>.</returns> + /// <inheritdoc /> public abstract bool IsEnabledFor(BaseItem item, ItemUpdateType updateType); protected virtual List<string> GetTagsUsed(BaseItem item) @@ -169,14 +171,16 @@ namespace MediaBrowser.XbmcMetadata.Savers foreach (var providerKey in item.ProviderIds.Keys) { var providerIdTagName = GetTagForProviderKey(providerKey); - if (!CommonTags.ContainsKey(providerIdTagName)) + if (!_commonTags.Contains(providerIdTagName)) { list.Add(providerIdTagName); } } + return list; } + /// <inheritdoc /> public void Save(BaseItem item, CancellationToken cancellationToken) { var path = GetSavePath(item); @@ -196,10 +200,11 @@ namespace MediaBrowser.XbmcMetadata.Savers private void SaveToFile(Stream stream, string path) { Directory.CreateDirectory(Path.GetDirectoryName(path)); + // On Windows, savint the file will fail if the file is hidden or readonly FileSystem.SetAttributes(path, false, false); - using (var filestream = FileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) + using (var filestream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) { stream.CopyTo(filestream); } @@ -216,9 +221,9 @@ namespace MediaBrowser.XbmcMetadata.Savers { FileSystem.SetHidden(path, hidden); } - catch (Exception ex) + catch (IOException ex) { - Logger.LogError(ex, "Error setting hidden attribute on {path}", path); + Logger.LogError(ex, "Error setting hidden attribute on {Path}", path); } } @@ -248,9 +253,7 @@ namespace MediaBrowser.XbmcMetadata.Savers WriteCustomElements(item, writer); - var hasMediaSources = baseItem as IHasMediaSources; - - if (hasMediaSources != null) + if (baseItem is IHasMediaSources hasMediaSources) { AddMediaInfo(hasMediaSources, writer); } @@ -259,7 +262,7 @@ namespace MediaBrowser.XbmcMetadata.Savers try { - AddCustomTags(xmlPath, tagsUsed, writer, Logger, FileSystem); + AddCustomTags(xmlPath, tagsUsed, writer, Logger); } catch (FileNotFoundException) { @@ -283,7 +286,7 @@ namespace MediaBrowser.XbmcMetadata.Savers protected abstract void WriteCustomElements(BaseItem item, XmlWriter writer); public static void AddMediaInfo<T>(T item, XmlWriter writer) - where T : IHasMediaSources + where T : IHasMediaSources { writer.WriteStartElement("fileinfo"); writer.WriteStartElement("streamdetails"); @@ -313,17 +316,17 @@ namespace MediaBrowser.XbmcMetadata.Savers if (stream.BitRate.HasValue) { - writer.WriteElementString("bitrate", stream.BitRate.Value.ToString(UsCulture)); + writer.WriteElementString("bitrate", stream.BitRate.Value.ToString(CultureInfo.InvariantCulture)); } if (stream.Width.HasValue) { - writer.WriteElementString("width", stream.Width.Value.ToString(UsCulture)); + writer.WriteElementString("width", stream.Width.Value.ToString(CultureInfo.InvariantCulture)); } if (stream.Height.HasValue) { - writer.WriteElementString("height", stream.Height.Value.ToString(UsCulture)); + writer.WriteElementString("height", stream.Height.Value.ToString(CultureInfo.InvariantCulture)); } if (!string.IsNullOrEmpty(stream.AspectRatio)) @@ -336,14 +339,14 @@ namespace MediaBrowser.XbmcMetadata.Savers if (framerate.HasValue) { - writer.WriteElementString("framerate", framerate.Value.ToString(UsCulture)); + writer.WriteElementString("framerate", framerate.Value.ToString(CultureInfo.InvariantCulture)); } if (!string.IsNullOrEmpty(stream.Language)) { // http://web.archive.org/web/20181230211547/https://emby.media/community/index.php?/topic/49071-nfo-not-generated-on-actualize-or-rescan-or-identify // Web Archive version of link since it's not really explained in the thread. - writer.WriteElementString("language", RemoveInvalidXMLChars(stream.Language)); + writer.WriteElementString("language", Regex.Replace(stream.Language, _invalidXMLCharsRegex, string.Empty)); } var scanType = stream.IsInterlaced ? "interlaced" : "progressive"; @@ -354,12 +357,12 @@ namespace MediaBrowser.XbmcMetadata.Savers if (stream.Channels.HasValue) { - writer.WriteElementString("channels", stream.Channels.Value.ToString(UsCulture)); + writer.WriteElementString("channels", stream.Channels.Value.ToString(CultureInfo.InvariantCulture)); } if (stream.SampleRate.HasValue) { - writer.WriteElementString("samplingrate", stream.SampleRate.Value.ToString(UsCulture)); + writer.WriteElementString("samplingrate", stream.SampleRate.Value.ToString(CultureInfo.InvariantCulture)); } writer.WriteElementString("default", stream.IsDefault.ToString()); @@ -372,13 +375,15 @@ namespace MediaBrowser.XbmcMetadata.Savers { var timespan = TimeSpan.FromTicks(runtimeTicks.Value); - writer.WriteElementString("duration", Math.Floor(timespan.TotalMinutes).ToString(UsCulture)); - writer.WriteElementString("durationinseconds", Math.Floor(timespan.TotalSeconds).ToString(UsCulture)); + writer.WriteElementString( + "duration", + Math.Floor(timespan.TotalMinutes).ToString(CultureInfo.InvariantCulture)); + writer.WriteElementString( + "durationinseconds", + Math.Floor(timespan.TotalSeconds).ToString(CultureInfo.InvariantCulture)); } - var video = item as Video; - - if (video != null) + if (item is Video video) { //AddChapters(video, builder, itemRepository); @@ -413,26 +418,18 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteEndElement(); } - // filters control characters but allows only properly-formed surrogate sequences - private static Regex _invalidXMLChars = new Regex( - @"(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFEFF\uFFFE\uFFFF]"); - - /// <summary> - /// removes any unusual unicode characters that can't be encoded into XML - /// </summary> - public static string RemoveInvalidXMLChars(string text) - { - if (string.IsNullOrEmpty(text)) return string.Empty; - return _invalidXMLChars.Replace(text, string.Empty); - } - - public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss"; - /// <summary> /// Adds the common nodes. /// </summary> /// <returns>Task.</returns> - private void AddCommonNodes(BaseItem item, XmlWriter writer, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataRepo, IFileSystem fileSystem, IServerConfigurationManager config) + private void AddCommonNodes( + BaseItem item, + XmlWriter writer, + ILibraryManager libraryManager, + IUserManager userManager, + IUserDataManager userDataRepo, + IFileSystem fileSystem, + IServerConfigurationManager config) { var writtenProviderIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase); @@ -524,12 +521,12 @@ namespace MediaBrowser.XbmcMetadata.Savers if (item.CommunityRating.HasValue) { - writer.WriteElementString("rating", item.CommunityRating.Value.ToString(UsCulture)); + writer.WriteElementString("rating", item.CommunityRating.Value.ToString(CultureInfo.InvariantCulture)); } if (item.ProductionYear.HasValue) { - writer.WriteElementString("year", item.ProductionYear.Value.ToString(UsCulture)); + writer.WriteElementString("year", item.ProductionYear.Value.ToString(CultureInfo.InvariantCulture)); } var forcedSortName = item.ForcedSortName; @@ -543,13 +540,10 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteElementString("mpaa", item.OfficialRating); } - var hasAspectRatio = item as IHasAspectRatio; - if (hasAspectRatio != null) + if (item is IHasAspectRatio hasAspectRatio + && !string.IsNullOrEmpty(hasAspectRatio.AspectRatio)) { - if (!string.IsNullOrEmpty(hasAspectRatio.AspectRatio)) - { - writer.WriteElementString("aspectratio", hasAspectRatio.AspectRatio); - } + writer.WriteElementString("aspectratio", hasAspectRatio.AspectRatio); } var tmdbCollection = item.GetProviderId(MetadataProviders.TmdbCollection); @@ -571,6 +565,7 @@ namespace MediaBrowser.XbmcMetadata.Savers { writer.WriteElementString("imdbid", imdb); } + writtenProviderIds.Add(MetadataProviders.Imdb.ToString()); } @@ -607,12 +602,18 @@ namespace MediaBrowser.XbmcMetadata.Savers if (item is MusicArtist) { - writer.WriteElementString("formed", item.PremiereDate.Value.ToLocalTime().ToString(formatString)); + writer.WriteElementString( + "formed", + item.PremiereDate.Value.ToLocalTime().ToString(formatString)); } else { - writer.WriteElementString("premiered", item.PremiereDate.Value.ToLocalTime().ToString(formatString)); - writer.WriteElementString("releasedate", item.PremiereDate.Value.ToLocalTime().ToString(formatString)); + writer.WriteElementString( + "premiered", + item.PremiereDate.Value.ToLocalTime().ToString(formatString)); + writer.WriteElementString( + "releasedate", + item.PremiereDate.Value.ToLocalTime().ToString(formatString)); } } @@ -622,18 +623,20 @@ namespace MediaBrowser.XbmcMetadata.Savers { var formatString = options.ReleaseDateFormat; - writer.WriteElementString("enddate", item.EndDate.Value.ToLocalTime().ToString(formatString)); + writer.WriteElementString( + "enddate", + item.EndDate.Value.ToLocalTime().ToString(formatString)); } } if (item.CriticRating.HasValue) { - writer.WriteElementString("criticrating", item.CriticRating.Value.ToString(UsCulture)); + writer.WriteElementString( + "criticrating", + item.CriticRating.Value.ToString(CultureInfo.InvariantCulture)); } - var hasDisplayOrder = item as IHasDisplayOrder; - - if (hasDisplayOrder != null) + if (item is IHasDisplayOrder hasDisplayOrder) { if (!string.IsNullOrEmpty(hasDisplayOrder.DisplayOrder)) { @@ -648,7 +651,9 @@ namespace MediaBrowser.XbmcMetadata.Savers { var timespan = TimeSpan.FromTicks(runTimeTicks.Value); - writer.WriteElementString("runtime", Convert.ToInt64(timespan.TotalMinutes).ToString(UsCulture)); + writer.WriteElementString( + "runtime", + Convert.ToInt64(timespan.TotalMinutes).ToString(CultureInfo.InvariantCulture)); } if (!string.IsNullOrWhiteSpace(item.Tagline)) @@ -756,9 +761,9 @@ namespace MediaBrowser.XbmcMetadata.Savers try { var tagName = GetTagForProviderKey(providerKey); - //logger.LogDebug("Verifying custom provider tagname {0}", tagName); + Logger.LogDebug("Verifying custom provider tagname {0}", tagName); XmlConvert.VerifyName(tagName); - //logger.LogDebug("Saving custom provider tagname {0}", tagName); + Logger.LogDebug("Saving custom provider tagname {0}", tagName); writer.WriteElementString(GetTagForProviderKey(providerKey), providerId); } @@ -783,8 +788,7 @@ namespace MediaBrowser.XbmcMetadata.Savers AddActors(people, writer, libraryManager, fileSystem, config, options.SaveImagePathsInNfo); - var folder = item as BoxSet; - if (folder != null) + if (item is BoxSet folder) { AddCollectionItems(folder, writer); } @@ -866,29 +870,43 @@ namespace MediaBrowser.XbmcMetadata.Savers var userdata = userDataRepo.GetUserData(user, item); - writer.WriteElementString("isuserfavorite", userdata.IsFavorite.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + writer.WriteElementString( + "isuserfavorite", + userdata.IsFavorite.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); if (userdata.Rating.HasValue) { - writer.WriteElementString("userrating", userdata.Rating.Value.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + writer.WriteElementString( + "userrating", + userdata.Rating.Value.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); } if (!item.IsFolder) { - writer.WriteElementString("playcount", userdata.PlayCount.ToString(UsCulture)); - writer.WriteElementString("watched", userdata.Played.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + writer.WriteElementString( + "playcount", + userdata.PlayCount.ToString(CultureInfo.InvariantCulture)); + writer.WriteElementString( + "watched", + userdata.Played.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); if (userdata.LastPlayedDate.HasValue) { - writer.WriteElementString("lastplayed", userdata.LastPlayedDate.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss").ToLowerInvariant()); + writer.WriteElementString( + "lastplayed", + userdata.LastPlayedDate.Value.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss").ToLowerInvariant()); } writer.WriteStartElement("resume"); var runTimeTicks = item.RunTimeTicks ?? 0; - writer.WriteElementString("position", TimeSpan.FromTicks(userdata.PlaybackPositionTicks).TotalSeconds.ToString(UsCulture)); - writer.WriteElementString("total", TimeSpan.FromTicks(runTimeTicks).TotalSeconds.ToString(UsCulture)); + writer.WriteElementString( + "position", + TimeSpan.FromTicks(userdata.PlaybackPositionTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture)); + writer.WriteElementString( + "total", + TimeSpan.FromTicks(runTimeTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture)); } writer.WriteEndElement(); @@ -922,24 +940,21 @@ namespace MediaBrowser.XbmcMetadata.Savers if (person.SortOrder.HasValue) { - writer.WriteElementString("sortorder", person.SortOrder.Value.ToString(UsCulture)); + writer.WriteElementString( + "sortorder", + person.SortOrder.Value.ToString(CultureInfo.InvariantCulture)); } if (saveImagePath) { - try - { - var personEntity = libraryManager.GetPerson(person.Name); - var image = personEntity.GetImageInfo(ImageType.Primary, 0); + var personEntity = libraryManager.GetPerson(person.Name); + var image = personEntity.GetImageInfo(ImageType.Primary, 0); - if (image != null) - { - writer.WriteElementString("thumb", GetImagePathToSave(image, libraryManager, config)); - } - } - catch (Exception) + if (image != null) { - // Already logged in core + writer.WriteElementString( + "thumb", + GetImagePathToSave(image, libraryManager, config)); } } @@ -958,11 +973,10 @@ namespace MediaBrowser.XbmcMetadata.Savers } private bool IsPersonType(PersonInfo person, string type) - { - return string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase) || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase); - } + => string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase) + || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase); - private void AddCustomTags(string path, List<string> xmlTagsUsed, XmlWriter writer, ILogger logger, IFileSystem fileSystem) + private void AddCustomTags(string path, List<string> xmlTagsUsed, XmlWriter writer, ILogger logger) { var settings = new XmlReaderSettings() { @@ -982,7 +996,7 @@ namespace MediaBrowser.XbmcMetadata.Savers } catch (Exception ex) { - logger.LogError(ex, "Error reading existing xml tags from {path}.", path); + logger.LogError(ex, "Error reading existing xml tags from {Path}.", path); return; } @@ -995,7 +1009,8 @@ namespace MediaBrowser.XbmcMetadata.Savers { var name = reader.Name; - if (!CommonTags.ContainsKey(name) && !xmlTagsUsed.Contains(name, StringComparer.OrdinalIgnoreCase)) + if (!_commonTags.Contains(name) + && !xmlTagsUsed.Contains(name, StringComparer.OrdinalIgnoreCase)) { writer.WriteNode(reader, false); } @@ -1013,8 +1028,6 @@ namespace MediaBrowser.XbmcMetadata.Savers } private string GetTagForProviderKey(string providerKey) - { - return providerKey.ToLowerInvariant() + "id"; - } + => providerKey.ToLowerInvariant() + "id"; } } diff --git a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs index cf1b6468a..091c1957e 100644 --- a/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/EpisodeNfoSaver.cs @@ -14,43 +14,43 @@ namespace MediaBrowser.XbmcMetadata.Savers { public class EpisodeNfoSaver : BaseNfoSaver { - protected override string GetLocalSavePath(BaseItem item) + public EpisodeNfoSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger) + : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger) { - return Path.ChangeExtension(item.Path, ".nfo"); } + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + /// <inheritdoc /> + protected override string GetLocalSavePath(BaseItem item) + => Path.ChangeExtension(item.Path, ".nfo"); + + /// <inheritdoc /> protected override string GetRootElementName(BaseItem item) - { - return "episodedetails"; - } + => "episodedetails"; + /// <inheritdoc /> public override bool IsEnabledFor(BaseItem item, ItemUpdateType updateType) - { - if (!item.SupportsLocalMetadata) - { - return false; - } - - return item is Episode && updateType >= MinimumUpdateType; - } + => item.SupportsLocalMetadata && item is Episode && updateType >= MinimumUpdateType; + /// <inheritdoc /> protected override void WriteCustomElements(BaseItem item, XmlWriter writer) { var episode = (Episode)item; if (episode.IndexNumber.HasValue) { - writer.WriteElementString("episode", episode.IndexNumber.Value.ToString(UsCulture)); + writer.WriteElementString("episode", episode.IndexNumber.Value.ToString(_usCulture)); } if (episode.IndexNumberEnd.HasValue) { - writer.WriteElementString("episodenumberend", episode.IndexNumberEnd.Value.ToString(UsCulture)); + writer.WriteElementString("episodenumberend", episode.IndexNumberEnd.Value.ToString(_usCulture)); } if (episode.ParentIndexNumber.HasValue) { - writer.WriteElementString("season", episode.ParentIndexNumber.Value.ToString(UsCulture)); + writer.WriteElementString("season", episode.ParentIndexNumber.Value.ToString(_usCulture)); } if (episode.PremiereDate.HasValue) @@ -64,32 +64,33 @@ namespace MediaBrowser.XbmcMetadata.Savers { if (episode.AirsAfterSeasonNumber.HasValue && episode.AirsAfterSeasonNumber.Value != -1) { - writer.WriteElementString("airsafter_season", episode.AirsAfterSeasonNumber.Value.ToString(UsCulture)); + writer.WriteElementString("airsafter_season", episode.AirsAfterSeasonNumber.Value.ToString(_usCulture)); } + if (episode.AirsBeforeEpisodeNumber.HasValue && episode.AirsBeforeEpisodeNumber.Value != -1) { - writer.WriteElementString("airsbefore_episode", episode.AirsBeforeEpisodeNumber.Value.ToString(UsCulture)); + writer.WriteElementString("airsbefore_episode", episode.AirsBeforeEpisodeNumber.Value.ToString(_usCulture)); } + if (episode.AirsBeforeSeasonNumber.HasValue && episode.AirsBeforeSeasonNumber.Value != -1) { - writer.WriteElementString("airsbefore_season", episode.AirsBeforeSeasonNumber.Value.ToString(UsCulture)); + writer.WriteElementString("airsbefore_season", episode.AirsBeforeSeasonNumber.Value.ToString(_usCulture)); } if (episode.AirsBeforeEpisodeNumber.HasValue && episode.AirsBeforeEpisodeNumber.Value != -1) { - writer.WriteElementString("displayepisode", episode.AirsBeforeEpisodeNumber.Value.ToString(UsCulture)); + writer.WriteElementString("displayepisode", episode.AirsBeforeEpisodeNumber.Value.ToString(_usCulture)); } var specialSeason = episode.AiredSeasonNumber; if (specialSeason.HasValue && specialSeason.Value != -1) { - writer.WriteElementString("displayseason", specialSeason.Value.ToString(UsCulture)); + writer.WriteElementString("displayseason", specialSeason.Value.ToString(_usCulture)); } } } - private readonly CultureInfo UsCulture = new CultureInfo("en-US"); - + /// <inheritdoc /> protected override List<string> GetTagsUsed(BaseItem item) { var list = base.GetTagsUsed(item); @@ -105,12 +106,8 @@ namespace MediaBrowser.XbmcMetadata.Savers "displayseason", "displayepisode" }); - return list; - } - public EpisodeNfoSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger) - : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger) - { + return list; } } } diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs index 5e0eff029..08a752e33 100644 --- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using System.Xml; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -15,28 +16,29 @@ namespace MediaBrowser.XbmcMetadata.Savers { public class MovieNfoSaver : BaseNfoSaver { - protected override string GetLocalSavePath(BaseItem item) + public MovieNfoSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger) + : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger) { - var paths = GetMovieSavePaths(new ItemInfo(item), FileSystem); - return paths.Count == 0 ? null : paths[0]; } - public static List<string> GetMovieSavePaths(ItemInfo item, IFileSystem fileSystem) - { - var list = new List<string>(); + /// <inheritdoc /> + protected override string GetLocalSavePath(BaseItem item) + => GetMovieSavePaths(new ItemInfo(item)).FirstOrDefault(); + public static IEnumerable<string> GetMovieSavePaths(ItemInfo item) + { if (item.VideoType == VideoType.Dvd && !item.IsPlaceHolder) { var path = item.ContainingFolderPath; - list.Add(Path.Combine(path, "VIDEO_TS", "VIDEO_TS.nfo")); + yield return Path.Combine(path, "VIDEO_TS", "VIDEO_TS.nfo"); } if (!item.IsPlaceHolder && (item.VideoType == VideoType.Dvd || item.VideoType == VideoType.BluRay)) { var path = item.ContainingFolderPath; - list.Add(Path.Combine(path, Path.GetFileName(path) + ".nfo")); + yield return Path.Combine(path, Path.GetFileName(path) + ".nfo"); } else { @@ -47,22 +49,20 @@ namespace MediaBrowser.XbmcMetadata.Savers // list.Add(Path.Combine(item.ContainingFolderPath, "movie.nfo")); //} - list.Add(Path.ChangeExtension(item.Path, ".nfo")); + yield return Path.ChangeExtension(item.Path, ".nfo"); if (!item.IsInMixedFolder) { - list.Add(Path.Combine(item.ContainingFolderPath, "movie.nfo")); + yield return Path.Combine(item.ContainingFolderPath, "movie.nfo"); } } - - return list; } + /// <inheritdoc /> protected override string GetRootElementName(BaseItem item) - { - return item is MusicVideo ? "musicvideo" : "movie"; - } + => item is MusicVideo ? "musicvideo" : "movie"; + /// <inheritdoc /> public override bool IsEnabledFor(BaseItem item, ItemUpdateType updateType) { if (!item.SupportsLocalMetadata) @@ -70,10 +70,8 @@ namespace MediaBrowser.XbmcMetadata.Savers return false; } - var video = item as Video; - // Check parent for null to avoid running this against things like video backdrops - if (video != null && !(item is Episode) && !video.ExtraType.HasValue) + if (item is Video video && !(item is Episode) && !video.ExtraType.HasValue) { return updateType >= MinimumUpdateType; } @@ -81,6 +79,7 @@ namespace MediaBrowser.XbmcMetadata.Savers return false; } + /// <inheritdoc /> protected override void WriteCustomElements(BaseItem item, XmlWriter writer) { var imdb = item.GetProviderId(MetadataProviders.Imdb); @@ -90,9 +89,7 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteElementString("id", imdb); } - var musicVideo = item as MusicVideo; - - if (musicVideo != null) + if (item is MusicVideo musicVideo) { foreach (var artist in musicVideo.Artists) { @@ -104,9 +101,7 @@ namespace MediaBrowser.XbmcMetadata.Savers } } - var movie = item as Movie; - - if (movie != null) + if (item is Movie movie) { if (!string.IsNullOrEmpty(movie.CollectionName)) { @@ -115,6 +110,7 @@ namespace MediaBrowser.XbmcMetadata.Savers } } + /// <inheritdoc /> protected override List<string> GetTagsUsed(BaseItem item) { var list = base.GetTagsUsed(item); @@ -125,12 +121,8 @@ namespace MediaBrowser.XbmcMetadata.Savers "set", "id" }); - return list; - } - public MovieNfoSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger) - : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger) - { + return list; } } } diff --git a/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs index aa8d3e96c..25695121d 100644 --- a/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/SeasonNfoSaver.cs @@ -13,16 +13,26 @@ namespace MediaBrowser.XbmcMetadata.Savers { public class SeasonNfoSaver : BaseNfoSaver { - protected override string GetLocalSavePath(BaseItem item) + public SeasonNfoSaver( + IFileSystem fileSystem, + IServerConfigurationManager configurationManager, + ILibraryManager libraryManager, + IUserManager userManager, + IUserDataManager userDataManager, + ILogger logger) + : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger) { - return Path.Combine(item.Path, "season.nfo"); } + /// <inheritdoc /> + protected override string GetLocalSavePath(BaseItem item) + => Path.Combine(item.Path, "season.nfo"); + + /// <inheritdoc /> protected override string GetRootElementName(BaseItem item) - { - return "season"; - } + => "season"; + /// <inheritdoc /> public override bool IsEnabledFor(BaseItem item, ItemUpdateType updateType) { if (!item.SupportsLocalMetadata) @@ -38,6 +48,7 @@ namespace MediaBrowser.XbmcMetadata.Savers return updateType >= MinimumUpdateType || (updateType >= ItemUpdateType.MetadataImport && File.Exists(GetSavePath(item))); } + /// <inheritdoc /> protected override void WriteCustomElements(BaseItem item, XmlWriter writer) { var season = (Season)item; @@ -48,6 +59,7 @@ namespace MediaBrowser.XbmcMetadata.Savers } } + /// <inheritdoc /> protected override List<string> GetTagsUsed(BaseItem item) { var list = base.GetTagsUsed(item); @@ -58,16 +70,5 @@ namespace MediaBrowser.XbmcMetadata.Savers return list; } - - public SeasonNfoSaver( - IFileSystem fileSystem, - IServerConfigurationManager configurationManager, - ILibraryManager libraryManager, - IUserManager userManager, - IUserDataManager userDataManager, - ILogger logger) - : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger) - { - } } } diff --git a/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs index b0fc8c368..8d7faece7 100644 --- a/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Xml; using MediaBrowser.Controller.Configuration; @@ -13,26 +14,30 @@ namespace MediaBrowser.XbmcMetadata.Savers { public class SeriesNfoSaver : BaseNfoSaver { - protected override string GetLocalSavePath(BaseItem item) + public SeriesNfoSaver( + IFileSystem fileSystem, + IServerConfigurationManager configurationManager, + ILibraryManager libraryManager, + IUserManager userManager, + IUserDataManager userDataManager, + ILogger logger) + : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger) { - return Path.Combine(item.Path, "tvshow.nfo"); } + /// <inheritdoc /> + protected override string GetLocalSavePath(BaseItem item) + => Path.Combine(item.Path, "tvshow.nfo"); + + /// <inheritdoc /> protected override string GetRootElementName(BaseItem item) - { - return "tvshow"; - } + => "tvshow"; + /// <inheritdoc /> public override bool IsEnabledFor(BaseItem item, ItemUpdateType updateType) - { - if (!item.SupportsLocalMetadata) - { - return false; - } - - return item is Series && updateType >= MinimumUpdateType; - } + => item.SupportsLocalMetadata && item is Series && updateType >= MinimumUpdateType; + /// <inheritdoc /> protected override void WriteCustomElements(BaseItem item, XmlWriter writer) { var series = (Series)item; @@ -52,7 +57,12 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteStartElement("url"); writer.WriteAttributeString("cache", string.Format("{0}.xml", tvdb)); - writer.WriteString(string.Format("http://www.thetvdb.com/api/1D62F2F90030C444/series/{0}/all/{1}.zip", tvdb, language)); + writer.WriteString( + string.Format( + CultureInfo.InvariantCulture, + "http://www.thetvdb.com/api/1D62F2F90030C444/series/{0}/all/{1}.zip", + tvdb, + language)); writer.WriteEndElement(); writer.WriteEndElement(); @@ -67,6 +77,7 @@ namespace MediaBrowser.XbmcMetadata.Savers } } + /// <inheritdoc /> protected override List<string> GetTagsUsed(BaseItem item) { var list = base.GetTagsUsed(item); @@ -79,12 +90,8 @@ namespace MediaBrowser.XbmcMetadata.Savers "status", "displayorder" }); - return list; - } - public SeriesNfoSaver(IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, IUserManager userManager, IUserDataManager userDataManager, ILogger logger) - : base(fileSystem, configurationManager, libraryManager, userManager, userDataManager, logger) - { + return list; } } } diff --git a/MediaBrowser.sln b/MediaBrowser.sln index 3ed86d65c..dd4e9f8a6 100644 --- a/MediaBrowser.sln +++ b/MediaBrowser.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26730.3 @@ -41,8 +41,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Naming", "Emby.Naming\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.XmlTv", "Emby.XmlTv\Emby.XmlTv\Emby.XmlTv.csproj", "{6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IsoMounter", "Emby.IsoMounting\IsoMounter\IsoMounter.csproj", "{9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MediaBrowser.MediaEncoding", "MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj", "{960295EE-4AF4-4440-A525-B4C295B01A61}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Server", "Jellyfin.Server\Jellyfin.Server.csproj", "{07E39F42-A2C6-4B32-AF8C-725F957A73FF}" @@ -55,6 +53,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Drawing.Skia", "Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj", "{154872D9-6C12-4007-96E3-8F70A58386CE}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.Common.Tests", "tests\Jellyfin.Common.Tests\Jellyfin.Common.Tests.csproj", "{DF194677-DFD3-42AF-9F75-D44D5A416478}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.MediaEncoding.Tests", "tests\Jellyfin.MediaEncoding.Tests\Jellyfin.MediaEncoding.Tests.csproj", "{28464062-0939-4AA7-9F7B-24DDDA61A7C0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -141,10 +145,6 @@ Global {6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}.Debug|Any CPU.Build.0 = Debug|Any CPU {6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}.Release|Any CPU.ActiveCfg = Release|Any CPU {6EAFA7F0-8A82-49E6-B2FA-086C5CAEA95B}.Release|Any CPU.Build.0 = Release|Any CPU - {9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9BA471D2-6DB9-4DBF-B3A0-9FB3171F94A6}.Release|Any CPU.Build.0 = Release|Any CPU {960295EE-4AF4-4440-A525-B4C295B01A61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {960295EE-4AF4-4440-A525-B4C295B01A61}.Debug|Any CPU.Build.0 = Debug|Any CPU {960295EE-4AF4-4440-A525-B4C295B01A61}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -157,6 +157,14 @@ Global {154872D9-6C12-4007-96E3-8F70A58386CE}.Debug|Any CPU.Build.0 = Debug|Any CPU {154872D9-6C12-4007-96E3-8F70A58386CE}.Release|Any CPU.ActiveCfg = Release|Any CPU {154872D9-6C12-4007-96E3-8F70A58386CE}.Release|Any CPU.Build.0 = Release|Any CPU + {DF194677-DFD3-42AF-9F75-D44D5A416478}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF194677-DFD3-42AF-9F75-D44D5A416478}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF194677-DFD3-42AF-9F75-D44D5A416478}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF194677-DFD3-42AF-9F75-D44D5A416478}.Release|Any CPU.Build.0 = Release|Any CPU + {28464062-0939-4AA7-9F7B-24DDDA61A7C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28464062-0939-4AA7-9F7B-24DDDA61A7C0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28464062-0939-4AA7-9F7B-24DDDA61A7C0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28464062-0939-4AA7-9F7B-24DDDA61A7C0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -182,4 +190,8 @@ Global $0.DotNetNamingPolicy = $2 $2.DirectoryNamespaceAssociation = PrefixedHierarchical EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {DF194677-DFD3-42AF-9F75-D44D5A416478} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {28464062-0939-4AA7-9F7B-24DDDA61A7C0} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + EndGlobalSection EndGlobal diff --git a/Mono.Nat/Upnp/Messages/GetServicesMessage.cs b/Mono.Nat/Upnp/Messages/GetServicesMessage.cs index 72b4c2b25..f619f5ca4 100644 --- a/Mono.Nat/Upnp/Messages/GetServicesMessage.cs +++ b/Mono.Nat/Upnp/Messages/GetServicesMessage.cs @@ -25,50 +25,37 @@ // using System; -using System.Diagnostics; using System.Net; using MediaBrowser.Common.Net; -using Microsoft.Extensions.Logging; namespace Mono.Nat.Upnp { internal class GetServicesMessage : MessageBase { - private string servicesDescriptionUrl; - private EndPoint hostAddress; - private readonly ILogger _logger; + private string _servicesDescriptionUrl; + private EndPoint _hostAddress; - public GetServicesMessage(string description, EndPoint hostAddress, ILogger logger) + public GetServicesMessage(string description, EndPoint hostAddress) : base(null) { if (string.IsNullOrEmpty(description)) - _logger.LogWarning("Description is null"); - - if (hostAddress == null) - _logger.LogWarning("hostaddress is null"); - - this.servicesDescriptionUrl = description; - this.hostAddress = hostAddress; - _logger = logger; - } - - public override string Method - { - get { - return "GET"; + throw new ArgumentException("Description is null/empty", nameof(description)); } + + this._servicesDescriptionUrl = description; + this._hostAddress = hostAddress ?? throw new ArgumentNullException(nameof(hostAddress)); } + public override string Method => "GET"; + public override HttpRequestOptions Encode() { - var req = new HttpRequestOptions(); - - // The periodic request logging may keep some devices awake - req.LogRequestAsDebug = true; - req.LogErrors = false; + var req = new HttpRequestOptions() + { + Url = $"http://{this._hostAddress}{this._servicesDescriptionUrl}" + }; - req.Url = "http://" + this.hostAddress.ToString() + this.servicesDescriptionUrl; req.RequestHeaders.Add("ACCEPT-LANGUAGE", "en"); return req; diff --git a/Mono.Nat/Upnp/Messages/UpnpMessage.cs b/Mono.Nat/Upnp/Messages/UpnpMessage.cs index ade9df50b..d47241d4a 100644 --- a/Mono.Nat/Upnp/Messages/UpnpMessage.cs +++ b/Mono.Nat/Upnp/Messages/UpnpMessage.cs @@ -24,13 +24,8 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -using System; -using System.Diagnostics; using System.Xml; -using System.Net; -using System.IO; using System.Text; -using System.Globalization; using MediaBrowser.Common.Net; namespace Mono.Nat.Upnp @@ -46,38 +41,31 @@ namespace Mono.Nat.Upnp protected HttpRequestOptions CreateRequest(string upnpMethod, string methodParameters) { - string ss = "http://" + this.device.HostEndPoint.ToString() + this.device.ControlUrl; + var req = new HttpRequestOptions() + { + Url = $"http://{this.device.HostEndPoint}{this.device.ControlUrl}", + EnableKeepAlive = false, + RequestContentType = "text/xml", + RequestContent = "<s:Envelope " + + "xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" " + + "s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + + "<s:Body>" + + "<u:" + upnpMethod + " " + + "xmlns:u=\"" + device.ServiceType + "\">" + + methodParameters + + "</u:" + upnpMethod + ">" + + "</s:Body>" + + "</s:Envelope>\r\n\r\n" + }; - var req = new HttpRequestOptions(); - req.LogErrors = false; - - // The periodic request logging may keep some devices awake - req.LogRequestAsDebug = true; - - req.Url = ss; - req.EnableKeepAlive = false; - req.RequestContentType = "text/xml"; req.RequestHeaders.Add("SOAPACTION", "\"" + device.ServiceType + "#" + upnpMethod + "\""); - req.RequestContent = "<s:Envelope " - + "xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" " - + "s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" - + "<s:Body>" - + "<u:" + upnpMethod + " " - + "xmlns:u=\"" + device.ServiceType + "\">" - + methodParameters - + "</u:" + upnpMethod + ">" - + "</s:Body>" - + "</s:Envelope>\r\n\r\n"; return req; } public abstract HttpRequestOptions Encode(); - public virtual string Method - { - get { return "POST"; } - } + public virtual string Method => "POST"; protected void WriteFullElement(XmlWriter writer, string element, string value) { diff --git a/Mono.Nat/Upnp/UpnpNatDevice.cs b/Mono.Nat/Upnp/UpnpNatDevice.cs index fd408ee63..3ff1eeb90 100644 --- a/Mono.Nat/Upnp/UpnpNatDevice.cs +++ b/Mono.Nat/Upnp/UpnpNatDevice.cs @@ -27,11 +27,9 @@ // using System; -using System.IO; using System.Net; using System.Xml; using System.Text; -using System.Diagnostics; using System.Threading.Tasks; using MediaBrowser.Common.Net; using Microsoft.Extensions.Logging; @@ -96,7 +94,7 @@ namespace Mono.Nat.Upnp public async Task GetServicesList() { // Create a HTTPWebRequest to download the list of services the device offers - var message = new GetServicesMessage(this.serviceDescriptionUrl, this.hostEndPoint, _logger); + var message = new GetServicesMessage(this.serviceDescriptionUrl, this.hostEndPoint); using (var response = await _httpClient.SendAsync(message.Encode(), message.Method).ConfigureAwait(false)) { @@ -8,7 +8,7 @@ <br/><br/> <a href="https://github.com/jellyfin/jellyfin"><img alt="GPL 2.0 License" src="https://img.shields.io/github/license/jellyfin/jellyfin.svg"/></a> <a href="https://github.com/jellyfin/jellyfin/releases"><img alt="Current Release" src="https://img.shields.io/github/release/jellyfin/jellyfin.svg"/></a> -<a href="https://translate.jellyfin.org/engage/jellyfin/?utm_source=widget"><img alt="Translations" src="https://translate.jellyfin.org/widgets/jellyfin/-/svg-badge.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" /></a> <a href="https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=1"><img alt="Azure DevOps builds" src="https://dev.azure.com/jellyfin-project/jellyfin/_apis/build/status/Jellyfin%20CI"></a> <a href="https://hub.docker.com/r/jellyfin/jellyfin"><img alt="Docker Pull Count" src="https://img.shields.io/docker/pulls/jellyfin/jellyfin.svg"/></a> </br> @@ -23,19 +23,23 @@ Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media. It is an alternative to the proprietary Emby and Plex, to provide media from a dedicated server to end-user devices via multiple apps. Jellyfin is descended from Emby's 3.5.2 release and ported to the .NET Core framework to enable full cross-platform support. There are no strings attached, no premium licenses or features, and no hidden agendas: just a team who want to build something better and work together to achieve it. We welcome anyone who is interested in joining us in our quest! -For further details, please see [our documentation page](https://jellyfin.readthedocs.io). To receive the latest updates, get help with Jellyfin, and join the community, please visit [one of our communication channels on Matrix/Riot or social media](https://jellyfin.readthedocs.io/en/latest/getting-help/). +For further details, please see [our documentation page](https://docs.jellyfin.org/). To receive the latest updates, get help with Jellyfin, and join the community, please visit [one of our communication channels on Matrix/Riot or social media](https://docs.jellyfin.org/general/getting-help.html). -For more information about the project, please see our [about page](https://jellyfin.readthedocs.io/en/latest/about/). +For more information about the project, please see our [about page](https://docs.jellyfin.org/general/about.html). <p align="center"> <strong>Want to get started?</strong> -<em>Choose from <a href="https://jellyfin.readthedocs.io/en/latest/administrator-docs/installing/">Prebuilt Packages</a> or <a href="https://jellyfin.readthedocs.io/en/latest/administrator-docs/building/">Build from Source</a>, then see our <a href="https://jellyfin.readthedocs.io/en/latest/administrator-docs/quick-start/">quick start guide</a>.</em> +<em>Choose from <a href="https://docs.jellyfin.org/general/administration/installing.html">Prebuilt Packages</a> or <a href="https://docs.jellyfin.org/general/administration/building.html">Build from Source</a>, then see our <a href="https://docs.jellyfin.org/general/administration/quick-start.html">quick start guide</a>.</em> </p> <p align="center"> <strong>Want to contribute?</strong> -<em>Check out <a href="https://jellyfin.readthedocs.io/en/latest/contributor-docs/contributing/">our documentation for guidelines</a>.</em> +<em>Check out <a href="https://docs.jellyfin.org/general/contributing/index.html">our documentation for guidelines</a>.</em> </p> <p align="center"> -<strong>New idea or improvement? Something not working right?</strong> -<em>Open an <a href="https://jellyfin.readthedocs.io/en/latest/contributor-docs/issues/">Issue</a>.</em> +<strong>New idea or improvement?</strong> +<em>Check out our <a href="https://features.jellyfin.org/?view=most-wanted">feature request hub</a>.</em> +</p> +<p align="center"> +<strong>Something not working right?</strong> +<em>Open an <a href="https://docs.jellyfin.org/general/contributing/issues.html">Issue</a>.</em> </p> diff --git a/RSSDP/SsdpDevicePublisher.cs b/RSSDP/SsdpDevicePublisher.cs index 7f3e56394..53b740052 100644 --- a/RSSDP/SsdpDevicePublisher.cs +++ b/RSSDP/SsdpDevicePublisher.cs @@ -86,7 +86,6 @@ namespace Rssdp.Infrastructure ThrowIfDisposed(); - var minCacheTime = TimeSpan.Zero; bool wasAdded = false; lock (_Devices) { @@ -94,7 +93,6 @@ namespace Rssdp.Infrastructure { _Devices.Add(device); wasAdded = true; - minCacheTime = GetMinimumNonZeroCacheLifetime(); } } @@ -120,14 +118,12 @@ namespace Rssdp.Infrastructure if (device == null) throw new ArgumentNullException(nameof(device)); bool wasRemoved = false; - var minCacheTime = TimeSpan.Zero; lock (_Devices) { if (_Devices.Contains(device)) { _Devices.Remove(device); wasRemoved = true; - minCacheTime = GetMinimumNonZeroCacheLifetime(); } } diff --git a/SharedVersion.cs b/SharedVersion.cs index 500e6fe55..d741f379d 100644 --- a/SharedVersion.cs +++ b/SharedVersion.cs @@ -1,4 +1,4 @@ using System.Reflection; -[assembly: AssemblyVersion("10.3.7")] -[assembly: AssemblyFileVersion("10.3.7")] +[assembly: AssemblyVersion("10.5.0")] +[assembly: AssemblyFileVersion("10.5.0")] @@ -164,40 +164,6 @@ for target_platform in ${platform[@]}; do fi done -if [[ ${web_branch} != 'local' ]]; then - # Initialize submodules - git submodule update --init --recursive - - # configure branch - pushd MediaBrowser.WebDashboard/jellyfin-web - - if ! git diff-index --quiet HEAD --; then - popd - echo - echo "ERROR: Your 'jellyfin-web' submodule working directory is not clean!" - echo "This script will overwrite your unstaged and unpushed changes." - echo "Please do development on 'jellyfin-web' outside of the submodule." - exit 1 - fi - - git fetch --all - # If this is an official branch name, fetch it from origin - official_branches_regex="^master$|^dev$|^release-.*$|^hotfix-.*$" - if [[ ${web_branch} =~ ${official_branches_regex} ]]; then - git checkout origin/${web_branch} || { - echo "ERROR: 'jellyfin-web' branch 'origin/${web_branch}' is invalid." - exit 1 - } - # Otherwise, just check out the local branch (for testing, etc.) - else - git checkout ${web_branch} || { - echo "ERROR: 'jellyfin-web' branch '${web_branch}' is invalid." - exit 1 - } - fi - popd -fi - # Execute each platform and action in order, if said action is enabled pushd deployment/ for target_platform in ${platform[@]}; do @@ -214,7 +180,7 @@ for target_platform in ${platform[@]}; do for target_action in ${action[@]}; do echo -e ">> Processing action ${target_action}" if [[ -f ${target_action}.sh && -x ${target_action}.sh ]]; then - ./${target_action}.sh + ./${target_action}.sh web_branch=${web_branch} fi done if [[ -d pkg-dist/ ]]; then diff --git a/build.yaml b/build.yaml index 2d4bc29f4..123f77fb8 100644 --- a/build.yaml +++ b/build.yaml @@ -1,7 +1,7 @@ --- # We just wrap `build` so this is really it name: "jellyfin" -version: "10.3.7" +version: "10.5.0" packages: - debian-package-x64 - debian-package-armhf diff --git a/bump_version b/bump_version index 398caee15..106dd7a78 100755 --- a/bump_version +++ b/bump_version @@ -24,29 +24,6 @@ fi shared_version_file="./SharedVersion.cs" build_file="./build.yaml" -web_branch="$( git branch 2>/dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/' )" - -# Initialize submodules -git submodule update --init --recursive - -# configure branch -pushd MediaBrowser.WebDashboard/jellyfin-web - -if ! git diff-index --quiet HEAD --; then - popd - echo - echo "ERROR: Your 'jellyfin-web' submodule working directory is not clean!" - echo "This script will overwrite your unstaged and unpushed changes." - echo "Please do development on 'jellyfin-web' outside of the submodule." - exit 1 -fi - -git fetch --all -git checkout origin/${web_branch} -popd - -git add MediaBrowser.WebDashboard/jellyfin-web - new_version="$1" # Parse the version from the AssemblyVersion @@ -80,7 +57,7 @@ fi # Set the Dockerfile web version to the specified new_version old_version="$( grep "JELLYFIN_WEB_VERSION=" Dockerfile \ - | sed -E 's/ARG JELLYFIN_WEB_VERSION=([0-9\.]+[-a-z0-9]*)/\1/' + | sed -E 's/ARG JELLYFIN_WEB_VERSION=v([0-9\.]+[-a-z0-9]*)/\1/' )" echo $old_version diff --git a/deployment/README.md b/deployment/README.md index a00cd3e6c..a805f59ca 100644 --- a/deployment/README.md +++ b/deployment/README.md @@ -11,10 +11,8 @@ This directory contains the packaging configuration of Jellyfin for multiple pla ### Portable Builds (archives) -* `debian-x64`: Portable binary archive for Debian amd64 systems. -* `ubuntu-x64`: Portable binary archive for Ubuntu amd64 systems. * `linux-x64`: Portable binary archive for generic Linux amd64 systems. -* `osx-x64`: Portable binary archive for MacOS amd64 systems. +* `macos`: Portable binary archive for MacOS amd64 systems. * `win-x64`: Portable binary archive for Windows amd64 systems. * `win-x86`: Portable binary archive for Windows i386 systems. @@ -22,10 +20,10 @@ This directory contains the packaging configuration of Jellyfin for multiple pla These builds are not necessarily run from the `build` script, but are present for other platforms. -* `framework`: Compiled `.dll` for use with .NET Core runtime on any system. +* `portable`: Compiled `.dll` for use with .NET Core runtime on any system. * `docker`: Docker manifests for auto-publishing. * `unraid`: unRaid Docker template; not built by `build` but imported into unRaid directly. -* `win-generic`: Portable binary for generic Windows systems. +* `windows`: Support files and scripts for Windows CI build. ## Package Specification @@ -62,52 +60,3 @@ These builds are not necessarily run from the `build` script, but are present fo * Upon completion of the defined actions, at least one output file must be created in the `<platform>/pkg-dist` directory. * Output files will be moved to the directory `jellyfin-build/<platform>` one directory above the repository root upon completion. - -### Common Functions - -* A number of common functions are defined in `deployment/common.build.sh` for use by platform scripts. - -* Each action script should import the common functions to define a number of standard variables. - -* The common variables are: - - * `ROOT`: The Jellyfin repostiory root, usually `../..`. - * `CONFIG`: The .NET config, usually `Release`. - * `DOTNETRUNTIME`: The .NET `--runtime` value, platform-dependent. - * `OUTPUT_DIR`: The intermediate output dir, usually `./dist/jellyfin_${VERSION}`. - * `BUILD_CONTEXT`: The Docker build context, usually `../..`. - * `DOCKERFILE`: The Dockerfile, usually `Dockerfile` in the platform directory. - * `IMAGE_TAG`: A tag for the built Docker image. - * `PKG_DIR`: The final binary output directory for collection, invariably `pkg-dist`. - * `ARCHIVE_CMD`: The compression/archive command for release archives, usually `tar -xvzf` or `zip`. - -#### `get_version` - -Reads the version information from `SharedVersion.cs`. - -**Arguments:** `ROOT` - -#### `build_jellyfin` - -Build a standard self-contained binary in the current OS context. - -**Arguments:** `ROOT` `CONFIG` `DOTNETRUNTIME` `OUTPUT_DIR` - -#### `build_jellyfin_docker` - -Build a standard self-contained binary in a Docker image. - -**Arguments:** `BUILD_CONTEXT` `DOCKERFILE` `IMAGE_TAG` - -#### `clean_jellyfin` - -Clean up a build for housekeeping. - -**Arguments:** `ROOT` `CONFIG` `OUTPUT_DIR` `PKG_DIR` - -#### `package_portable` - -Produce a compressed archive. - -**Arguments:** `ROOT` `OUTPUT_DIR` `PKG_DIR` `ARCHIVE_CMD` - diff --git a/deployment/centos-package-x64/Dockerfile b/deployment/centos-package-x64/Dockerfile index 38853f173..855b0a479 100644 --- a/deployment/centos-package-x64/Dockerfile +++ b/deployment/centos-package-x64/Dockerfile @@ -8,13 +8,26 @@ ARG SDK_VERSION=2.2 ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist -# Prepare CentOS build environment +# Prepare CentOS environment RUN yum update -y \ - && yum install -y @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel \ - && rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm \ + && yum install -y epel-release + +# Install build dependencies +RUN yum install -y @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel wget git + +# Install recent NodeJS and Yarn +RUN wget -O- https://raw.githubusercontent.com/creationix/nvm/v0.35.0/install.sh | /bin/bash \ + && source "$HOME/.nvm/nvm.sh" \ + && nvm install v8 \ + && npm install -g yarn + +# Install DotNET SDK +RUN rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm \ && rpmdev-setuptree \ - && yum install -y dotnet-sdk-${SDK_VERSION} \ - && ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \ + && yum install -y dotnet-sdk-${SDK_VERSION} + +# Create symlinks and directories +RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \ && mkdir -p ${SOURCE_DIR}/SPECS \ && ln -s ${PLATFORM_DIR}/pkg-src/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \ && mkdir -p ${SOURCE_DIR}/SOURCES \ diff --git a/deployment/centos-package-x64/clean.sh b/deployment/centos-package-x64/clean.sh index 7278372e1..31455de0d 100755 --- a/deployment/centos-package-x64/clean.sh +++ b/deployment/centos-package-x64/clean.sh @@ -1,7 +1,5 @@ #!/usr/bin/env bash -source ../common.build.sh - keep_artifacts="${1}" WORKDIR="$( pwd )" diff --git a/deployment/centos-package-x64/docker-build.sh b/deployment/centos-package-x64/docker-build.sh index cefb1652e..18e10661c 100755 --- a/deployment/centos-package-x64/docker-build.sh +++ b/deployment/centos-package-x64/docker-build.sh @@ -8,7 +8,71 @@ set -o xtrace # Move to source directory pushd ${SOURCE_DIR} -ls -al SOURCES/pkg-src/ +VERSION="$( grep '^Version:' ${SOURCE_DIR}/SOURCES/pkg-src/jellyfin.spec | awk '{ print $NF }' )" + +# Clone down and build Web frontend +web_build_dir="$( mktemp -d )" +web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web" +git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/ +pushd ${web_build_dir} +if [[ -n ${web_branch} ]]; then + checkout -b origin/${web_branch} +fi +source "$HOME/.nvm/nvm.sh" +nvm use v8 +yarn install +mkdir -p ${web_target} +mv dist/* ${web_target}/ +popd +rm -rf ${web_build_dir} + +# Create RPM source archive +GNU_TAR=1 +echo "Bundling all sources for RPM build." +tar \ +--transform "s,^\.,jellyfin-${VERSION}," \ +--exclude='.git*' \ +--exclude='**/.git' \ +--exclude='**/.hg' \ +--exclude='**/.vs' \ +--exclude='**/.vscode' \ +--exclude='deployment' \ +--exclude='**/bin' \ +--exclude='**/obj' \ +--exclude='**/.nuget' \ +--exclude='*.deb' \ +--exclude='*.rpm' \ +-czf "${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-${VERSION}.tar.gz" \ +-C ${SOURCE_DIR} ./ || GNU_TAR=0 + +if [ $GNU_TAR -eq 0 ]; then + echo "The installed tar binary did not support --transform. Using workaround." + package_temporary_dir="$( mktemp -d )" + mkdir -p "${package_temporary_dir}/jellyfin" + # Not GNU tar + tar \ + --exclude='.git*' \ + --exclude='**/.git' \ + --exclude='**/.hg' \ + --exclude='**/.vs' \ + --exclude='**/.vscode' \ + --exclude='deployment' \ + --exclude='**/bin' \ + --exclude='**/obj' \ + --exclude='**/.nuget' \ + --exclude='*.deb' \ + --exclude='*.rpm' \ + -czf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" \ + -C ${SOURCE_DIR} ./ + echo "Extracting filtered package." + mkdir -p "${package_temporary_dir}/jellyfin-${VERSION}" + tar -xzf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}/jellyfin-${VERSION}" + echo "Removing filtered package." + rm -f "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" + echo "Repackaging package into final tarball." + tar -czf "${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}" "jellyfin-${VERSION}" + rm -rf ${package_temporary_dir} +fi # Build RPM spectool -g -R SPECS/jellyfin.spec diff --git a/deployment/centos-package-x64/package.sh b/deployment/centos-package-x64/package.sh index df5a66580..1b983f49d 100755 --- a/deployment/centos-package-x64/package.sh +++ b/deployment/centos-package-x64/package.sh @@ -1,13 +1,15 @@ #!/usr/bin/env bash -source ../common.build.sh +args="${@}" +declare -a docker_envvars +for arg in ${args}; do + docker_envvars+=("-e ${arg}") +done WORKDIR="$( pwd )" -VERSION="$( grep '^Version:' ${WORKDIR}/pkg-src/jellyfin.spec | awk '{ print $NF }' )" package_temporary_dir="${WORKDIR}/pkg-dist-tmp" output_dir="${WORKDIR}/pkg-dist" -pkg_src_dir="${WORKDIR}/pkg-src" current_user="$( whoami )" image_name="jellyfin-centos-build" @@ -21,57 +23,12 @@ else docker_sudo="" fi -# Create RPM source archive -GNU_TAR=1 +# Prepare temporary package dir mkdir -p "${package_temporary_dir}" -echo "Bundling all sources for RPM build." -tar \ ---transform "s,^\.,jellyfin-${VERSION}," \ ---exclude='.git*' \ ---exclude='**/.git' \ ---exclude='**/.hg' \ ---exclude='**/.vs' \ ---exclude='**/.vscode' \ ---exclude='deployment' \ ---exclude='**/bin' \ ---exclude='**/obj' \ ---exclude='**/.nuget' \ ---exclude='*.deb' \ ---exclude='*.rpm' \ --czf "${pkg_src_dir}/jellyfin-${VERSION}.tar.gz" \ --C "../.." ./ || GNU_TAR=0 - -if [ $GNU_TAR -eq 0 ]; then - echo "The installed tar binary did not support --transform. Using workaround." - mkdir -p "${package_temporary_dir}/jellyfin" - # Not GNU tar - tar \ - --exclude='.git*' \ - --exclude='**/.git' \ - --exclude='**/.hg' \ - --exclude='**/.vs' \ - --exclude='**/.vscode' \ - --exclude='deployment' \ - --exclude='**/bin' \ - --exclude='**/obj' \ - --exclude='**/.nuget' \ - --exclude='*.deb' \ - --exclude='*.rpm' \ - -zcf \ - "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" \ - -C "../.." ./ - echo "Extracting filtered package." - tar -xzf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}/jellyfin-${VERSION}" - echo "Removing filtered package." - rm -f "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" - echo "Repackaging package into final tarball." - tar -czf "${pkg_src_dir}/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}" "jellyfin-${VERSION}" -fi - # Set up the build environment Docker image ${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile # Build the RPMs and copy out to ${package_temporary_dir} -${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" +${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} # Move the RPMs to the output directory mkdir -p "${output_dir}" mv "${package_temporary_dir}"/rpm/* "${output_dir}" diff --git a/deployment/common.build.sh b/deployment/common.build.sh deleted file mode 100755 index 000872ea9..000000000 --- a/deployment/common.build.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env bash - -set -o errexit -set -o nounset - -RED='\033[0;31m' -GREEN='\033[0;32m' -CYAN='\033[0;36m' -NC='\033[0m' # No Color - -DEFAULT_BUILD_CONTEXT="../.." -DEFAULT_ROOT="." -DEFAULT_DOTNETRUNTIME="framework" -DEFAULT_CONFIG="Release" -DEFAULT_OUTPUT_DIR="dist/jellyfin-git" -DEFAULT_PKG_DIR="pkg-dist" -DEFAULT_DOCKERFILE="Dockerfile" -DEFAULT_ARCHIVE_CMD="tar -xvzf" - -# Parse the version from the build.yaml version -get_version() -( - local ROOT=${1-$DEFAULT_ROOT} - grep "version:" ${ROOT}/build.yaml \ - | sed -E 's/version: "([0-9\.]+.*)"/\1/' -) - -# Run a build -build_jellyfin() -( - ROOT=${1-$DEFAULT_ROOT} - CONFIG=${2-$DEFAULT_CONFIG} - DOTNETRUNTIME=${3-$DEFAULT_DOTNETRUNTIME} - OUTPUT_DIR=${4-$DEFAULT_OUTPUT_DIR} - - echo -e "${CYAN}Building jellyfin in '${ROOT}' for ${DOTNETRUNTIME} with configuration ${CONFIG} and output directory '${OUTPUT_DIR}'.${NC}" - if [[ $DOTNETRUNTIME == 'framework' ]]; then - dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}" "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" - else - dotnet publish "${ROOT}" --configuration "${CONFIG}" --output="${OUTPUT_DIR}" --self-contained --runtime ${DOTNETRUNTIME} "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" - fi - EXIT_CODE=$? - if [ $EXIT_CODE -eq 0 ]; then - echo -e "${GREEN}[DONE] Build jellyfin in '${ROOT}' for ${DOTNETRUNTIME} with configuration ${CONFIG} and output directory '${OUTPUT_DIR}' complete.${NC}" - else - echo -e "${RED}[FAIL] Build jellyfin in '${ROOT}' for ${DOTNETRUNTIME} with configuration ${CONFIG} and output directory '${OUTPUT_DIR}' FAILED.${NC}" - fi -) - -# Run a docker -build_jellyfin_docker() -( - BUILD_CONTEXT=${1-$DEFAULT_BUILD_CONTEXT} - DOCKERFILE=${2-$DEFAULT_DOCKERFILE} - IMAGE_TAG=${3-"jellyfin:$(git rev-parse --abbrev-ref HEAD)"} - - echo -e "${CYAN}Building jellyfin docker image in '${BUILD_CONTEXT}' with Dockerfile '${DOCKERFILE}' and tag '${IMAGE_TAG}'.${NC}" - docker build -t ${IMAGE_TAG} -f ${DOCKERFILE} ${BUILD_CONTEXT} - EXIT_CODE=$? - if [ $EXIT_CODE -eq 0 ]; then - echo -e "${GREEN}[DONE] Building jellyfin docker image in '${BUILD_CONTEXT}' with Dockerfile '${DOCKERFILE}' and tag '${IMAGE_TAG}' complete.${NC}" - else - echo -e "${RED}[FAIL] Building jellyfin docker image in '${BUILD_CONTEXT}' with Dockerfile '${DOCKERFILE}' and tag '${IMAGE_TAG}' FAILED.${NC}" - fi -) - -# Clean a build -clean_jellyfin() -( - local ROOT=${1-$DEFAULT_ROOT} - local CONFIG=${2-$DEFAULT_CONFIG} - local OUTPUT_DIR=${3-$DEFAULT_OUTPUT_DIR} - local PKG_DIR=${4-$DEFAULT_PKG_DIR} - echo -e "${CYAN}Cleaning jellyfin in '${ROOT}'' with configuration ${CONFIG} and output directory '${OUTPUT_DIR}'.${NC}" - echo -e "${CYAN}Deleting '${OUTPUT_DIR}'${NC}" - rm -rf "$OUTPUT_DIR" - echo -e "${CYAN}Deleting '${PKG_DIR}'${NC}" - rm -rf "$PKG_DIR" - dotnet clean "${ROOT}" -maxcpucount:1 --configuration ${CONFIG} - local EXIT_CODE=$? - if [ $EXIT_CODE -eq 0 ]; then - echo -e "${GREEN}[DONE] Clean jellyfin in '${ROOT}' with configuration ${CONFIG} and output directory '${OUTPUT_DIR}' complete.${NC}" - else - echo -e "${RED}[FAIL] Clean jellyfin in '${ROOT}' with configuration ${CONFIG} and output directory '${OUTPUT_DIR}' failed.${NC}" - fi -) - -# Packages the output folder into an archive. -package_portable() -( - local ROOT=${1-$DEFAULT_ROOT} - local OUTPUT_DIR=${2-$DEFAULT_OUTPUT_DIR} - local PKG_DIR=${3-$DEFAULT_PKG_DIR} - local ARCHIVE_CMD=${4-$DEFAULT_ARCHIVE_CMD} - # Package portable build result - if [ -d ${OUTPUT_DIR} ]; then - echo -e "${CYAN}Packaging build in '${OUTPUT_DIR}' for `basename "${OUTPUT_DIR}"` to '${PKG_DIR}' with root '${ROOT}'.${NC}" - mkdir -p ${PKG_DIR} - tar -zcvf "${PKG_DIR}/`basename "${OUTPUT_DIR}"`.portable.tar.gz" -C "`dirname "${OUTPUT_DIR}"`" "`basename "${OUTPUT_DIR}"`" - local EXIT_CODE=$? - if [ $EXIT_CODE -eq 0 ]; then - echo -e "${GREEN}[DONE] Packaging build in '${OUTPUT_DIR}' for `basename "${OUTPUT_DIR}"` to '${PKG_DIR}' with root '${ROOT}' complete.${NC}" - else - echo -e "${RED}[FAIL] Packaging build in '${OUTPUT_DIR}' for `basename "${OUTPUT_DIR}"` to '${PKG_DIR}' with root '${ROOT}' FAILED.${NC}" - fi - else - echo -e "${RED}[FAIL] Build artifacts do not exist for ${OUTPUT_DIR}. Run build.sh first.${NC}" - fi -) - diff --git a/deployment/debian-package-arm64/Dockerfile.amd64 b/deployment/debian-package-arm64/Dockerfile.amd64 index 2cb8bd856..5644c1470 100644 --- a/deployment/debian-package-arm64/Dockerfile.amd64 +++ b/deployment/debian-package-arm64/Dockerfile.amd64 @@ -1,4 +1,4 @@ -FROM debian:9 +FROM debian:10 # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-arm64 @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/69937b49-a877-4ced-81e6-286620b390ab/8ab938cf6f5e83b2221630354160ef21/dotnet-sdk-2.2.104-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/228832ea-805f-45ab-8c88-fa36165701b9/16ce29a06031eeb09058dee94d6f5330/dotnet-sdk-2.2.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet @@ -25,9 +25,15 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/69937b49-a877-4 RUN dpkg --add-architecture arm64 \ && apt-get update \ && apt-get install -y cross-gcc-dev \ - && TARGET_LIST="arm64" cross-gcc-gensource 6 \ - && cd cross-gcc-packages-amd64/cross-gcc-6-arm64 \ - && apt-get install -y gcc-6-source libstdc++6-arm64-cross binutils-aarch64-linux-gnu bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 libfreetype6-dev:arm64 liblttng-ust0:arm64 libstdc++6:arm64 + && TARGET_LIST="arm64" cross-gcc-gensource 8 \ + && cd cross-gcc-packages-amd64/cross-gcc-8-arm64 \ + && apt-get install -y gcc-8-source libstdc++-8-dev-arm64-cross binutils-aarch64-linux-gnu bison flex libtool gdb sharutils netbase libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 libfreetype6-dev:arm64 libssl-dev:arm64 liblttng-ust0:arm64 libstdc++-8-dev:arm64 + +# Install yarn package manager +RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ + && apt update \ + && apt install -y yarn # Link to docker-build script RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh diff --git a/deployment/debian-package-arm64/Dockerfile.arm64 b/deployment/debian-package-arm64/Dockerfile.arm64 index bb9e28d0a..438436766 100644 --- a/deployment/debian-package-arm64/Dockerfile.arm64 +++ b/deployment/debian-package-arm64/Dockerfile.arm64 @@ -1,4 +1,4 @@ -FROM debian:9 +FROM debian:10 # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-arm64 @@ -12,11 +12,11 @@ ENV ARCH=arm64 # Prepare Debian build environment RUN apt-get update \ - && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0 + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev liblttng-ust0 # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/d9f37b73-df8d-4dfa-a905-b7648d3401d0/6312573ac13d7a8ddc16e4058f7d7dc5/dotnet-sdk-2.2.104-linux-arm.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/1560f31a-d566-4de0-9fef-1a40b2b2a748/163f23fb8018e064034f3492f54358f1/dotnet-sdk-2.2.401-linux-arm64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/debian-package-arm64/clean.sh b/deployment/debian-package-arm64/clean.sh index ac143c3d0..e7bfdf8b4 100755 --- a/deployment/debian-package-arm64/clean.sh +++ b/deployment/debian-package-arm64/clean.sh @@ -1,7 +1,5 @@ #!/usr/bin/env bash -source ../common.build.sh - keep_artifacts="${1}" WORKDIR="$( pwd )" diff --git a/deployment/debian-package-arm64/docker-build.sh b/deployment/debian-package-arm64/docker-build.sh index 1c75ece8e..7a13bafcb 100755 --- a/deployment/debian-package-arm64/docker-build.sh +++ b/deployment/debian-package-arm64/docker-build.sh @@ -11,6 +11,20 @@ pushd ${SOURCE_DIR} # Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image sed -i '/dotnet-sdk-2.2,/d' debian/control +# Clone down and build Web frontend +web_build_dir="$( mktemp -d )" +web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web" +git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/ +pushd ${web_build_dir} +if [[ -n ${web_branch} ]]; then + checkout -b origin/${web_branch} +fi +yarn install +mkdir -p ${web_target} +mv dist/* ${web_target}/ +popd +rm -rf ${web_build_dir} + # Build DEB export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH} dpkg-buildpackage -us -uc -aarm64 diff --git a/deployment/debian-package-arm64/package.sh b/deployment/debian-package-arm64/package.sh index ce02b1af5..209198218 100755 --- a/deployment/debian-package-arm64/package.sh +++ b/deployment/debian-package-arm64/package.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash -source ../common.build.sh +args="${@}" +declare -a docker_envvars +for arg in ${args}; do + docker_envvars+=("-e ${arg}") +done ARCH="$( arch )" WORKDIR="$( pwd )" @@ -35,7 +39,7 @@ mkdir -p "${package_temporary_dir}" # Set up the build environment Docker image ${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE} # Build the DEBs and copy out to ${package_temporary_dir} -${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" +${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} # Move the DEBs to the output directory mkdir -p "${output_dir}" mv "${package_temporary_dir}"/deb/* "${output_dir}" diff --git a/deployment/debian-package-armhf/Dockerfile.amd64 b/deployment/debian-package-armhf/Dockerfile.amd64 index 0d62352e0..b05f10def 100644 --- a/deployment/debian-package-armhf/Dockerfile.amd64 +++ b/deployment/debian-package-armhf/Dockerfile.amd64 @@ -1,4 +1,4 @@ -FROM debian:9 +FROM debian:10 # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-armhf @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/69937b49-a877-4ced-81e6-286620b390ab/8ab938cf6f5e83b2221630354160ef21/dotnet-sdk-2.2.104-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/228832ea-805f-45ab-8c88-fa36165701b9/16ce29a06031eeb09058dee94d6f5330/dotnet-sdk-2.2.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet @@ -25,9 +25,15 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/69937b49-a877-4 RUN dpkg --add-architecture armhf \ && apt-get update \ && apt-get install -y cross-gcc-dev \ - && TARGET_LIST="armhf" cross-gcc-gensource 6 \ - && cd cross-gcc-packages-amd64/cross-gcc-6-armhf \ - && apt-get install -y gcc-6-source libstdc++6-armhf-cross binutils-arm-linux-gnueabihf bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:armhf linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf libfontconfig1-dev:armhf libfreetype6-dev:armhf liblttng-ust0:armhf libstdc++6:armhf + && TARGET_LIST="armhf" cross-gcc-gensource 8 \ + && cd cross-gcc-packages-amd64/cross-gcc-8-armhf \ + && apt-get install -y gcc-8-source libstdc++-8-dev-armhf-cross binutils-aarch64-linux-gnu bison flex libtool gdb sharutils netbase libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip binutils-arm-linux-gnueabihf libc6-dev:armhf linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf libfontconfig1-dev:armhf libfreetype6-dev:armhf libssl-dev:armhf liblttng-ust0:armhf libstdc++-8-dev:armhf + +# Install yarn package manager +RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ + && apt update \ + && apt install -y yarn # Link to docker-build script RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh diff --git a/deployment/debian-package-armhf/Dockerfile.armhf b/deployment/debian-package-armhf/Dockerfile.armhf index eb4152116..6729d8f38 100644 --- a/deployment/debian-package-armhf/Dockerfile.armhf +++ b/deployment/debian-package-armhf/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM debian:9 +FROM debian:10 # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-armhf @@ -12,15 +12,21 @@ ENV ARCH=armhf # Prepare Debian build environment RUN apt-get update \ - && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0 + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev liblttng-ust0 # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/d9f37b73-df8d-4dfa-a905-b7648d3401d0/6312573ac13d7a8ddc16e4058f7d7dc5/dotnet-sdk-2.2.104-linux-arm.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/3cb1d917-19cc-4399-9a53-03bb5de223f6/be3e011601610d9fe0a4f6b1962378ea/dotnet-sdk-2.2.401-linux-arm.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet +# Install yarn package manager +RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ + && apt update \ + && apt install -y yarn + # Link to docker-build script RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh diff --git a/deployment/debian-package-armhf/clean.sh b/deployment/debian-package-armhf/clean.sh index 3898110af..35a3d3e9a 100755 --- a/deployment/debian-package-armhf/clean.sh +++ b/deployment/debian-package-armhf/clean.sh @@ -1,7 +1,5 @@ #!/usr/bin/env bash -source ../common.build.sh - keep_artifacts="${1}" WORKDIR="$( pwd )" diff --git a/deployment/debian-package-armhf/docker-build.sh b/deployment/debian-package-armhf/docker-build.sh index df35345bd..c48ccb3fb 100755 --- a/deployment/debian-package-armhf/docker-build.sh +++ b/deployment/debian-package-armhf/docker-build.sh @@ -11,6 +11,20 @@ pushd ${SOURCE_DIR} # Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image sed -i '/dotnet-sdk-2.2,/d' debian/control +# Clone down and build Web frontend +web_build_dir="$( mktemp -d )" +web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web" +git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/ +pushd ${web_build_dir} +if [[ -n ${web_branch} ]]; then + checkout -b origin/${web_branch} +fi +yarn install +mkdir -p ${web_target} +mv dist/* ${web_target}/ +popd +rm -rf ${web_build_dir} + # Build DEB export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH} dpkg-buildpackage -us -uc -aarmhf diff --git a/deployment/debian-package-armhf/package.sh b/deployment/debian-package-armhf/package.sh index 4393fb834..4a27dd828 100755 --- a/deployment/debian-package-armhf/package.sh +++ b/deployment/debian-package-armhf/package.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash -source ../common.build.sh +args="${@}" +declare -a docker_envvars +for arg in ${args}; do + docker_envvars+=("-e ${arg}") +done ARCH="$( arch )" WORKDIR="$( pwd )" @@ -35,7 +39,7 @@ mkdir -p "${package_temporary_dir}" # Set up the build environment Docker image ${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE} # Build the DEBs and copy out to ${package_temporary_dir} -${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" +${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} # Move the DEBs to the output directory mkdir -p "${output_dir}" mv "${package_temporary_dir}"/deb/* "${output_dir}" diff --git a/deployment/debian-package-x64/Dockerfile b/deployment/debian-package-x64/Dockerfile index 9819cc20d..2f97d3944 100644 --- a/deployment/debian-package-x64/Dockerfile +++ b/deployment/debian-package-x64/Dockerfile @@ -1,18 +1,37 @@ -FROM microsoft/dotnet:2.2-sdk-stretch +FROM debian:10 # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG PLATFORM_DIR=/jellyfin/deployment/debian-package-x64 ARG ARTIFACT_DIR=/dist +ARG SDK_VERSION=2.2 # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist ENV DEB_BUILD_OPTIONS=noddebs +ENV ARCH=amd64 # Prepare Debian build environment RUN apt-get update \ - && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev \ - && ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \ - && mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 + +# Install dotnet repository +# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current +RUN wget https://download.visualstudio.microsoft.com/download/pr/228832ea-805f-45ab-8c88-fa36165701b9/16ce29a06031eeb09058dee94d6f5330/dotnet-sdk-2.2.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ + && mkdir -p dotnet-sdk \ + && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ + && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet + +# Install yarn package manager +RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ + && apt update \ + && apt install -y yarn + +# Link to docker-build script +RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh + +# Link to Debian source dir; mkdir needed or it fails, can't force dest +RUN mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian VOLUME ${ARTIFACT_DIR}/ diff --git a/deployment/debian-package-x64/clean.sh b/deployment/debian-package-x64/clean.sh index b2960fcb3..4e507bcb2 100755 --- a/deployment/debian-package-x64/clean.sh +++ b/deployment/debian-package-x64/clean.sh @@ -1,7 +1,5 @@ #!/usr/bin/env bash -source ../common.build.sh - keep_artifacts="${1}" WORKDIR="$( pwd )" diff --git a/deployment/debian-package-x64/docker-build.sh b/deployment/debian-package-x64/docker-build.sh index 9781879f6..97bc45a06 100755 --- a/deployment/debian-package-x64/docker-build.sh +++ b/deployment/debian-package-x64/docker-build.sh @@ -11,6 +11,20 @@ pushd ${SOURCE_DIR} # Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image sed -i '/dotnet-sdk-2.2,/d' debian/control +# Clone down and build Web frontend +web_build_dir="$( mktemp -d )" +web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web" +git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/ +pushd ${web_build_dir} +if [[ -n ${web_branch} ]]; then + checkout -b origin/${web_branch} +fi +yarn install +mkdir -p ${web_target} +mv dist/* ${web_target}/ +popd +rm -rf ${web_build_dir} + # Build DEB dpkg-buildpackage -us -uc diff --git a/deployment/debian-package-x64/package.sh b/deployment/debian-package-x64/package.sh index 2530e253b..5a416959a 100755 --- a/deployment/debian-package-x64/package.sh +++ b/deployment/debian-package-x64/package.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash -source ../common.build.sh +args="${@}" +declare -a docker_envvars +for arg in ${args}; do + docker_envvars+=("-e ${arg}") +done WORKDIR="$( pwd )" @@ -24,7 +28,7 @@ mkdir -p "${package_temporary_dir}" # Set up the build environment Docker image ${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile # Build the DEBs and copy out to ${package_temporary_dir} -${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" +${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} # Move the DEBs to the output directory mkdir -p "${output_dir}" mv "${package_temporary_dir}"/deb/* "${output_dir}" diff --git a/deployment/debian-package-x64/pkg-src/changelog b/deployment/debian-package-x64/pkg-src/changelog index aa15827a7..51c482237 100644 --- a/deployment/debian-package-x64/pkg-src/changelog +++ b/deployment/debian-package-x64/pkg-src/changelog @@ -1,3 +1,15 @@ +jellyfin (10.5.0-1) unstable; urgency=medium + + * New upstream version 10.5.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.5.0 + + -- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 11 Oct 2019 20:12:38 -0400 + +jellyfin (10.4.0-1) unstable; urgency=medium + + * New upstream version 10.4.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.4.0 + + -- Jellyfin Packaging Team <packaging@jellyfin.org> Sat, 31 Aug 2019 21:38:56 -0400 + jellyfin (10.3.7-1) unstable; urgency=medium * New upstream version 10.3.7; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.7 @@ -45,297 +57,3 @@ jellyfin (10.3.0-1) unstable; urgency=medium * New upstream version 10.3.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0 -- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 19 Apr 2019 14:24:29 -0400 - -jellyfin (10.2.2-1) unstable; urgency=medium - - * jellyfin: - * PR968 Release 10.2.z copr autobuild - * PR964 Install the dotnet runtime package in Fedora build - * PR979 Build Package releases without debug turned on - * PR990 Fix slow local image validation - * PR991 Fix the ffmpeg compatibility - * PR992 Add Debian armhf (Raspberry Pi) build plus crossbuild - * PR998 Set EnableRaisingEvents to true for processes that require it - * PR1017 Set ffmpeg+ffprobe paths in Docker container - * jellyfin-web: - * PR152 Go back on Media stop - * PR156 Fix volume slider not working on nowplayingbar - - -- Jellyfin Packaging Team <packaging@jellyfin.org> Thu, 28 Feb 2019 15:32:16 -0500 - -jellyfin (10.2.1-1) unstable; urgency=medium - - * jellyfin: - * PR920 Fix cachedir missing from Docker container - * PR924 Use the movie name instead of folder name - * PR933 Semi-revert to prefer old movie grouping behaviour - * PR948 Revert movie matching (supercedes PR933, PR924, PR739) - * PR960 Use jellyfin/ffmpeg image - * jellyfin-web: - * PR136 Re-add OpenSubtitles configuration page - * PR137 Replace HeaderEmbyServer with HeaderJellyfinServer on plugincatalog - * PR138 Remove left-over JS for Customize Home Screen - * PR141 Exit fullscreen automatically after video playback ends - - -- Jellyfin Packaging Team <packaging@jellyfin.org> Wed, 20 Feb 2019 11:36:16 -0500 - -jellyfin (10.2.0-2) unstable; urgency=medium - - * jellyfin: - * PR452 Use EF Core for Activity database - * PR535 Clean up streambuilder - * PR655 Support trying local branches in submodule - * PR656 Do some logging in MediaInfoService - * PR657 Remove conditions that are always true/false - * PR661 Fix NullRef from progress report - * PR663 Use TagLibSharp Nuget package - * PR664 Revert "Fix segment_time_delta for ffmpeg 4.1" - * PR666 Add cross-platform build for arm64 - * PR668 Return Audio objects from MusicAlbum.Tracks - * PR671 Set EnableRaisingEvents correctly - * PR672 Remove unconditional caching, modified since header and use ETags - * PR677 Fix arm32 Docker - * PR681 Fix Windows build script errors + pin ffmpeg to 4.0 - * PR686 Disable some StyleCop warnings - * PR687 Fix some analyzer warnings - * PR689 Fix RPM package build for fedora - * PR702 Fix debug build on windows - * PR706 Make another docker layer reusable - * PR709 Fix always null expressions - * PR710 Fix a spelling mistake - * PR711 Remove remnants of system events - * PR713 Fix empty statement in DidlBuilder.cs - * PR716 Remove more compile time warnings - * PR721 Change image dimentions from double to int - * PR723 Minor improvements to db code - * PR724 Move Skia back into it's own project - * PR726 Clean up IFileSystem wrappers around stdlib. - * PR727 Change default aspect ratio to 2/3 from 0 - * PR728 Use ffmpeg from jrottenberg/ffmpeg - * PR732 Reworked LocalizationManager to load data async - * PR733 Remove unused function - * PR734 Fix more analyzer warnings - * PR736 Start startup tasks async - * PR737 Add AssemblyInfo for Jellyfin.Drawing.Skia - * PR739 Change multi version logic for movies - * PR740 Remove code for pre-installed plugins & properly check if file exists - * PR756 Make cache dir configurable - * PR757 Fix default aspect ratio - * PR758 Add password field to initial setup - * PR764 Remove dead code, made some functions properly async - * PR769 Fix conditions where the ! was swallowed in #726 - * PR774 reimplement support for plugin repository - * PR782 Remove commented file MediaBrowser.LocalMetadata.Savers.PersonXmlSaver - * PR783 Update builds to use #749 and #756 - * PR788 Fix more warnings - * PR794 Remove MoreLINQ - * PR797 Fix all warnings - * PR798 Cleanup around the api endpoints - * PR800 Add CentOS and update rpm spec for the cachedir option - * PR802 Fix build error - * PR804 Handle new option parser properly - * PR805 Add weblate translation status to README - * PR807 Fix restart script in OS packages - * PR810 Fix loading of rating files - * PR812 Fix up the explicit docs links in the README - * PR819 Some small changes in Device.cs and DidlBuilder.cs - * PR822 Complete rename ImageSize -> ImageDimensions - * PR824 Improved Docker pkgbuild - * PR831 Move some arrays to generics - * PR833 Add await to GetCountries in LocalizationService - * PR834 Add donation badge and reorganize badges - * PR838 Quick style fix - * PR840 Fix more warnings - * PR841 Fix OC badge to all and add forum badge - * PR842 Use VAAPI-enabled ffmpeg - * PR852 Use SQLitePCL.pretty.netstandard on NuGet - * PR853 Fix poor handling of cache directories - * PR864: Add support for ZIP plugin archives - * PR868: Fix audio streaming via BaseProgressiveStreamingService - * PR869: Remove DLL support and require all packages/plugins to be zip archives - * PR872: Fix potential NullReferenceException - * PR890: Drop ETag and use Last-Modified header - * PR892: Add jellyfin-ffmpeg and versioning to package deps - * PR899: DLNA: Fix race condition leading to missing device names - * PR901: Properly dispose HttpWebResponse when the request failed to avoid 'too many open files' - * PR909: Fix docker arm builds - * PR910: Enhance Dockerfiles - * PR911: Checkout submodules in Docker Hub hook - * jellyfin-web: - * PR51 remove more code for sync and camera roll - * PR56 Use English for fallback translations and clean up language files - * PR58 Css slider fixes - * PR62 remove BOM markers - * PR65 Fix profile image not being shown on profile page - * PR73 Dev sync - * PR74 Add download menu option to media items - * PR75 User profile fixes - * PR76 Fix syntax error caused by deminification - * PR79 Remove unused Connect related from the frontend - * PR80 Remove games - * PR92 Added frontend support for a password field on setup - * PR94 Update british strings - * PR95 add display language option back - * PR112 Removed seasonal theme support - * PR116 Consolidate all strings into a single file per language - * PR117 Fix volume slider behavior - * PR118 Enable and fix PiP for Safari - * PR119 Make the toggle track visible on all themes - * PR121 Fix syntax error in site.js - * PR127 Change sharedcomponents module to core - * PR135 Make sure fallback culture is always available - - -- Jellyfin Packaging Team <packaging@jellyfin.org> Fri, 15 Feb 2019 20:51:25 -0500 - -jellyfin (10.1.0-1) unstable; urgency=medium - - * jellyfin: - * PR335 Build scripts and build system consolidation. - * PR424 add jellyfin-web as submodule - * PR455 Cleanup some small things - * PR458 Clean up several minor issues and add TODOs - * PR506 Removing tabs and trailing whitespace - * PR508 Update internal versioning and user agents. - * PR516 Remove useless properties from IEnvironmentInfo - * PR520 Fix potential bug where aspect ratio would be incorrectly calculated - * PR534 Add linux-arm and linux-arm64 native NuGet dependency. - * PR540 Update Emby API keys to our own - * PR541 Change ItemId to Guid in ProviderManager - * PR556 Fix "Password Reset by PIN" page - * PR562 Fix error with uppercase photo extension and fix typo in a log line - * PR563 Update dev from master - * PR566 Avoid printing stacktrace when bind to port 1900 fails - * PR567 Shutdown gracefully when recieving a termination signal - * PR571 Add more NuGet metadata properties - * PR575 Reformat all C# server code to conform with code standards - * PR576 Add code analysers for debug builds - * PR580 Fix Docker build - * PR582 Replace custom image parser with Skia - * PR587 Add nuget info to Emby.Naming - * PR589 Ensure config and log folders exist - * PR596 Fix indentation for xml files - * PR598 Remove MediaBrowser.Text for license violations and hackiness - * PR606 Slim down docker image - * PR613 Update MediaEncoding - * PR616 Add Swagger documentation - * PR619 Really slim down Docker container - * PR621 Minor improvements to library scan code - * PR622 Add unified build script and bump_version script - * PR623 Replaced injections of ILogger with ILoggerFactory - * PR625 Update taglib-sharp - * PR626 Fix extra type name in parameter, add out keyword - * PR627 Use string for ApplicationVersion - * PR628 Update Product Name (User-Agent) - * PR629 Fix subtitle converter misinterpreting 0 valued endTimeTicks - * PR631 Cleanup ImageProcessor and SkiaEncoder - * PR634 Replace our TVDB key with @drakus72's which is V1 - * PR636 Allow subtitle extraction and conversion in direct streaming - * PR637 Remove unused font - * PR638 Removed XmlTv testfiles and nuget install - * PR646: Fix infinite loop bug on subtitle.m3u8 request - * PR655: Support trying local branches in submodule - * PR661: Fix NullRef from progress report - * PR666: Add cross-platform build for arm64 - * jellyfin-web: - * PR1: Change webcomponents to non-minified version - * PR4: Fix user profile regression - * PR6: Make icon into proper ico and large PNG - * PR7: Fix firefox failing to set password for users with no password set - * PR8: Remove premiere stuff and fix crashes caused by earlier removals - * PR12: Fix return from PIN reset to index.html - * PR13: Send android clients to select server before login - * PR14: Reimplement page to add server - * PR16: Fix spinning circle at the end of config wizard - * PR17: Fix directorybrower not resetting scroll - * PR19: Set union merge for CONTRIBUTORS.md - * PR20: Show album thumbnail and artist image in page itemdetail - * PR26: Make the card titles clickable - * PR27: Stop pagination and adding a library from being able to trigger multiple times - * PR28: Add transparent nav bar to BlueRadiance theme CSS - * PR29: Clean up imageuploader - * PR30: Remove iap and simplify registrationservices - * PR36: Open videos in fullscreen on android devices - * PR37: Remove broken features from web interface - * PR38: Fix inconsistent UI coloring around settings drawer - * PR39: Remove back button from dashboard and metadata manager - * PR42: Fix Home backdrop not loading - * PR43: Filter videos by audio stream language - * PR44: Remove filter from library collection type options - * PR45: Fix data-backbutton logic - * PR46: Minor changes to navbar elements - * PR48: Remove Sync code - * PR52: Fix progress color - * PR53: Fix user tabs color - * PR54: Add back button to server dashboard - - -- Jellyfin Packaging Team <packaging@jellyfin.org> Sun, 20 Jan 2019 23:19:46 -0500 - -jellyfin (10.0.2-1) unstable; urgency=medium - - * Hotfix release - * jellyfin/jellyfin-web#23: Update Chromecast app ID [via direct commit] - * #540: Update Emby API keys to our own - * #541: Change ItemId to Guid in ProviderManager - * #566: Avoid printing stacktrace when bind to port 1900 fails - - -- Joshua Boniface <joshua@boniface.me> Sat, 19 Jan 2019 01:19:59 -0500 - -jellyfin (10.0.1-1) unstable; urgency=medium - - * Hotfix release, corrects several small bugs from 10.0.0 - * #512: Fix CONTRIBUTORS.md formatting - * #501: Fix regression in integer divisions in latest movies category - * #498: Change contributing link in settings to readthedocs.io - * #493: Remove unused values.txt resource - * #491: Fix userprofile.js crash - * #519: Fix the DecodeJfif function to get proper image sizes - * #486: Add NuGet package info to plugin projects - - -- Joshua Boniface <joshua@boniface.me> Tue, 08 Jan 2019 20:06:01 -0500 - -jellyfin (10.0.0-1) unstable; urgency=medium - - * The first Jellyfin release under our new versioning scheme - * Numerous bugfixes and code readability improvements - * Updated logging configuration, including flag for it and configdir - * Updated theming including logo - * Dozens of other improvements as documented in GitHub pull request #419 - - -- Joshua Boniface <joshua@boniface.me> Sat, 05 Jan 2019 15:39:25 -0500 - -jellyfin (3.5.2-5) unstable; urgency=medium - - * Fully GPL'd release - remove tainted code from MediaBrowser.Common - * Several code cleanups and tweaks - - -- Joshua Boniface <joshua@boniface.me> Fri, 28 Dec 2018 10:26:30 -0500 - -jellyfin (3.5.2-4) unstable; urgency=medium - - * Correct manifest.json bug and vdpau - - -- Joshua Boniface <joshua@boniface.me> Thu, 20 Dec 2018 18:31:43 -0500 - -jellyfin (3.5.2-3) unstable; urgency=medium - - * Correct several bugs in 3.5.2-2 packaging - - -- Joshua Boniface <joshua@boniface.me> Sat, 15 Dec 2018 18:17:32 -0500 - -jellyfin (3.5.2-2) unstable; urgency=medium - - * Major code updates related to rebranding and cleanup - - -- Joshua Boniface <joshua@boniface.me> Fri, 14 Dec 2018 00:07:46 -0500 - -jellyfin (3.5.2-1) unstable; urgency=medium - - * Add ffmpeg dependency and cleanup work - - -- Joshua Boniface <joshua@boniface.me> Tue, 11 Dec 2018 20:55:32 -0500 - -jellyfin (3.5.2) unstable; urgency=medium - - * Rename from emby-server on version 3.5.2 - - -- Joshua Boniface <joshua@boniface.me> Sun, 9 Dec 2018 15:20:58 -0400 diff --git a/deployment/debian-package-x64/pkg-src/control b/deployment/debian-package-x64/pkg-src/control index 4422f0fda..af6459604 100644 --- a/deployment/debian-package-x64/pkg-src/control +++ b/deployment/debian-package-x64/pkg-src/control @@ -7,7 +7,8 @@ Build-Depends: debhelper (>= 9), libc6-dev, libcurl4-openssl-dev, libfontconfig1-dev, - libfreetype6-dev + libfreetype6-dev, + libssl-dev Standards-Version: 3.9.4 Homepage: https://jellyfin.media/ Vcs-Git: https://github.org/jellyfin/jellyfin.git @@ -23,6 +24,6 @@ Depends: at, jellyfin-ffmpeg, libfontconfig1, libfreetype6, - libssl1.0.0 | libssl1.0.2 | libssl1.1 + libssl1.1 Description: Jellyfin is a home media server. It is built on top of other popular open source technologies such as Service Stack, jQuery, jQuery mobile, and Mono. It features a REST-based api with built-in documentation to facilitate client development. We also have client libraries for our api to enable rapid development. diff --git a/deployment/docker/build.sh b/deployment/docker/build.sh deleted file mode 100755 index 444208c85..000000000 --- a/deployment/docker/build.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -source ../common.build.sh - -VERSION=`get_version ../..` - -build_jellyfin_docker ../.. ../../Dockerfile jellyfin:amd64-${VERSION} - -build_jellyfin_docker ../.. ../../Dockerfile.arm jellyfin:arm-${VERSION} - -#build_jellyfin_docker ../.. ../../Dockerfile.arm64v8 jellyfin:arm64v8-${VERSION} -#build_jellyfin_docker ../.. ../../Dockerfile.arm32v7 jellyfin:arm32v7-${VERSION} diff --git a/deployment/docker/package.sh b/deployment/docker/package.sh deleted file mode 100755 index d74426e2f..000000000 --- a/deployment/docker/package.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -source ../common.build.sh - -VERSION=`get_version ../..` - -docker manifest create jellyfin:${VERSION} jellyfin:amd64-${VERSION} jellyfin:arm32v7-${VERSION} jellyfin:arm64v8-${VERSION} -docker manifest annotate jellyfin:amd64-${VERSION} --os linux --arch amd64 -#docker manifest annotate jellyfin:arm32v7-${VERSION} --os linux --arch arm --variant armv7 -#docker manifest annotate jellyfin:arm64v8-${VERSION} --os linux --arch arm64 --variant armv8 - -#TODO publish.sh - docker manifest push jellyfin:${VERSION} diff --git a/deployment/fedora-package-x64/Dockerfile b/deployment/fedora-package-x64/Dockerfile index 397c944ea..b8226b173 100644 --- a/deployment/fedora-package-x64/Dockerfile +++ b/deployment/fedora-package-x64/Dockerfile @@ -8,13 +8,23 @@ ARG SDK_VERSION=2.2 ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist -# Prepare Fedora build environment -RUN dnf update -y \ - && dnf install -y @buildsys-build rpmdevtools dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel \ - && dnf copr enable -y @dotnet-sig/dotnet \ +# Prepare Fedora environment +RUN dnf update -y + +# Install build dependencies +RUN dnf install -y @buildsys-build rpmdevtools dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel nodejs wget git + +# Install DotNET SDK +RUN dnf copr enable -y @dotnet-sig/dotnet \ && rpmdev-setuptree \ - && dnf install -y dotnet-sdk-${SDK_VERSION} dotnet-runtime-${SDK_VERSION} \ - && ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \ + && dnf install -y dotnet-sdk-${SDK_VERSION} dotnet-runtime-${SDK_VERSION} + +# Install yarn package manager +RUN wget -q -O /etc/yum.repos.d/yarn.repo https://dl.yarnpkg.com/rpm/yarn.repo \ + && dnf install -y yarn + +# Create symlinks and directories +RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \ && mkdir -p ${SOURCE_DIR}/SPECS \ && ln -s ${PLATFORM_DIR}/pkg-src/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \ && mkdir -p ${SOURCE_DIR}/SOURCES \ diff --git a/deployment/fedora-package-x64/clean.sh b/deployment/fedora-package-x64/clean.sh index 408167e49..700c8f1bb 100755 --- a/deployment/fedora-package-x64/clean.sh +++ b/deployment/fedora-package-x64/clean.sh @@ -1,7 +1,5 @@ #!/usr/bin/env bash -source ../common.build.sh - keep_artifacts="${1}" WORKDIR="$( pwd )" diff --git a/deployment/fedora-package-x64/create_tarball.sh b/deployment/fedora-package-x64/create_tarball.sh deleted file mode 100755 index e8301c989..000000000 --- a/deployment/fedora-package-x64/create_tarball.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash - -# shellcheck disable=SC1091 -source ../common.build.sh - -WORKDIR="$( pwd )" -VERSION="$( sed -ne '/^Version:/s/.* *//p' "${WORKDIR}"/pkg-src/jellyfin.spec )" - -package_temporary_dir="${WORKDIR}/pkg-dist-tmp" -pkg_src_dir="${WORKDIR}/pkg-src" - -GNU_TAR=1 -echo "Bundling all sources for RPM build." -tar \ ---transform "s,^\.,jellyfin-${VERSION}," \ ---exclude='.git*' \ ---exclude='**/.git' \ ---exclude='**/.hg' \ ---exclude='**/.vs' \ ---exclude='**/.vscode' \ ---exclude='deployment' \ ---exclude='**/bin' \ ---exclude='**/obj' \ ---exclude='**/.nuget' \ ---exclude='*.deb' \ ---exclude='*.rpm' \ --czf "$pkg_src_dir/jellyfin-${VERSION}.tar.gz" \ --C "../.." ./ || GNU_TAR=0 - -if [ $GNU_TAR -eq 0 ]; then - echo "The installed tar binary did not support --transform. Using workaround." - mkdir -p "${package_temporary_dir}/jellyfin"{,-"${VERSION}"} - # Not GNU tar - tar \ - --exclude='.git*' \ - --exclude='**/.git' \ - --exclude='**/.hg' \ - --exclude='**/.vs' \ - --exclude='**/.vscode' \ - --exclude='deployment' \ - --exclude='**/bin' \ - --exclude='**/obj' \ - --exclude='**/.nuget' \ - --exclude='*.deb' \ - --exclude='*.rpm' \ - -zcf \ - "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" \ - -C "../.." ./ - echo "Extracting filtered package." - tar -xzf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}/jellyfin-${VERSION}" - echo "Removing filtered package." - rm -f "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" - echo "Repackaging package into final tarball." - tar -czf "${pkg_src_dir}/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}" "jellyfin-${VERSION}" -fi diff --git a/deployment/fedora-package-x64/docker-build.sh b/deployment/fedora-package-x64/docker-build.sh index cefb1652e..014f582f0 100755 --- a/deployment/fedora-package-x64/docker-build.sh +++ b/deployment/fedora-package-x64/docker-build.sh @@ -8,7 +8,69 @@ set -o xtrace # Move to source directory pushd ${SOURCE_DIR} -ls -al SOURCES/pkg-src/ +VERSION="$( grep '^Version:' ${SOURCE_DIR}/SOURCES/pkg-src/jellyfin.spec | awk '{ print $NF }' )" + +# Clone down and build Web frontend +web_build_dir="$( mktemp -d )" +web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web" +git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/ +pushd ${web_build_dir} +if [[ -n ${web_branch} ]]; then + checkout -b origin/${web_branch} +fi +yarn install +mkdir -p ${web_target} +mv dist/* ${web_target}/ +popd +rm -rf ${web_build_dir} + +# Create RPM source archive +GNU_TAR=1 +echo "Bundling all sources for RPM build." +tar \ +--transform "s,^\.,jellyfin-${VERSION}," \ +--exclude='.git*' \ +--exclude='**/.git' \ +--exclude='**/.hg' \ +--exclude='**/.vs' \ +--exclude='**/.vscode' \ +--exclude='deployment' \ +--exclude='**/bin' \ +--exclude='**/obj' \ +--exclude='**/.nuget' \ +--exclude='*.deb' \ +--exclude='*.rpm' \ +-czf "${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-${VERSION}.tar.gz" \ +-C ${SOURCE_DIR} ./ || GNU_TAR=0 + +if [ $GNU_TAR -eq 0 ]; then + echo "The installed tar binary did not support --transform. Using workaround." + package_temporary_dir="$( mktemp -d )" + mkdir -p "${package_temporary_dir}/jellyfin" + # Not GNU tar + tar \ + --exclude='.git*' \ + --exclude='**/.git' \ + --exclude='**/.hg' \ + --exclude='**/.vs' \ + --exclude='**/.vscode' \ + --exclude='deployment' \ + --exclude='**/bin' \ + --exclude='**/obj' \ + --exclude='**/.nuget' \ + --exclude='*.deb' \ + --exclude='*.rpm' \ + -czf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" \ + -C ${SOURCE_DIR} ./ + echo "Extracting filtered package." + mkdir -p "${package_temporary_dir}/jellyfin-${VERSION}" + tar -xzf "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}/jellyfin-${VERSION}" + echo "Removing filtered package." + rm -f "${package_temporary_dir}/jellyfin/jellyfin-${VERSION}.tar.gz" + echo "Repackaging package into final tarball." + tar -czf "${SOURCE_DIR}/SOURCES/pkg-src/jellyfin-${VERSION}.tar.gz" -C "${package_temporary_dir}" "jellyfin-${VERSION}" + rm -rf ${package_temporary_dir} +fi # Build RPM spectool -g -R SPECS/jellyfin.spec diff --git a/deployment/fedora-package-x64/package.sh b/deployment/fedora-package-x64/package.sh index e659ee5e9..ae6962dd1 100755 --- a/deployment/fedora-package-x64/package.sh +++ b/deployment/fedora-package-x64/package.sh @@ -1,13 +1,15 @@ #!/usr/bin/env bash -source ../common.build.sh +args="${@}" +declare -a docker_envvars +for arg in ${args}; do + docker_envvars+=("-e ${arg}") +done WORKDIR="$( pwd )" -VERSION="$( grep '^Version:' ${WORKDIR}/pkg-src/jellyfin.spec | awk '{ print $NF }' )" package_temporary_dir="${WORKDIR}/pkg-dist-tmp" output_dir="${WORKDIR}/pkg-dist" -pkg_src_dir="${WORKDIR}/pkg-src" current_user="$( whoami )" image_name="jellyfin-fedora-build" @@ -21,14 +23,12 @@ else docker_sudo="" fi -./create_tarball.sh - # Prepare temporary package dir mkdir -p "${package_temporary_dir}" # Set up the build environment Docker image ${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile # Build the RPMs and copy out to ${package_temporary_dir} -${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" +${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} # Move the RPMs to the output directory mkdir -p "${output_dir}" mv "${package_temporary_dir}"/rpm/* "${output_dir}" diff --git a/deployment/fedora-package-x64/pkg-src/jellyfin.spec b/deployment/fedora-package-x64/pkg-src/jellyfin.spec index 91b74ffe1..0c6bf7180 100644 --- a/deployment/fedora-package-x64/pkg-src/jellyfin.spec +++ b/deployment/fedora-package-x64/pkg-src/jellyfin.spec @@ -7,7 +7,7 @@ %endif Name: jellyfin -Version: 10.3.7 +Version: 10.5.0 Release: 1%{?dist} Summary: The Free Software Media Browser License: GPLv2 @@ -68,7 +68,7 @@ EOF %{__install} -D -m 0644 %{SOURCE2} %{buildroot}%{_sysconfdir}/sysconfig/%{name} %{__install} -D -m 0600 %{SOURCE3} %{buildroot}%{_sysconfdir}/sudoers.d/%{name}-sudoers %{__install} -D -m 0755 %{SOURCE4} %{buildroot}%{_libexecdir}/%{name}/restart.sh -%{__install} -D -m 0644 %{SOURCE6} %{buildroot}%{_prefix}/lib/firewalld/service/%{name}.xml +%{__install} -D -m 0644 %{SOURCE6} %{buildroot}%{_prefix}/lib/firewalld/services/%{name}.xml %files %{_libdir}/%{name}/jellyfin-web/* @@ -83,7 +83,7 @@ EOF %{_libdir}/%{name}/sosdocsunix.txt %{_unitdir}/%{name}.service %{_libexecdir}/%{name}/restart.sh -%{_prefix}/lib/firewalld/service/%{name}.xml +%{_prefix}/lib/firewalld/services/%{name}.xml %attr(755,jellyfin,jellyfin) %dir %{_sysconfdir}/%{name} %config %{_sysconfdir}/sysconfig/%{name} %config(noreplace) %attr(600,root,root) %{_sysconfdir}/sudoers.d/%{name}-sudoers @@ -140,6 +140,10 @@ fi %systemd_postun_with_restart jellyfin.service %changelog +* Fri Oct 11 2019 Jellyfin Packaging Team <packaging@jellyfin.org> +- New upstream version 10.5.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.5.0 +* Sat Aug 31 2019 Jellyfin Packaging Team <packaging@jellyfin.org> +- New upstream version 10.4.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.4.0 * Wed Jul 24 2019 Jellyfin Packaging Team <packaging@jellyfin.org> - New upstream version 10.3.7; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.7 * Sat Jul 06 2019 Jellyfin Packaging Team <packaging@jellyfin.org> @@ -156,213 +160,3 @@ fi - New upstream version 10.3.1; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.1 * Fri Apr 19 2019 Jellyfin Packaging Team <packaging@jellyfin.org> - New upstream version 10.3.0; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v10.3.0 -* Thu Feb 28 2019 Jellyfin Packaging Team <packaging@jellyfin.org> -- jellyfin: -- PR968 Release 10.2.z copr autobuild -- PR964 Install the dotnet runtime package in Fedora build -- PR979 Build Package releases without debug turned on -- PR990 Fix slow local image validation -- PR991 Fix the ffmpeg compatibility -- PR992 Add Debian armhf (Raspberry Pi) build plus crossbuild -- PR998 Set EnableRaisingEvents to true for processes that require it -- PR1017 Set ffmpeg+ffprobe paths in Docker container -- jellyfin-web: -- PR152 Go back on Media stop -- PR156 Fix volume slider not working on nowplayingbar -* Wed Feb 20 2019 Jellyfin Packaging Team <packaging@jellyfin.org> -- jellyfin: -- PR920 Fix cachedir missing from Docker container -- PR924 Use the movie name instead of folder name -- PR933 Semi-revert to prefer old movie grouping behaviour -- PR948 Revert movie matching (supercedes PR933, PR924, PR739) -- PR960 Use jellyfin/ffmpeg image -- jellyfin-web: -- PR136 Re-add OpenSubtitles configuration page -- PR137 Replace HeaderEmbyServer with HeaderJellyfinServer on plugincatalog -- PR138 Remove left-over JS for Customize Home Screen -- PR141 Exit fullscreen automatically after video playback ends -* Fri Feb 15 2019 Jellyfin Packaging Team <packaging@jellyfin.org> -- jellyfin: -- PR452 Use EF Core for Activity database -- PR535 Clean up streambuilder -- PR655 Support trying local branches in submodule -- PR656 Do some logging in MediaInfoService -- PR657 Remove conditions that are always true/false -- PR661 Fix NullRef from progress report -- PR663 Use TagLibSharp Nuget package -- PR664 Revert "Fix segment_time_delta for ffmpeg 4.1" -- PR666 Add cross-platform build for arm64 -- PR668 Return Audio objects from MusicAlbum.Tracks -- PR671 Set EnableRaisingEvents correctly -- PR672 Remove unconditional caching, modified since header and use ETags -- PR677 Fix arm32 Docker -- PR681 Fix Windows build script errors + pin ffmpeg to 4.0 -- PR686 Disable some StyleCop warnings -- PR687 Fix some analyzer warnings -- PR689 Fix RPM package build for fedora -- PR702 Fix debug build on windows -- PR706 Make another docker layer reusable -- PR709 Fix always null expressions -- PR710 Fix a spelling mistake -- PR711 Remove remnants of system events -- PR713 Fix empty statement in DidlBuilder.cs -- PR716 Remove more compile time warnings -- PR721 Change image dimentions from double to int -- PR723 Minor improvements to db code -- PR724 Move Skia back into it's own project -- PR726 Clean up IFileSystem wrappers around stdlib. -- PR727 Change default aspect ratio to 2/3 from 0 -- PR728 Use ffmpeg from jrottenberg/ffmpeg -- PR732 Reworked LocalizationManager to load data async -- PR733 Remove unused function -- PR734 Fix more analyzer warnings -- PR736 Start startup tasks async -- PR737 Add AssemblyInfo for Jellyfin.Drawing.Skia -- PR739 Change multi version logic for movies -- PR740 Remove code for pre-installed plugins & properly check if file exists -- PR756 Make cache dir configurable -- PR757 Fix default aspect ratio -- PR758 Add password field to initial setup -- PR764 Remove dead code, made some functions properly async -- PR769 Fix conditions where the ! was swallowed in #726 -- PR774 reimplement support for plugin repository -- PR782 Remove commented file MediaBrowser.LocalMetadata.Savers.PersonXmlSaver -- PR783 Update builds to use #749 and #756 -- PR788 Fix more warnings -- PR794 Remove MoreLINQ -- PR797 Fix all warnings -- PR798 Cleanup around the api endpoints -- PR800 Add CentOS and update rpm spec for the cachedir option -- PR802 Fix build error -- PR804 Handle new option parser properly -- PR805 Add weblate translation status to README -- PR807 Fix restart script in OS packages -- PR810 Fix loading of rating files -- PR812 Fix up the explicit docs links in the README -- PR819 Some small changes in Device.cs and DidlBuilder.cs -- PR822 Complete rename ImageSize -> ImageDimensions -- PR824 Improved Docker pkgbuild -- PR831 Move some arrays to generics -- PR833 Add await to GetCountries in LocalizationService -- PR834 Add donation badge and reorganize badges -- PR838 Quick style fix -- PR840 Fix more warnings -- PR841 Fix OC badge to all and add forum badge -- PR842 Use VAAPI-enabled ffmpeg -- PR852 Use SQLitePCL.pretty.netstandard on NuGet -- PR853 Fix poor handling of cache directories -- PR864 Add support for ZIP plugin archives -- PR868 Fix audio streaming via BaseProgressiveStreamingService -- PR869 Remove DLL support and require all packages/plugins to be zip archives -- PR872 Fix potential NullReferenceException -- PR899: DLNA: Fix race condition leading to missing device names -- PR890 Drop ETag and use Last-Modified header -- PR892: Add jellyfin-ffmpeg and versioning to package deps -- PR901: Properly dispose HttpWebResponse when the request failed to avoid 'too many open files' -- PR909: Fix docker arm builds -- PR910: Enhance Dockerfiles -- PR911: Checkout submodules in Docker Hub hook -- jellyfin-web: -- PR51 remove more code for sync and camera roll -- PR56 Use English for fallback translations and clean up language files -- PR58 Css slider fixes -- PR62 remove BOM markers -- PR65 Fix profile image not being shown on profile page -- PR73 Dev sync -- PR74 Add download menu option to media items -- PR75 User profile fixes -- PR76 Fix syntax error caused by deminification -- PR79 Remove unused Connect related from the frontend -- PR80 Remove games -- PR92 Added frontend support for a password field on setup -- PR94 Update british strings -- PR95 add display language option back -- PR112 Removed seasonal theme support -- PR116 Consolidate all strings into a single file per language -- PR117 Fix volume slider behavior -- PR118 Enable and fix PiP for Safari -- PR119 Make the toggle track visible on all themes -- PR121 Fix syntax error in site.js -- PR127 Change sharedcomponents module to core -- PR135 Make sure fallback culture is always available -* Sun Jan 20 2019 Jellyfin Packaging Team <packaging@jellyfin.org> -- jellyfin: -- PR335 Build scripts and build system consolidation. -- PR424 add jellyfin-web as submodule -- PR455 Cleanup some small things -- PR458 Clean up several minor issues and add TODOs -- PR506 Removing tabs and trailing whitespace -- PR508 Update internal versioning and user agents. -- PR516 Remove useless properties from IEnvironmentInfo -- PR520 Fix potential bug where aspect ratio would be incorrectly calculated -- PR534 Add linux-arm and linux-arm64 native NuGet dependency. -- PR540 Update Emby API keys to our own -- PR541 Change ItemId to Guid in ProviderManager -- PR556 Fix "Password Reset by PIN" page -- PR562 Fix error with uppercase photo extension and fix typo in a log line -- PR563 Update dev from master -- PR566 Avoid printing stacktrace when bind to port 1900 fails -- PR567 Shutdown gracefully when recieving a termination signal -- PR571 Add more NuGet metadata properties -- PR575 Reformat all C# server code to conform with code standards -- PR576 Add code analysers for debug builds -- PR580 Fix Docker build -- PR582 Replace custom image parser with Skia -- PR587 Add nuget info to Emby.Naming -- PR589 Ensure config and log folders exist -- PR596 Fix indentation for xml files -- PR598 Remove MediaBrowser.Text for license violations and hackiness -- PR606 Slim down docker image -- PR613 Update MediaEncoding -- PR616 Add Swagger documentation -- PR619 Really slim down Docker container -- PR621 Minor improvements to library scan code -- PR622 Add unified build script and bump_version script -- PR623 Replaced injections of ILogger with ILoggerFactory -- PR625 Update taglib-sharp -- PR626 Fix extra type name in parameter, add out keyword -- PR627 Use string for ApplicationVersion -- PR628 Update Product Name (User-Agent) -- PR629 Fix subtitle converter misinterpreting 0 valued endTimeTicks -- PR631 Cleanup ImageProcessor and SkiaEncoder -- PR634 Replace our TVDB key with @drakus72's which is V1 -- PR636 Allow subtitle extraction and conversion in direct streaming -- PR637 Remove unused font -- PR638 Removed XmlTv testfiles and nuget install -- PR646: Fix infinite loop bug on subtitle.m3u8 request -- PR655: Support trying local branches in submodule -- PR661: Fix NullRef from progress report -- PR666: Add cross-platform build for arm64 -- jellyfin-web: -- PR1: Change webcomponents to non-minified version -- PR4: Fix user profile regression -- PR6: Make icon into proper ico and large PNG -- PR7: Fix firefox failing to set password for users with no password set -- PR8: Remove premiere stuff and fix crashes caused by earlier removals -- PR12: Fix return from PIN reset to index.html -- PR13: Send android clients to select server before login -- PR14: Reimplement page to add server -- PR16: Fix spinning circle at the end of config wizard -- PR17: Fix directorybrower not resetting scroll -- PR19: Set union merge for CONTRIBUTORS.md -- PR20: Show album thumbnail and artist image in page itemdetail -- PR26: Make the card titles clickable -- PR27: Stop pagination and adding a library from being able to trigger multiple times -- PR28: Add transparent nav bar to BlueRadiance theme CSS -- PR29: Clean up imageuploader -- PR30: Remove iap and simplify registrationservices -- PR36: Open videos in fullscreen on android devices -- PR37: Remove broken features from web interface -- PR38: Fix inconsistent UI coloring around settings drawer -- PR39: Remove back button from dashboard and metadata manager -- PR42: Fix Home backdrop not loading -- PR43: Filter videos by audio stream language -- PR44: Remove filter from library collection type options -- PR45: Fix data-backbutton logic -- PR46: Minor changes to navbar elements -- PR48: Remove Sync code -- PR52: Fix progress color -- PR53: Fix user tabs color -- PR54: Add back button to server dashboard -* Fri Jan 11 2019 Thomas Büttner <thomas@vergesslicher.tech> - 10.0.2-1 -- TODO Changelog for 10.0.2 diff --git a/deployment/linux-x64/Dockerfile b/deployment/linux-x64/Dockerfile new file mode 100644 index 000000000..d634b55de --- /dev/null +++ b/deployment/linux-x64/Dockerfile @@ -0,0 +1,37 @@ +FROM debian:10 +# Docker build arguments +ARG SOURCE_DIR=/jellyfin +ARG PLATFORM_DIR=/jellyfin/deployment/linux-x64 +ARG ARTIFACT_DIR=/dist +ARG SDK_VERSION=2.2 +# Docker run environment +ENV SOURCE_DIR=/jellyfin +ENV ARTIFACT_DIR=/dist +ENV DEB_BUILD_OPTIONS=noddebs +ENV ARCH=amd64 + +# Prepare Debian build environment +RUN apt-get update \ + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 + +# Install dotnet repository +# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current +RUN wget https://download.visualstudio.microsoft.com/download/pr/228832ea-805f-45ab-8c88-fa36165701b9/16ce29a06031eeb09058dee94d6f5330/dotnet-sdk-2.2.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ + && mkdir -p dotnet-sdk \ + && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ + && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet + +# Install yarn package manager +RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ + && apt update \ + && apt install -y yarn + +# Link to docker-build script +RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh + +VOLUME ${ARTIFACT_DIR}/ + +COPY . ${SOURCE_DIR}/ + +ENTRYPOINT ["/docker-build.sh"] diff --git a/deployment/linux-x64/build.sh b/deployment/linux-x64/build.sh deleted file mode 100755 index 1f0fb62d3..000000000 --- a/deployment/linux-x64/build.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -source ../common.build.sh - -VERSION=`get_version ../..` - -build_jellyfin ../../Jellyfin.Server Release linux-x64 `pwd`/dist/jellyfin_${VERSION} diff --git a/deployment/linux-x64/clean.sh b/deployment/linux-x64/clean.sh index 3df2d7796..c07501a7b 100755 --- a/deployment/linux-x64/clean.sh +++ b/deployment/linux-x64/clean.sh @@ -1,7 +1,27 @@ #!/usr/bin/env bash -source ../common.build.sh +keep_artifacts="${1}" -VERSION=`get_version ../..` +WORKDIR="$( pwd )" -clean_jellyfin ../.. Release `pwd`/dist/jellyfin_${VERSION} +package_temporary_dir="${WORKDIR}/pkg-dist-tmp" +output_dir="${WORKDIR}/pkg-dist" +current_user="$( whoami )" +image_name="jellyfin-linux-build" + +rm -rf "${package_temporary_dir}" &>/dev/null \ + || sudo rm -rf "${package_temporary_dir}" &>/dev/null + +rm -rf "${output_dir}" &>/dev/null \ + || sudo rm -rf "${output_dir}" &>/dev/null + +if [[ ${keep_artifacts} == 'n' ]]; then + docker_sudo="" + if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ + && [[ ! ${EUID:-1000} -eq 0 ]] \ + && [[ ! ${USER} == "root" ]] \ + && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then + docker_sudo=sudo + fi + ${docker_sudo} docker image rm ${image_name} --force +fi diff --git a/deployment/linux-x64/dependencies.txt b/deployment/linux-x64/dependencies.txt index 3d25d1bdf..bdb967096 100644 --- a/deployment/linux-x64/dependencies.txt +++ b/deployment/linux-x64/dependencies.txt @@ -1 +1 @@ -dotnet +docker diff --git a/deployment/linux-x64/docker-build.sh b/deployment/linux-x64/docker-build.sh new file mode 100755 index 000000000..8860f943c --- /dev/null +++ b/deployment/linux-x64/docker-build.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Builds the TAR archive inside the Docker container + +set -o errexit +set -o xtrace + +# Move to source directory +pushd ${SOURCE_DIR} + +# Clone down and build Web frontend +web_build_dir="$( mktemp -d )" +web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web" +git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/ +pushd ${web_build_dir} +if [[ -n ${web_branch} ]]; then + checkout -b origin/${web_branch} +fi +yarn install +mkdir -p ${web_target} +mv dist/* ${web_target}/ +popd +rm -rf ${web_build_dir} + +# Get version +version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )" + +# Build archives +dotnet publish --configuration Release --self-contained --runtime linux-x64 --output /dist/jellyfin_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true" +tar -cvzf /jellyfin_${version}.portable.tar.gz -C /dist jellyfin_${version} +rm -rf /dist/jellyfin_${version} + +# Move the artifacts out +mkdir -p ${ARTIFACT_DIR}/ +mv /jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/ +chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/linux-x64/package.sh b/deployment/linux-x64/package.sh index 13b943ea8..dfe8a9aa4 100755 --- a/deployment/linux-x64/package.sh +++ b/deployment/linux-x64/package.sh @@ -1,7 +1,34 @@ #!/usr/bin/env bash -source ../common.build.sh +args="${@}" +declare -a docker_envvars +for arg in ${args}; do + docker_envvars+=("-e ${arg}") +done -VERSION=`get_version ../..` +WORKDIR="$( pwd )" -package_portable ../.. `pwd`/dist/jellyfin_${VERSION} +package_temporary_dir="${WORKDIR}/pkg-dist-tmp" +output_dir="${WORKDIR}/pkg-dist" +current_user="$( whoami )" +image_name="jellyfin-linux-build" + +# Determine if sudo should be used for Docker +if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ + && [[ ! ${EUID:-1000} -eq 0 ]] \ + && [[ ! ${USER} == "root" ]] \ + && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then + docker_sudo="sudo" +else + docker_sudo="" +fi + +# Prepare temporary package dir +mkdir -p "${package_temporary_dir}" +# Set up the build environment Docker image +${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile +# Build the DEBs and copy out to ${package_temporary_dir} +${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} +# Move the DEBs to the output directory +mkdir -p "${output_dir}" +mv "${package_temporary_dir}"/* "${output_dir}" diff --git a/deployment/macos/Dockerfile b/deployment/macos/Dockerfile new file mode 100644 index 000000000..406a2d853 --- /dev/null +++ b/deployment/macos/Dockerfile @@ -0,0 +1,37 @@ +FROM debian:10 +# Docker build arguments +ARG SOURCE_DIR=/jellyfin +ARG PLATFORM_DIR=/jellyfin/deployment/macos +ARG ARTIFACT_DIR=/dist +ARG SDK_VERSION=2.2 +# Docker run environment +ENV SOURCE_DIR=/jellyfin +ENV ARTIFACT_DIR=/dist +ENV DEB_BUILD_OPTIONS=noddebs +ENV ARCH=amd64 + +# Prepare Debian build environment +RUN apt-get update \ + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 + +# Install dotnet repository +# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current +RUN wget https://download.visualstudio.microsoft.com/download/pr/228832ea-805f-45ab-8c88-fa36165701b9/16ce29a06031eeb09058dee94d6f5330/dotnet-sdk-2.2.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ + && mkdir -p dotnet-sdk \ + && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ + && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet + +# Install yarn package manager +RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ + && apt update \ + && apt install -y yarn + +# Link to docker-build script +RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh + +VOLUME ${ARTIFACT_DIR}/ + +COPY . ${SOURCE_DIR}/ + +ENTRYPOINT ["/docker-build.sh"] diff --git a/deployment/macos/build.sh b/deployment/macos/build.sh deleted file mode 100755 index d6bfb9f5e..000000000 --- a/deployment/macos/build.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -source ../common.build.sh - -VERSION=`get_version ../..` - -build_jellyfin ../../Jellyfin.Server Release osx-x64 `pwd`/dist/jellyfin_${VERSION} diff --git a/deployment/macos/clean.sh b/deployment/macos/clean.sh index 3df2d7796..c07501a7b 100755 --- a/deployment/macos/clean.sh +++ b/deployment/macos/clean.sh @@ -1,7 +1,27 @@ #!/usr/bin/env bash -source ../common.build.sh +keep_artifacts="${1}" -VERSION=`get_version ../..` +WORKDIR="$( pwd )" -clean_jellyfin ../.. Release `pwd`/dist/jellyfin_${VERSION} +package_temporary_dir="${WORKDIR}/pkg-dist-tmp" +output_dir="${WORKDIR}/pkg-dist" +current_user="$( whoami )" +image_name="jellyfin-linux-build" + +rm -rf "${package_temporary_dir}" &>/dev/null \ + || sudo rm -rf "${package_temporary_dir}" &>/dev/null + +rm -rf "${output_dir}" &>/dev/null \ + || sudo rm -rf "${output_dir}" &>/dev/null + +if [[ ${keep_artifacts} == 'n' ]]; then + docker_sudo="" + if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ + && [[ ! ${EUID:-1000} -eq 0 ]] \ + && [[ ! ${USER} == "root" ]] \ + && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then + docker_sudo=sudo + fi + ${docker_sudo} docker image rm ${image_name} --force +fi diff --git a/deployment/macos/dependencies.txt b/deployment/macos/dependencies.txt index 3d25d1bdf..bdb967096 100644 --- a/deployment/macos/dependencies.txt +++ b/deployment/macos/dependencies.txt @@ -1 +1 @@ -dotnet +docker diff --git a/deployment/macos/docker-build.sh b/deployment/macos/docker-build.sh new file mode 100755 index 000000000..1b4a554e6 --- /dev/null +++ b/deployment/macos/docker-build.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Builds the TAR archive inside the Docker container + +set -o errexit +set -o xtrace + +# Move to source directory +pushd ${SOURCE_DIR} + +# Clone down and build Web frontend +web_build_dir="$( mktemp -d )" +web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web" +git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/ +pushd ${web_build_dir} +if [[ -n ${web_branch} ]]; then + checkout -b origin/${web_branch} +fi +yarn install +mkdir -p ${web_target} +mv dist/* ${web_target}/ +popd +rm -rf ${web_build_dir} + +# Get version +version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )" + +# Build archives +dotnet publish --configuration Release --self-contained --runtime osx-x64 --output /dist/jellyfin_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true" +tar -cvzf /jellyfin_${version}.portable.tar.gz -C /dist jellyfin_${version} +rm -rf /dist/jellyfin_${version} + +# Move the artifacts out +mkdir -p ${ARTIFACT_DIR}/ +mv /jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/ +chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/macos/package.sh b/deployment/macos/package.sh index 13b943ea8..464c0d382 100755 --- a/deployment/macos/package.sh +++ b/deployment/macos/package.sh @@ -1,7 +1,34 @@ #!/usr/bin/env bash -source ../common.build.sh +args="${@}" +declare -a docker_envvars +for arg in ${args}; do + docker_envvars+=("-e ${arg}") +done -VERSION=`get_version ../..` +WORKDIR="$( pwd )" -package_portable ../.. `pwd`/dist/jellyfin_${VERSION} +package_temporary_dir="${WORKDIR}/pkg-dist-tmp" +output_dir="${WORKDIR}/pkg-dist" +current_user="$( whoami )" +image_name="jellyfin-macos-build" + +# Determine if sudo should be used for Docker +if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ + && [[ ! ${EUID:-1000} -eq 0 ]] \ + && [[ ! ${USER} == "root" ]] \ + && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then + docker_sudo="sudo" +else + docker_sudo="" +fi + +# Prepare temporary package dir +mkdir -p "${package_temporary_dir}" +# Set up the build environment Docker image +${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile +# Build the DEBs and copy out to ${package_temporary_dir} +${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} +# Move the DEBs to the output directory +mkdir -p "${output_dir}" +mv "${package_temporary_dir}"/* "${output_dir}" diff --git a/deployment/portable/Dockerfile b/deployment/portable/Dockerfile new file mode 100644 index 000000000..bdbf978fe --- /dev/null +++ b/deployment/portable/Dockerfile @@ -0,0 +1,37 @@ +FROM debian:10 +# Docker build arguments +ARG SOURCE_DIR=/jellyfin +ARG PLATFORM_DIR=/jellyfin/deployment/portable +ARG ARTIFACT_DIR=/dist +ARG SDK_VERSION=2.2 +# Docker run environment +ENV SOURCE_DIR=/jellyfin +ENV ARTIFACT_DIR=/dist +ENV DEB_BUILD_OPTIONS=noddebs +ENV ARCH=amd64 + +# Prepare Debian build environment +RUN apt-get update \ + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 + +# Install dotnet repository +# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current +RUN wget https://download.visualstudio.microsoft.com/download/pr/228832ea-805f-45ab-8c88-fa36165701b9/16ce29a06031eeb09058dee94d6f5330/dotnet-sdk-2.2.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ + && mkdir -p dotnet-sdk \ + && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ + && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet + +# Install yarn package manager +RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ + && apt update \ + && apt install -y yarn + +# Link to docker-build script +RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh + +VOLUME ${ARTIFACT_DIR}/ + +COPY . ${SOURCE_DIR}/ + +ENTRYPOINT ["/docker-build.sh"] diff --git a/deployment/portable/build.sh b/deployment/portable/build.sh deleted file mode 100755 index 4f2e6363e..000000000 --- a/deployment/portable/build.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -source ../common.build.sh - -VERSION=`get_version ../..` - -#Magic word framework will create a non self contained build -build_jellyfin ../../Jellyfin.Server Release framework `pwd`/dist/jellyfin_${VERSION} diff --git a/deployment/portable/clean.sh b/deployment/portable/clean.sh index 3df2d7796..c07501a7b 100755 --- a/deployment/portable/clean.sh +++ b/deployment/portable/clean.sh @@ -1,7 +1,27 @@ #!/usr/bin/env bash -source ../common.build.sh +keep_artifacts="${1}" -VERSION=`get_version ../..` +WORKDIR="$( pwd )" -clean_jellyfin ../.. Release `pwd`/dist/jellyfin_${VERSION} +package_temporary_dir="${WORKDIR}/pkg-dist-tmp" +output_dir="${WORKDIR}/pkg-dist" +current_user="$( whoami )" +image_name="jellyfin-linux-build" + +rm -rf "${package_temporary_dir}" &>/dev/null \ + || sudo rm -rf "${package_temporary_dir}" &>/dev/null + +rm -rf "${output_dir}" &>/dev/null \ + || sudo rm -rf "${output_dir}" &>/dev/null + +if [[ ${keep_artifacts} == 'n' ]]; then + docker_sudo="" + if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ + && [[ ! ${EUID:-1000} -eq 0 ]] \ + && [[ ! ${USER} == "root" ]] \ + && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then + docker_sudo=sudo + fi + ${docker_sudo} docker image rm ${image_name} --force +fi diff --git a/deployment/docker/dependencies.txt b/deployment/portable/dependencies.txt index bdb967096..bdb967096 100644 --- a/deployment/docker/dependencies.txt +++ b/deployment/portable/dependencies.txt diff --git a/deployment/portable/docker-build.sh b/deployment/portable/docker-build.sh new file mode 100755 index 000000000..0cc6e84f0 --- /dev/null +++ b/deployment/portable/docker-build.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Builds the TAR archive inside the Docker container + +set -o errexit +set -o xtrace + +# Move to source directory +pushd ${SOURCE_DIR} + +# Clone down and build Web frontend +web_build_dir="$( mktemp -d )" +web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web" +git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/ +pushd ${web_build_dir} +if [[ -n ${web_branch} ]]; then + checkout -b origin/${web_branch} +fi +yarn install +mkdir -p ${web_target} +mv dist/* ${web_target}/ +popd +rm -rf ${web_build_dir} + +# Get version +version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )" + +# Build archives +dotnet publish --configuration Release --output /dist/jellyfin_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" +tar -cvzf /jellyfin_${version}.portable.tar.gz -C /dist jellyfin_${version} +rm -rf /dist/jellyfin_${version} + +# Move the artifacts out +mkdir -p ${ARTIFACT_DIR}/ +mv /jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/ +chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/portable/package.sh b/deployment/portable/package.sh index 13b943ea8..0ceb54dda 100755 --- a/deployment/portable/package.sh +++ b/deployment/portable/package.sh @@ -1,7 +1,34 @@ #!/usr/bin/env bash -source ../common.build.sh +args="${@}" +declare -a docker_envvars +for arg in ${args}; do + docker_envvars+=("-e ${arg}") +done -VERSION=`get_version ../..` +WORKDIR="$( pwd )" -package_portable ../.. `pwd`/dist/jellyfin_${VERSION} +package_temporary_dir="${WORKDIR}/pkg-dist-tmp" +output_dir="${WORKDIR}/pkg-dist" +current_user="$( whoami )" +image_name="jellyfin-portable-build" + +# Determine if sudo should be used for Docker +if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ + && [[ ! ${EUID:-1000} -eq 0 ]] \ + && [[ ! ${USER} == "root" ]] \ + && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then + docker_sudo="sudo" +else + docker_sudo="" +fi + +# Prepare temporary package dir +mkdir -p "${package_temporary_dir}" +# Set up the build environment Docker image +${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile +# Build the DEBs and copy out to ${package_temporary_dir} +${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} +# Move the DEBs to the output directory +mkdir -p "${output_dir}" +mv "${package_temporary_dir}"/* "${output_dir}" diff --git a/deployment/ubuntu-package-arm64/Dockerfile.amd64 b/deployment/ubuntu-package-arm64/Dockerfile.amd64 index 5e51ef0f0..838e70d50 100644 --- a/deployment/ubuntu-package-arm64/Dockerfile.amd64 +++ b/deployment/ubuntu-package-arm64/Dockerfile.amd64 @@ -38,7 +38,13 @@ RUN rm /etc/apt/sources.list \ && TARGET_LIST="arm64" cross-gcc-gensource 6 \ && cd cross-gcc-packages-amd64/cross-gcc-6-arm64 \ && ln -fs /usr/share/zoneinfo/America/Toronto /etc/localtime \ - && apt-get install -y gcc-6-source libstdc++6-arm64-cross binutils-aarch64-linux-gnu bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 libfreetype6-dev:arm64 liblttng-ust0:arm64 libstdc++6:arm64 + && apt-get install -y gcc-6-source libstdc++6-arm64-cross binutils-aarch64-linux-gnu bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 libfreetype6-dev:arm64 liblttng-ust0:arm64 libstdc++6:arm64 libssl-dev:arm64 + +# Install yarn package manager +RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ + && apt update \ + && apt install -y yarn # Link to docker-build script RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh diff --git a/deployment/ubuntu-package-arm64/Dockerfile.arm64 b/deployment/ubuntu-package-arm64/Dockerfile.arm64 index 646679328..789dcc15a 100644 --- a/deployment/ubuntu-package-arm64/Dockerfile.arm64 +++ b/deployment/ubuntu-package-arm64/Dockerfile.arm64 @@ -12,7 +12,7 @@ ENV ARCH=arm64 # Prepare Debian build environment RUN apt-get update \ - && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0 + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0 libssl-dev # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current diff --git a/deployment/ubuntu-package-arm64/clean.sh b/deployment/ubuntu-package-arm64/clean.sh index c92c7fdec..82d427f9e 100755 --- a/deployment/ubuntu-package-arm64/clean.sh +++ b/deployment/ubuntu-package-arm64/clean.sh @@ -1,7 +1,5 @@ #!/usr/bin/env bash -source ../common.build.sh - keep_artifacts="${1}" WORKDIR="$( pwd )" diff --git a/deployment/ubuntu-package-arm64/docker-build.sh b/deployment/ubuntu-package-arm64/docker-build.sh index 1c75ece8e..7a13bafcb 100755 --- a/deployment/ubuntu-package-arm64/docker-build.sh +++ b/deployment/ubuntu-package-arm64/docker-build.sh @@ -11,6 +11,20 @@ pushd ${SOURCE_DIR} # Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image sed -i '/dotnet-sdk-2.2,/d' debian/control +# Clone down and build Web frontend +web_build_dir="$( mktemp -d )" +web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web" +git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/ +pushd ${web_build_dir} +if [[ -n ${web_branch} ]]; then + checkout -b origin/${web_branch} +fi +yarn install +mkdir -p ${web_target} +mv dist/* ${web_target}/ +popd +rm -rf ${web_build_dir} + # Build DEB export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH} dpkg-buildpackage -us -uc -aarm64 diff --git a/deployment/ubuntu-package-arm64/package.sh b/deployment/ubuntu-package-arm64/package.sh index 5a2bf61c8..d1140a727 100755 --- a/deployment/ubuntu-package-arm64/package.sh +++ b/deployment/ubuntu-package-arm64/package.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash -source ../common.build.sh +args="${@}" +declare -a docker_envvars +for arg in ${args}; do + docker_envvars+=("-e ${arg}") +done ARCH="$( arch )" WORKDIR="$( pwd )" @@ -35,7 +39,7 @@ mkdir -p "${package_temporary_dir}" # Set up the build environment Docker image ${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE} # Build the DEBs and copy out to ${package_temporary_dir} -${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" +${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} # Move the DEBs to the output directory mkdir -p "${output_dir}" mv "${package_temporary_dir}"/deb/* "${output_dir}" diff --git a/deployment/ubuntu-package-armhf/Dockerfile.amd64 b/deployment/ubuntu-package-armhf/Dockerfile.amd64 index ef0735e42..d1123e0b6 100644 --- a/deployment/ubuntu-package-armhf/Dockerfile.amd64 +++ b/deployment/ubuntu-package-armhf/Dockerfile.amd64 @@ -38,7 +38,13 @@ RUN rm /etc/apt/sources.list \ && TARGET_LIST="armhf" cross-gcc-gensource 6 \ && cd cross-gcc-packages-amd64/cross-gcc-6-armhf \ && ln -fs /usr/share/zoneinfo/America/Toronto /etc/localtime \ - && apt-get install -y gcc-6-source libstdc++6-armhf-cross binutils-arm-linux-gnueabihf bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:armhf linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf libfontconfig1-dev:armhf libfreetype6-dev:armhf liblttng-ust0:armhf libstdc++6:armhf + && apt-get install -y gcc-6-source libstdc++6-armhf-cross binutils-arm-linux-gnueabihf bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip libc6-dev:armhf linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf libfontconfig1-dev:armhf libfreetype6-dev:armhf liblttng-ust0:armhf libstdc++6:armhf libssl-dev:armhf + +# Install yarn package manager +RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ + && apt update \ + && apt install -y yarn # Link to docker-build script RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh diff --git a/deployment/ubuntu-package-armhf/Dockerfile.armhf b/deployment/ubuntu-package-armhf/Dockerfile.armhf index 72c464724..c9e093e51 100644 --- a/deployment/ubuntu-package-armhf/Dockerfile.armhf +++ b/deployment/ubuntu-package-armhf/Dockerfile.armhf @@ -12,7 +12,7 @@ ENV ARCH=armhf # Prepare Debian build environment RUN apt-get update \ - && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0 + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev liblttng-ust0 libssl-dev # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current @@ -21,6 +21,12 @@ RUN wget https://download.visualstudio.microsoft.com/download/pr/d9f37b73-df8d-4 && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet +# Install yarn package manager +RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ + && apt update \ + && apt install -y yarn + # Link to docker-build script RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh diff --git a/deployment/ubuntu-package-armhf/clean.sh b/deployment/ubuntu-package-armhf/clean.sh index c92c7fdec..82d427f9e 100755 --- a/deployment/ubuntu-package-armhf/clean.sh +++ b/deployment/ubuntu-package-armhf/clean.sh @@ -1,7 +1,5 @@ #!/usr/bin/env bash -source ../common.build.sh - keep_artifacts="${1}" WORKDIR="$( pwd )" diff --git a/deployment/ubuntu-package-armhf/docker-build.sh b/deployment/ubuntu-package-armhf/docker-build.sh index df35345bd..c48ccb3fb 100755 --- a/deployment/ubuntu-package-armhf/docker-build.sh +++ b/deployment/ubuntu-package-armhf/docker-build.sh @@ -11,6 +11,20 @@ pushd ${SOURCE_DIR} # Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image sed -i '/dotnet-sdk-2.2,/d' debian/control +# Clone down and build Web frontend +web_build_dir="$( mktemp -d )" +web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web" +git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/ +pushd ${web_build_dir} +if [[ -n ${web_branch} ]]; then + checkout -b origin/${web_branch} +fi +yarn install +mkdir -p ${web_target} +mv dist/* ${web_target}/ +popd +rm -rf ${web_build_dir} + # Build DEB export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH} dpkg-buildpackage -us -uc -aarmhf diff --git a/deployment/ubuntu-package-armhf/package.sh b/deployment/ubuntu-package-armhf/package.sh index 15f55bff2..2ceb3e816 100755 --- a/deployment/ubuntu-package-armhf/package.sh +++ b/deployment/ubuntu-package-armhf/package.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash -source ../common.build.sh +args="${@}" +declare -a docker_envvars +for arg in ${args}; do + docker_envvars+=("-e ${arg}") +done ARCH="$( arch )" WORKDIR="$( pwd )" @@ -35,7 +39,7 @@ mkdir -p "${package_temporary_dir}" # Set up the build environment Docker image ${docker_sudo} docker build ../.. -t "${image_name}" -f ./${DOCKERFILE} # Build the DEBs and copy out to ${package_temporary_dir} -${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" +${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} # Move the DEBs to the output directory mkdir -p "${output_dir}" mv "${package_temporary_dir}"/deb/* "${output_dir}" diff --git a/deployment/ubuntu-package-x64/Dockerfile b/deployment/ubuntu-package-x64/Dockerfile index 485b6c42c..1749d2ad0 100644 --- a/deployment/ubuntu-package-x64/Dockerfile +++ b/deployment/ubuntu-package-x64/Dockerfile @@ -10,10 +10,16 @@ ENV DEB_BUILD_OPTIONS=noddebs # Prepare Ubuntu build environment RUN apt-get update \ - && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev \ + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev \ && ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh \ && mkdir -p ${SOURCE_DIR} && ln -sf ${PLATFORM_DIR}/pkg-src ${SOURCE_DIR}/debian +# Install yarn package manager +RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ + && apt update \ + && apt install -y yarn + VOLUME ${ARTIFACT_DIR}/ COPY . ${SOURCE_DIR}/ diff --git a/deployment/ubuntu-package-x64/clean.sh b/deployment/ubuntu-package-x64/clean.sh index c92c7fdec..82d427f9e 100755 --- a/deployment/ubuntu-package-x64/clean.sh +++ b/deployment/ubuntu-package-x64/clean.sh @@ -1,7 +1,5 @@ #!/usr/bin/env bash -source ../common.build.sh - keep_artifacts="${1}" WORKDIR="$( pwd )" diff --git a/deployment/ubuntu-package-x64/docker-build.sh b/deployment/ubuntu-package-x64/docker-build.sh index 9781879f6..97bc45a06 100755 --- a/deployment/ubuntu-package-x64/docker-build.sh +++ b/deployment/ubuntu-package-x64/docker-build.sh @@ -11,6 +11,20 @@ pushd ${SOURCE_DIR} # Remove build-dep for dotnet-sdk-2.2, since it's not a package in this image sed -i '/dotnet-sdk-2.2,/d' debian/control +# Clone down and build Web frontend +web_build_dir="$( mktemp -d )" +web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web" +git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/ +pushd ${web_build_dir} +if [[ -n ${web_branch} ]]; then + checkout -b origin/${web_branch} +fi +yarn install +mkdir -p ${web_target} +mv dist/* ${web_target}/ +popd +rm -rf ${web_build_dir} + # Build DEB dpkg-buildpackage -us -uc diff --git a/deployment/ubuntu-package-x64/package.sh b/deployment/ubuntu-package-x64/package.sh index 32e6d4fd6..08c003778 100755 --- a/deployment/ubuntu-package-x64/package.sh +++ b/deployment/ubuntu-package-x64/package.sh @@ -1,6 +1,10 @@ #!/usr/bin/env bash -source ../common.build.sh +args="${@}" +declare -a docker_envvars +for arg in ${args}; do + docker_envvars+=("-e ${arg}") +done WORKDIR="$( pwd )" @@ -24,7 +28,7 @@ mkdir -p "${package_temporary_dir}" # Set up the build environment Docker image ${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile # Build the DEBs and copy out to ${package_temporary_dir} -${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" +${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} # Move the DEBs to the output directory mkdir -p "${output_dir}" mv "${package_temporary_dir}"/deb/* "${output_dir}" diff --git a/deployment/win-x64/Dockerfile b/deployment/win-x64/Dockerfile new file mode 100644 index 000000000..7f64c7dae --- /dev/null +++ b/deployment/win-x64/Dockerfile @@ -0,0 +1,37 @@ +FROM debian:10 +# Docker build arguments +ARG SOURCE_DIR=/jellyfin +ARG PLATFORM_DIR=/jellyfin/deployment/win-x64 +ARG ARTIFACT_DIR=/dist +ARG SDK_VERSION=2.2 +# Docker run environment +ENV SOURCE_DIR=/jellyfin +ENV ARTIFACT_DIR=/dist +ENV DEB_BUILD_OPTIONS=noddebs +ENV ARCH=amd64 + +# Prepare Debian build environment +RUN apt-get update \ + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 zip + +# Install dotnet repository +# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current +RUN wget https://download.visualstudio.microsoft.com/download/pr/228832ea-805f-45ab-8c88-fa36165701b9/16ce29a06031eeb09058dee94d6f5330/dotnet-sdk-2.2.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ + && mkdir -p dotnet-sdk \ + && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ + && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet + +# Install yarn package manager +RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ + && apt update \ + && apt install -y yarn + +# Link to docker-build script +RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh + +VOLUME ${ARTIFACT_DIR}/ + +COPY . ${SOURCE_DIR}/ + +ENTRYPOINT ["/docker-build.sh"] diff --git a/deployment/win-x64/build.sh b/deployment/win-x64/build.sh deleted file mode 100755 index 0b3046203..000000000 --- a/deployment/win-x64/build.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -source ../common.build.sh - -VERSION=`get_version ../..` - -build_jellyfin ../../Jellyfin.Server Release win-x64 `pwd`/dist/jellyfin_${VERSION} diff --git a/deployment/win-x64/clean.sh b/deployment/win-x64/clean.sh index 3df2d7796..6c183f337 100755 --- a/deployment/win-x64/clean.sh +++ b/deployment/win-x64/clean.sh @@ -1,7 +1,27 @@ #!/usr/bin/env bash -source ../common.build.sh +keep_artifacts="${1}" -VERSION=`get_version ../..` +WORKDIR="$( pwd )" -clean_jellyfin ../.. Release `pwd`/dist/jellyfin_${VERSION} +package_temporary_dir="${WORKDIR}/pkg-dist-tmp" +output_dir="${WORKDIR}/pkg-dist" +current_user="$( whoami )" +image_name="jellyfin-windows-x64-build" + +rm -rf "${package_temporary_dir}" &>/dev/null \ + || sudo rm -rf "${package_temporary_dir}" &>/dev/null + +rm -rf "${output_dir}" &>/dev/null \ + || sudo rm -rf "${output_dir}" &>/dev/null + +if [[ ${keep_artifacts} == 'n' ]]; then + docker_sudo="" + if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ + && [[ ! ${EUID:-1000} -eq 0 ]] \ + && [[ ! ${USER} == "root" ]] \ + && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then + docker_sudo=sudo + fi + ${docker_sudo} docker image rm ${image_name} --force +fi diff --git a/deployment/win-x64/dependencies.txt b/deployment/win-x64/dependencies.txt index 3d25d1bdf..bdb967096 100644 --- a/deployment/win-x64/dependencies.txt +++ b/deployment/win-x64/dependencies.txt @@ -1 +1 @@ -dotnet +docker diff --git a/deployment/win-x64/docker-build.sh b/deployment/win-x64/docker-build.sh new file mode 100755 index 000000000..20bf430c8 --- /dev/null +++ b/deployment/win-x64/docker-build.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# Builds the ZIP archive inside the Docker container + +set -o errexit +set -o xtrace + +# Version variables +NSSM_VERSION="nssm-2.24-101-g897c7ad" +NSSM_URL="https://nssm.cc/ci/${NSSM_VERSION}.zip" +FFMPEG_VERSION="ffmpeg-4.0.2-win64-static" +FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win64/static/${FFMPEG_VERSION}.zip" + +# Move to source directory +pushd ${SOURCE_DIR} + +# Clone down and build Web frontend +web_build_dir="$( mktemp -d )" +web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web" +git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/ +pushd ${web_build_dir} +if [[ -n ${web_branch} ]]; then + checkout -b origin/${web_branch} +fi +yarn install +mkdir -p ${web_target} +mv dist/* ${web_target}/ +popd +rm -rf ${web_build_dir} + +# Get version +version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )" + +# Build binary +dotnet publish --configuration Release --self-contained --runtime win-x64 --output /dist/jellyfin_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true" + +# Prepare addins +addin_build_dir="$( mktemp -d )" +wget ${NSSM_URL} -O ${addin_build_dir}/nssm.zip +wget ${FFMPEG_URL} -O ${addin_build_dir}/ffmpeg.zip +unzip ${addin_build_dir}/nssm.zip -d ${addin_build_dir} +cp ${addin_build_dir}/${NSSM_VERSION}/win64/nssm.exe /dist/jellyfin_${version}/nssm.exe +unzip ${addin_build_dir}/ffmpeg.zip -d ${addin_build_dir} +cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffmpeg.exe /dist/jellyfin_${version}/ffmpeg.exe +cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffprobe.exe /dist/jellyfin_${version}/ffprobe.exe +rm -rf ${addin_build_dir} + +# Prepare scripts +cp ${SOURCE_DIR}/deployment/windows/legacy/install-jellyfin.ps1 /dist/jellyfin_${version}/install-jellyfin.ps1 +cp ${SOURCE_DIR}/deployment/windows/legacy/install.bat /dist/jellyfin_${version}/install.bat + +# Create zip package +pushd /dist +zip -r /jellyfin_${version}.portable.zip jellyfin_${version} +popd +rm -rf /dist/jellyfin_${version} + +# Move the artifacts out +mkdir -p ${ARTIFACT_DIR}/ +mv /jellyfin[-_]*.zip ${ARTIFACT_DIR}/ +chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/win-x64/package.sh b/deployment/win-x64/package.sh index b438c28e4..a8ab190fa 100755 --- a/deployment/win-x64/package.sh +++ b/deployment/win-x64/package.sh @@ -1,47 +1,34 @@ #!/usr/bin/env bash -set -x -package_win64() ( - local NSSM_VERSION="nssm-2.24-101-g897c7ad" - local NSSM_URL="https://nssm.cc/ci/${NSSM_VERSION}.zip" - local FFMPEG_VERSION="ffmpeg-4.0.2-win64-static" - local FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win64/static/${FFMPEG_VERSION}.zip" - local ROOT=${1-$DEFAULT_ROOT} - local OUTPUT_DIR=${2-$DEFAULT_OUTPUT_DIR} - local PKG_DIR=${3-$DEFAULT_PKG_DIR} - local ARCHIVE_CMD="zip -r" - # Package portable build result - if [ -d ${OUTPUT_DIR} ]; then - echo -e "${CYAN}Packaging build in '${OUTPUT_DIR}' for `basename "${OUTPUT_DIR}"` to '${PKG_DIR}' with root '${ROOT}'.${NC}" - local TEMP_DIR="$(mktemp -d)" - wget ${NSSM_URL} -O ${TEMP_DIR}/nssm.zip - wget ${FFMPEG_URL} -O ${TEMP_DIR}/ffmpeg.zip - unzip ${TEMP_DIR}/nssm.zip -d $TEMP_DIR - cp ${TEMP_DIR}/${NSSM_VERSION}/win64/nssm.exe ${OUTPUT_DIR}/nssm.exe - unzip ${TEMP_DIR}/ffmpeg.zip -d $TEMP_DIR - cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffmpeg.exe ${OUTPUT_DIR}/ffmpeg.exe - cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffprobe.exe ${OUTPUT_DIR}/ffprobe.exe - rm -r ${TEMP_DIR} - cp ${ROOT}/deployment/windows/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1 - cp ${ROOT}/deployment/windows/install.bat ${OUTPUT_DIR}/install.bat - mkdir -p ${PKG_DIR} - pushd ${OUTPUT_DIR} - ${ARCHIVE_CMD} ${ROOT}/${PKG_DIR}/`basename "${OUTPUT_DIR}"`.zip . - popd - local EXIT_CODE=$? - if [ $EXIT_CODE -eq 0 ]; then - echo -e "${GREEN}[DONE] Packaging build in '${OUTPUT_DIR}' for `basename "${OUTPUT_DIR}"` to '${PKG_DIR}' with root '${ROOT}' complete.${NC}" - else - echo -e "${RED}[FAIL] Packaging build in '${OUTPUT_DIR}' for `basename "${OUTPUT_DIR}"` to '${PKG_DIR}' with root '${ROOT}' FAILED.${NC}" - fi - else - echo -e "${RED}[FAIL] Build artifacts do not exist for ${OUTPUT_DIR}. Run build.sh first.${NC}" - fi -) -source ../common.build.sh +args="${@}" +declare -a docker_envvars +for arg in ${args}; do + docker_envvars+=("-e ${arg}") +done -VERSION=`get_version ../..` +WORKDIR="$( pwd )" -package_win64 ../.. `pwd`/dist/jellyfin_${VERSION} +package_temporary_dir="${WORKDIR}/pkg-dist-tmp" +output_dir="${WORKDIR}/pkg-dist" +current_user="$( whoami )" +image_name="jellyfin-windows-x64-build" -#TODO setup and maybe change above code to produce the Windows native zip format. +# Determine if sudo should be used for Docker +if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ + && [[ ! ${EUID:-1000} -eq 0 ]] \ + && [[ ! ${USER} == "root" ]] \ + && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then + docker_sudo="sudo" +else + docker_sudo="" +fi + +# Prepare temporary package dir +mkdir -p "${package_temporary_dir}" +# Set up the build environment Docker image +${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile +# Build the DEBs and copy out to ${package_temporary_dir} +${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} +# Move the DEBs to the output directory +mkdir -p "${output_dir}" +mv "${package_temporary_dir}"/* "${output_dir}" diff --git a/deployment/win-x86/Dockerfile b/deployment/win-x86/Dockerfile new file mode 100644 index 000000000..fb5f5d6b6 --- /dev/null +++ b/deployment/win-x86/Dockerfile @@ -0,0 +1,37 @@ +FROM debian:10 +# Docker build arguments +ARG SOURCE_DIR=/jellyfin +ARG PLATFORM_DIR=/jellyfin/deployment/win-x86 +ARG ARTIFACT_DIR=/dist +ARG SDK_VERSION=2.2 +# Docker run environment +ENV SOURCE_DIR=/jellyfin +ENV ARTIFACT_DIR=/dist +ENV DEB_BUILD_OPTIONS=noddebs +ENV ARCH=amd64 + +# Prepare Debian build environment +RUN apt-get update \ + && apt-get install -y apt-transport-https debhelper gnupg wget devscripts mmv libc6-dev libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 zip + +# Install dotnet repository +# https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current +RUN wget https://download.visualstudio.microsoft.com/download/pr/228832ea-805f-45ab-8c88-fa36165701b9/16ce29a06031eeb09058dee94d6f5330/dotnet-sdk-2.2.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ + && mkdir -p dotnet-sdk \ + && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ + && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet + +# Install yarn package manager +RUN wget -q -O- https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ + && echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ + && apt update \ + && apt install -y yarn + +# Link to docker-build script +RUN ln -sf ${PLATFORM_DIR}/docker-build.sh /docker-build.sh + +VOLUME ${ARTIFACT_DIR}/ + +COPY . ${SOURCE_DIR}/ + +ENTRYPOINT ["/docker-build.sh"] diff --git a/deployment/win-x86/build.sh b/deployment/win-x86/build.sh deleted file mode 100755 index 610db356a..000000000 --- a/deployment/win-x86/build.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -source ../common.build.sh - -VERSION=`get_version ../..` - -build_jellyfin ../../Jellyfin.Server Release win-x86 `pwd`/dist/jellyfin_${VERSION} diff --git a/deployment/win-x86/clean.sh b/deployment/win-x86/clean.sh index 3df2d7796..8b78c5e4b 100755 --- a/deployment/win-x86/clean.sh +++ b/deployment/win-x86/clean.sh @@ -1,7 +1,27 @@ #!/usr/bin/env bash -source ../common.build.sh +keep_artifacts="${1}" -VERSION=`get_version ../..` +WORKDIR="$( pwd )" -clean_jellyfin ../.. Release `pwd`/dist/jellyfin_${VERSION} +package_temporary_dir="${WORKDIR}/pkg-dist-tmp" +output_dir="${WORKDIR}/pkg-dist" +current_user="$( whoami )" +image_name="jellyfin-windows-x86-build" + +rm -rf "${package_temporary_dir}" &>/dev/null \ + || sudo rm -rf "${package_temporary_dir}" &>/dev/null + +rm -rf "${output_dir}" &>/dev/null \ + || sudo rm -rf "${output_dir}" &>/dev/null + +if [[ ${keep_artifacts} == 'n' ]]; then + docker_sudo="" + if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ + && [[ ! ${EUID:-1000} -eq 0 ]] \ + && [[ ! ${USER} == "root" ]] \ + && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then + docker_sudo=sudo + fi + ${docker_sudo} docker image rm ${image_name} --force +fi diff --git a/deployment/win-x86/dependencies.txt b/deployment/win-x86/dependencies.txt index 3d25d1bdf..bdb967096 100644 --- a/deployment/win-x86/dependencies.txt +++ b/deployment/win-x86/dependencies.txt @@ -1 +1 @@ -dotnet +docker diff --git a/deployment/win-x86/docker-build.sh b/deployment/win-x86/docker-build.sh new file mode 100755 index 000000000..c5f6e82e7 --- /dev/null +++ b/deployment/win-x86/docker-build.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# Builds the ZIP archive inside the Docker container + +set -o errexit +set -o xtrace + +# Version variables +NSSM_VERSION="nssm-2.24-101-g897c7ad" +NSSM_URL="https://nssm.cc/ci/${NSSM_VERSION}.zip" +FFMPEG_VERSION="ffmpeg-4.0.2-win32-static" +FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win32/static/${FFMPEG_VERSION}.zip" + +# Move to source directory +pushd ${SOURCE_DIR} + +# Clone down and build Web frontend +web_build_dir="$( mktemp -d )" +web_target="${SOURCE_DIR}/MediaBrowser.WebDashboard/jellyfin-web" +git clone https://github.com/jellyfin/jellyfin-web.git ${web_build_dir}/ +pushd ${web_build_dir} +if [[ -n ${web_branch} ]]; then + checkout -b origin/${web_branch} +fi +yarn install +mkdir -p ${web_target} +mv dist/* ${web_target}/ +popd +rm -rf ${web_build_dir} + +# Get version +version="$( grep "version:" ./build.yaml | sed -E 's/version: "([0-9\.]+.*)"/\1/' )" + +# Build binary +dotnet publish --configuration Release --self-contained --runtime win-x86 --output /dist/jellyfin_${version}/ "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none;UseAppHost=true" + +# Prepare addins +addin_build_dir="$( mktemp -d )" +wget ${NSSM_URL} -O ${addin_build_dir}/nssm.zip +wget ${FFMPEG_URL} -O ${addin_build_dir}/ffmpeg.zip +unzip ${addin_build_dir}/nssm.zip -d ${addin_build_dir} +cp ${addin_build_dir}/${NSSM_VERSION}/win64/nssm.exe /dist/jellyfin_${version}/nssm.exe +unzip ${addin_build_dir}/ffmpeg.zip -d ${addin_build_dir} +cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffmpeg.exe /dist/jellyfin_${version}/ffmpeg.exe +cp ${addin_build_dir}/${FFMPEG_VERSION}/bin/ffprobe.exe /dist/jellyfin_${version}/ffprobe.exe +rm -rf ${addin_build_dir} + +# Prepare scripts +cp ${SOURCE_DIR}/deployment/windows/legacy/install-jellyfin.ps1 /dist/jellyfin_${version}/install-jellyfin.ps1 +cp ${SOURCE_DIR}/deployment/windows/legacy/install.bat /dist/jellyfin_${version}/install.bat + +# Create zip package +pushd /dist +zip -r /jellyfin_${version}.portable.zip jellyfin_${version} +popd +rm -rf /dist/jellyfin_${version} + +# Move the artifacts out +mkdir -p ${ARTIFACT_DIR}/ +mv /jellyfin[-_]*.zip ${ARTIFACT_DIR}/ +chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} diff --git a/deployment/win-x86/package.sh b/deployment/win-x86/package.sh index 8752d92a8..65e7e2928 100755 --- a/deployment/win-x86/package.sh +++ b/deployment/win-x86/package.sh @@ -1,45 +1,34 @@ #!/usr/bin/env bash -package_win32() ( - local NSSM_VERSION="nssm-2.24-101-g897c7ad" - local NSSM_URL="https://nssm.cc/ci/${NSSM_VERSION}.zip" - local FFMPEG_VERSION="ffmpeg-4.0.2-win32-static" - local FFMPEG_URL="https://ffmpeg.zeranoe.com/builds/win32/static/${FFMPEG_VERSION}.zip" - local ROOT=${1-$DEFAULT_ROOT} - local OUTPUT_DIR=${2-$DEFAULT_OUTPUT_DIR} - local PKG_DIR=${3-$DEFAULT_PKG_DIR} - local ARCHIVE_CMD="zip -r" - # Package portable build result - if [ -d ${OUTPUT_DIR} ]; then - echo -e "${CYAN}Packaging build in '${OUTPUT_DIR}' for `basename "${OUTPUT_DIR}"` to '${PKG_DIR}' with root '${ROOT}'.${NC}" - local TEMP_DIR="$(mktemp -d)" - wget ${NSSM_URL} -O ${TEMP_DIR}/nssm.zip - wget ${FFMPEG_URL} -O ${TEMP_DIR}/ffmpeg.zip - unzip ${TEMP_DIR}/nssm.zip -d $TEMP_DIR - cp ${TEMP_DIR}/${NSSM_VERSION}/win32/nssm.exe ${OUTPUT_DIR}/nssm.exe - unzip ${TEMP_DIR}/ffmpeg.zip -d $TEMP_DIR - cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffmpeg.exe ${OUTPUT_DIR}/ffmpeg.exe - cp ${TEMP_DIR}/${FFMPEG_VERSION}/bin/ffprobe.exe ${OUTPUT_DIR}/ffprobe.exe - rm -r ${TEMP_DIR} - cp ${ROOT}/deployment/windows/install-jellyfin.ps1 ${OUTPUT_DIR}/install-jellyfin.ps1 - cp ${ROOT}/deployment/windows/install.bat ${OUTPUT_DIR}/install.bat - mkdir -p ${PKG_DIR} - pushd ${OUTPUT_DIR} - ${ARCHIVE_CMD} ${ROOT}/${PKG_DIR}/`basename "${OUTPUT_DIR}"`.zip . - popd - local EXIT_CODE=$? - if [ $EXIT_CODE -eq 0 ]; then - echo -e "${GREEN}[DONE] Packaging build in '${OUTPUT_DIR}' for `basename "${OUTPUT_DIR}"` to '${PKG_DIR}' with root '${ROOT}' complete.${NC}" - else - echo -e "${RED}[FAIL] Packaging build in '${OUTPUT_DIR}' for `basename "${OUTPUT_DIR}"` to '${PKG_DIR}' with root '${ROOT}' FAILED.${NC}" - fi - else - echo -e "${RED}[FAIL] Build artifacts do not exist for ${OUTPUT_DIR}. Run build.sh first.${NC}" - fi -) -source ../common.build.sh -VERSION=`get_version ../..` +args="${@}" +declare -a docker_envvars +for arg in ${args}; do + docker_envvars+=("-e ${arg}") +done -package_win32 ../.. `pwd`/dist/jellyfin_${VERSION} +WORKDIR="$( pwd )" -#TODO setup and maybe change above code to produce the Windows native zip format. +package_temporary_dir="${WORKDIR}/pkg-dist-tmp" +output_dir="${WORKDIR}/pkg-dist" +current_user="$( whoami )" +image_name="jellyfin-windows-x86-build" + +# Determine if sudo should be used for Docker +if [[ ! -z $(id -Gn | grep -q 'docker') ]] \ + && [[ ! ${EUID:-1000} -eq 0 ]] \ + && [[ ! ${USER} == "root" ]] \ + && [[ ! -z $( echo "${OSTYPE}" | grep -q "darwin" ) ]]; then + docker_sudo="sudo" +else + docker_sudo="" +fi + +# Prepare temporary package dir +mkdir -p "${package_temporary_dir}" +# Set up the build environment Docker image +${docker_sudo} docker build ../.. -t "${image_name}" -f ./Dockerfile +# Build the DEBs and copy out to ${package_temporary_dir} +${docker_sudo} docker run --rm -v "${package_temporary_dir}:/dist" "${image_name}" ${docker_envvars} +# Move the DEBs to the output directory +mkdir -p "${output_dir}" +mv "${package_temporary_dir}"/* "${output_dir}" diff --git a/deployment/windows/build-jellyfin.ps1 b/deployment/windows/build-jellyfin.ps1 index 2999912b3..c4fb4b995 100644 --- a/deployment/windows/build-jellyfin.ps1 +++ b/deployment/windows/build-jellyfin.ps1 @@ -1,9 +1,13 @@ [CmdletBinding()] param( + [switch]$MakeNSIS, + [switch]$InstallNSIS, [switch]$InstallFFMPEG, [switch]$InstallNSSM, + [switch]$SkipJellyfinBuild, [switch]$GenerateZip, - [string]$InstallLocation = "$Env:AppData/Jellyfin-Server/", + [string]$InstallLocation = "./dist/jellyfin-win-nsis", + [string]$UXLocation = "../jellyfin-ux", [ValidateSet('Debug','Release')][string]$BuildType = 'Release', [ValidateSet('Quiet','Minimal', 'Normal')][string]$DotNetVerbosity = 'Minimal', [ValidateSet('win','win7', 'win8','win81','win10')][string]$WindowsVersion = 'win', @@ -16,6 +20,10 @@ if(($PSVersionTable.PSEdition -eq 'Core') -and (-not $IsWindows)){ }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')){ @@ -27,14 +35,14 @@ function Build-JellyFin { exit } Write-Verbose "windowsversion-Architecture: $windowsversion-$Architecture" - Write-Verbose "InstallLocation: $InstallLocation" + Write-Verbose "InstallLocation: $ResolvedInstallLocation" Write-Verbose "DotNetVerbosity: $DotNetVerbosity" - dotnet publish -c $BuildType -r `"$windowsversion-$Architecture`" MediaBrowser.sln -o $InstallLocation -v $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]$InstallLocation, + [string]$ResolvedInstallLocation, [string]$Architecture ) Write-Verbose "Checking Architecture" @@ -43,31 +51,31 @@ function Install-FFMPEG { Write-Warning "FFMPEG will not be installed" }elseif($Architecture -eq 'x64'){ Write-Verbose "Downloading 64 bit FFMPEG" - Invoke-WebRequest -Uri https://ffmpeg.zeranoe.com/builds/win64/static/ffmpeg-4.0.2-win64-static.zip -UseBasicParsing -OutFile "$tempdir/fmmpeg.zip" | Write-Verbose + 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/static/ffmpeg-4.0.2-win32-static.zip -UseBasicParsing -OutFile "$tempdir/fmmpeg.zip" | Write-Verbose + Invoke-WebRequest -Uri https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.0.2-win32-shared.zip -UseBasicParsing -OutFile "$tempdir/ffmpeg.zip" | Write-Verbose } - Expand-Archive "$tempdir/fmmpeg.zip" -DestinationPath "$tempdir/ffmpeg/" | 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/ffmpeg-4.0.2-win64-static/bin" | ForEach-Object { + 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/ffmpeg-4.0.2-win32-static/bin" | ForEach-Object { + Get-ChildItem "$tempdir/ffmpeg/ffmpeg-4.0.2-win32-shared/bin" | ForEach-Object { Copy-Item $_.FullName -Destination $installLocation | Write-Verbose } } Remove-Item "$tempdir/ffmpeg/" -Recurse -Force -ErrorAction Continue | Write-Verbose - Remove-Item "$tempdir/fmmpeg.zip" -Force -ErrorAction Continue | Write-Verbose + Remove-Item "$tempdir/ffmpeg.zip" -Force -ErrorAction Continue | Write-Verbose } function Install-NSSM { param( - [string]$InstallLocation, + [string]$ResolvedInstallLocation, [string]$Architecture ) Write-Verbose "Checking Architecture" @@ -80,7 +88,7 @@ function Install-NSSM { Invoke-WebRequest -Uri https://nssm.cc/ci/nssm-2.24-101-g897c7ad.zip -UseBasicParsing -OutFile "$tempdir/nssm.zip" | Write-Verbose } - Expand-Archive "$tempdir/nssm.zip" -DestinationPath "$tempdir/nssm/" | 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 { @@ -96,19 +104,61 @@ function Install-NSSM { Remove-Item "$tempdir/nssm.zip" -Force -ErrorAction Continue | Write-Verbose } -Write-Verbose "Starting Build Process: Selected Environment is $WindowsVersion-$Architecture" -Build-JellyFin +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 +} +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 $InstallLocation $Architecture + Install-FFMPEG $ResolvedInstallLocation $Architecture } if($InstallNSSM.IsPresent -or ($InstallNSSM -eq $true)){ Write-Verbose "Starting NSSM Install" - Install-NSSM $InstallLocation $Architecture + Install-NSSM $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 } -Copy-Item .\deployment\windows\install-jellyfin.ps1 $InstallLocation\install-jellyfin.ps1 -Copy-Item .\deployment\windows\install.bat $InstallLocation\install.bat if($GenerateZip.IsPresent -or ($GenerateZip -eq $true)){ - Compress-Archive -Path $InstallLocation -DestinationPath "$InstallLocation/jellyfin.zip" -Force + Compress-Archive -Path $ResolvedInstallLocation -DestinationPath "$ResolvedInstallLocation/jellyfin.zip" -Force } Write-Verbose "Finished" diff --git a/deployment/windows/dependencies.txt b/deployment/windows/dependencies.txt index 3d25d1bdf..16f77cce7 100644 --- a/deployment/windows/dependencies.txt +++ b/deployment/windows/dependencies.txt @@ -1 +1,2 @@ dotnet +nsis diff --git a/deployment/windows/dialogs/confirmation.nsddef b/deployment/windows/dialogs/confirmation.nsddef new file mode 100644 index 000000000..969ebacd6 --- /dev/null +++ b/deployment/windows/dialogs/confirmation.nsddef @@ -0,0 +1,24 @@ +<?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 new file mode 100644 index 000000000..f00e9b43a --- /dev/null +++ b/deployment/windows/dialogs/confirmation.nsdinc @@ -0,0 +1,61 @@ +; ========================================================= +; 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 new file mode 100644 index 000000000..3509ada24 --- /dev/null +++ b/deployment/windows/dialogs/service-config.nsddef @@ -0,0 +1,13 @@ +<?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 new file mode 100644 index 000000000..58c350f2e --- /dev/null +++ b/deployment/windows/dialogs/service-config.nsdinc @@ -0,0 +1,56 @@ +; ========================================================= +; 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/helpers/ShowError.nsh b/deployment/windows/helpers/ShowError.nsh new file mode 100644 index 000000000..6e09b1e40 --- /dev/null +++ b/deployment/windows/helpers/ShowError.nsh @@ -0,0 +1,10 @@ +; 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 new file mode 100644 index 000000000..b8aa771aa --- /dev/null +++ b/deployment/windows/helpers/StrSlash.nsh @@ -0,0 +1,47 @@ +; 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 new file mode 100644 index 000000000..e33efde91 --- /dev/null +++ b/deployment/windows/jellyfin.nsi @@ -0,0 +1,471 @@ +; Shows a lot of debug information while compiling +; This can be removed once stable. +!verbose 4 +SetCompressor lzma +ShowInstDetails show +ShowUninstDetails show +;-------------------------------- +!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 _INSTALLSERVICE_ + Var _SERVICESTART_ + Var _SERVICEACCOUNTTYPE_ + Var _EXISTINGINSTALLATION_ + Var _EXISTINGSERVICE_ +; +!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-uninstall.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 +; Components Page + !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\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 /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.media/" + 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 + + ExecWait '"$INSTDIR\nssm.exe" statuscode JellyfinServer' $0 + DetailPrint "Jellyfin Server service statuscode, $0" + ${If} $0 == 0 + InstallRetry: + ExecWait '"$INSTDIR\nssm.exe" install JellyfinServer "$INSTDIR\jellyfin.exe" --datadir \"$_JELLYFINDATADIR_\"' $0 + ${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 --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} + +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 + +;-------------------------------- +;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 + + 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 + + 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" + + 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 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" + + ; check if service was run using Network Service account + ClearErrors + ReadRegStr $_SERVICEACCOUNTTYPE_ HKLM "${REG_CONFIG_KEY}" "ServiceAccountType" ; in case of error _SERVICEACCOUNTTYPE_ will be NetworkService as default + + ClearErrors + ReadRegStr $_JELLYFINDATADIR_ HKLM "${REG_CONFIG_KEY}" "DataFolder" ; in case of error, the default holds + + ; Hide sections which will not be needed in case of previous install + ; SectionSetText ${InstallService} "" + + NoService: ; existing install was present but no service was detected + +; 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 + +; 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 + +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/install-jellyfin.ps1 b/deployment/windows/legacy/install-jellyfin.ps1 index e909a0468..e909a0468 100644 --- a/deployment/windows/install-jellyfin.ps1 +++ b/deployment/windows/legacy/install-jellyfin.ps1 diff --git a/deployment/windows/install.bat b/deployment/windows/legacy/install.bat index e21479a79..e21479a79 100644 --- a/deployment/windows/install.bat +++ b/deployment/windows/legacy/install.bat diff --git a/jellyfin.ruleset b/jellyfin.ruleset index 1249a60c0..e259131b0 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -8,6 +8,8 @@ <!-- disable warning SA1101: Prefix local calls with 'this.' --> <Rule Id="SA1101" Action="None" /> + <!-- disable warning SA1108: Block statements should not contain embedded comments --> + <Rule Id="SA1108" Action="None" /> <!-- disable warning SA1130: Use lambda syntax --> <Rule Id="SA1130" Action="None" /> <!-- disable warning SA1200: 'using' directive must appear within a namespace declaration --> @@ -27,6 +29,10 @@ <Rule Id="CA1031" Action="Info" /> <!-- disable warning CA1062: Validate arguments of public methods --> <Rule Id="CA1062" Action="Info" /> + <!-- disable warning CA1812: internal class that is apparently never instantiated. + If so, remove the code from the assembly. + If this class is intended to contain only static members, make it static --> + <Rule Id="CA1812" Action="Info" /> <!-- disable warning CA1822: Member does not access instance data and can be marked as static --> <Rule Id="CA1822" Action="Info" /> @@ -34,5 +40,9 @@ <Rule Id="CA1054" Action="None" /> <!-- disable warning CA1303: Do not pass literals as localized parameters --> <Rule Id="CA1303" Action="None" /> + <!-- disable warning CA1308: Normalize strings to uppercase --> + <Rule Id="CA1308" Action="None" /> + <!-- disable warning CA2000: Dispose objects before losing scope --> + <Rule Id="CA2000" Action="None" /> </Rules> </RuleSet> diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj new file mode 100644 index 000000000..bb40985a4 --- /dev/null +++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj @@ -0,0 +1,19 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>netcoreapp3.0</TargetFramework> + <IsPackable>false</IsPackable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" /> + <PackageReference Include="xunit" Version="2.4.1" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> + <PackageReference Include="coverlet.collector" Version="1.1.0" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="../../MediaBrowser.Common/MediaBrowser.Common.csproj" /> + </ItemGroup> + +</Project> diff --git a/tests/Jellyfin.Common.Tests/PasswordHashTests.cs b/tests/Jellyfin.Common.Tests/PasswordHashTests.cs new file mode 100644 index 000000000..5fa86f3bd --- /dev/null +++ b/tests/Jellyfin.Common.Tests/PasswordHashTests.cs @@ -0,0 +1,29 @@ +using MediaBrowser.Common.Cryptography; +using Xunit; +using static MediaBrowser.Common.HexHelper; + +namespace Jellyfin.Common.Tests +{ + public class PasswordHashTests + { + [Theory] + [InlineData("$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D", + "PBKDF2", + "", + "62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] + public void ParseTest(string passwordHash, string id, string salt, string hash) + { + var pass = PasswordHash.Parse(passwordHash); + Assert.Equal(id, pass.Id); + Assert.Equal(salt, ToHexString(pass.Salt)); + Assert.Equal(hash, ToHexString(pass.Hash)); + } + + [Theory] + [InlineData("$PBKDF2$iterations=1000$62FBA410AFCA5B4475F35137AB2E8596B127E4D927BA23F6CC05C067E897042D")] + public void ToStringTest(string passwordHash) + { + Assert.Equal(passwordHash, PasswordHash.Parse(passwordHash).ToString()); + } + } +} diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs new file mode 100644 index 000000000..a7848316e --- /dev/null +++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTests.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using MediaBrowser.MediaEncoding.Encoder; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace Jellyfin.MediaEncoding.Tests +{ + public class EncoderValidatorTests + { + private class GetFFmpegVersionTestData : IEnumerable<object[]> + { + public IEnumerator<object[]> GetEnumerator() + { + yield return new object[] { EncoderValidatorTestsData.FFmpegV421Output, new Version(4, 2, 1) }; + yield return new object[] { EncoderValidatorTestsData.FFmpegV42Output, new Version(4, 2) }; + yield return new object[] { EncoderValidatorTestsData.FFmpegV414Output, new Version(4, 1, 4) }; + yield return new object[] { EncoderValidatorTestsData.FFmpegV404Output, new Version(4, 0, 4) }; + yield return new object[] { EncoderValidatorTestsData.FFmpegGitUnknownOutput, null }; + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + [Theory] + [ClassData(typeof(GetFFmpegVersionTestData))] + public void GetFFmpegVersionTest(string versionOutput, Version version) + { + Assert.Equal(version, EncoderValidator.GetFFmpegVersion(versionOutput)); + } + + [Theory] + [InlineData(EncoderValidatorTestsData.FFmpegV421Output, true)] + [InlineData(EncoderValidatorTestsData.FFmpegV42Output, true)] + [InlineData(EncoderValidatorTestsData.FFmpegV414Output, true)] + [InlineData(EncoderValidatorTestsData.FFmpegV404Output, true)] + [InlineData(EncoderValidatorTestsData.FFmpegGitUnknownOutput, false)] + public void ValidateVersionInternalTest(string versionOutput, bool valid) + { + var val = new EncoderValidator(new NullLogger<EncoderValidatorTests>()); + Assert.Equal(valid, val.ValidateVersionInternal(versionOutput)); + } + } +} diff --git a/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs new file mode 100644 index 000000000..12fde0770 --- /dev/null +++ b/tests/Jellyfin.MediaEncoding.Tests/EncoderValidatorTestsData.cs @@ -0,0 +1,66 @@ +namespace Jellyfin.MediaEncoding.Tests +{ + internal static class EncoderValidatorTestsData + { + public const string FFmpegV421Output = @"ffmpeg version 4.2.1 Copyright (c) 2000-2019 the FFmpeg developers +built with gcc 9.1.1 (GCC) 20190807 +configuration: --enable-gpl --enable-version3 --enable-sdl2 --enable-fontconfig --enable-gnutls --enable-iconv --enable-libass --enable-libdav1d --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libopus --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libtheora --enable-libtwolame --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libzimg --enable-lzma --enable-zlib --enable-gmp --enable-libvidstab --enable-libvorbis --enable-libvo-amrwbenc --enable-libmysofa --enable-libspeex --enable-libxvid --enable-libaom --enable-libmfx --enable-amf --enable-ffnvcodec --enable-cuvid --enable-d3d11va --enable-nvenc --enable-nvdec --enable-dxva2 --enable-avisynth --enable-libopenmpt +libavutil 56. 31.100 / 56. 31.100 +libavcodec 58. 54.100 / 58. 54.100 +libavformat 58. 29.100 / 58. 29.100 +libavdevice 58. 8.100 / 58. 8.100 +libavfilter 7. 57.100 / 7. 57.100 +libswscale 5. 5.100 / 5. 5.100 +libswresample 3. 5.100 / 3. 5.100 +libpostproc 55. 5.100 / 55. 5.100"; + + public const string FFmpegV42Output = @"ffmpeg version n4.2 Copyright (c) 2000-2019 the FFmpeg developers +built with gcc 9.1.0 (GCC) +configuration: --prefix=/usr --disable-debug --disable-static --disable-stripping --enable-fontconfig --enable-gmp --enable-gnutls --enable-gpl --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libdav1d --enable-libdrm --enable-libfreetype --enable-libfribidi --enable-libgsm --enable-libiec61883 --enable-libjack --enable-libmodplug --enable-libmp3lame --enable-libopencore_amrnb --enable-libopencore_amrwb --enable-libopenjpeg --enable-libopus --enable-libpulse --enable-libsoxr --enable-libspeex --enable-libssh --enable-libtheora --enable-libv4l2 --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxcb --enable-libxml2 --enable-libxvid --enable-nvdec --enable-nvenc --enable-omx --enable-shared --enable-version3 +libavutil 56. 31.100 / 56. 31.100 +libavcodec 58. 54.100 / 58. 54.100 +libavformat 58. 29.100 / 58. 29.100 +libavdevice 58. 8.100 / 58. 8.100 +libavfilter 7. 57.100 / 7. 57.100 +libswscale 5. 5.100 / 5. 5.100 +libswresample 3. 5.100 / 3. 5.100 +libpostproc 55. 5.100 / 55. 5.100"; + + public const string FFmpegV414Output = @"ffmpeg version 4.1.4-1~deb10u1 Copyright (c) 2000-2019 the FFmpeg developers +built with gcc 8 (Raspbian 8.3.0-6+rpi1) +configuration: --prefix=/usr --extra-version='1~deb10u1' --toolchain=hardened --libdir=/usr/lib/arm-linux-gnueabihf --incdir=/usr/include/arm-linux-gnueabihf --arch=arm --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-avisynth --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opengl --enable-sdl2 --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libx264 --enable-shared +libavutil 56. 22.100 / 56. 22.100 +libavcodec 58. 35.100 / 58. 35.100 +libavformat 58. 20.100 / 58. 20.100 +libavdevice 58. 5.100 / 58. 5.100 +libavfilter 7. 40.101 / 7. 40.101 +libavresample 4. 0. 0 / 4. 0. 0 +libswscale 5. 3.100 / 5. 3.100 +libswresample 3. 3.100 / 3. 3.100 +libpostproc 55. 3.100 / 55. 3.100"; + + public const string FFmpegV404Output = @"ffmpeg version 4.0.4 Copyright (c) 2000-2019 the FFmpeg developers +built with gcc 8 (Debian 8.3.0-6) +configuration: --toolchain=hardened --prefix=/usr --target-os=linux --enable-cross-compile --extra-cflags=--static --enable-gpl --enable-static --disable-doc --disable-ffplay --disable-shared --disable-libxcb --disable-sdl2 --disable-xlib --enable-libfontconfig --enable-fontconfig --enable-gmp --enable-gnutls --enable-libass --enable-libbluray --enable-libdrm --enable-libfreetype --enable-libfribidi --enable-libmp3lame --enable-libopus --enable-libtheora --enable-libvorbis --enable-libwebp --enable-libx264 --enable-libx265 --enable-libzvbi --enable-omx --enable-omx-rpi --enable-version3 --enable-vaapi --enable-vdpau --arch=amd64 --enable-nvenc --enable-nvdec +libavutil 56. 14.100 / 56. 14.100 +libavcodec 58. 18.100 / 58. 18.100 +libavformat 58. 12.100 / 58. 12.100 +libavdevice 58. 3.100 / 58. 3.100 +libavfilter 7. 16.100 / 7. 16.100 +libswscale 5. 1.100 / 5. 1.100 +libswresample 3. 1.100 / 3. 1.100 +libpostproc 55. 1.100 / 55. 1.100"; + + public const string FFmpegGitUnknownOutput = @"ffmpeg version N-94303-g7cb4f8c962 Copyright (c) 2000-2019 the FFmpeg developers +built with gcc 9.1.1 (GCC) 20190716 +configuration: --enable-gpl --enable-version3 --enable-sdl2 --enable-fontconfig --enable-gnutls --enable-iconv --enable-libass --enable-libdav1d --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libopus --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libtheora --enable-libtwolame --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libzimg --enable-lzma --enable-zlib --enable-gmp --enable-libvidstab --enable-libvorbis --enable-libvo-amrwbenc --enable-libmysofa --enable-libspeex --enable-libxvid --enable-libaom --enable-libmfx --enable-amf --enable-ffnvcodec --enable-cuvid --enable-d3d11va --enable-nvenc --enable-nvdec --enable-dxva2 --enable-avisynth --enable-libopenmpt +libavutil 56. 30.100 / 56. 30.100 +libavcodec 58. 53.101 / 58. 53.101 +libavformat 58. 28.102 / 58. 28.102 +libavdevice 58. 7.100 / 58. 7.100 +libavfilter 7. 56.101 / 7. 56.101 +libswscale 5. 4.101 / 5. 4.101 +libswresample 3. 4.100 / 3. 4.100 +libpostproc 55. 4.100 / 55. 4.100"; + } +} diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj new file mode 100644 index 000000000..70e2d1851 --- /dev/null +++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj @@ -0,0 +1,19 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>netcoreapp3.0</TargetFramework> + <IsPackable>false</IsPackable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" /> + <PackageReference Include="xunit" Version="2.4.1" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> + <PackageReference Include="coverlet.collector" Version="1.1.0" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="../../MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj" /> + </ItemGroup> + +</Project> |
